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 Create( && bitsPerSample.Channel0 == 32, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new RgbFloat323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + return new RgbFloat323232TiffColor(configuration, isBigEndian: byteOrder == ByteOrder.BigEndian); case TiffColorType.RgbaFloat32323232: DebugGuard.IsTrue( @@ -383,7 +383,7 @@ public static TiffBaseColorDecoder Create( && bitsPerSample.Channel0 == 32, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new RgbaFloat32323232TiffColor(byteOrder == ByteOrder.BigEndian, extraSampleType); + return new RgbaFloat32323232TiffColor(configuration, byteOrder == ByteOrder.BigEndian, extraSampleType); case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffFloatGrayscaleColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffFloatGrayscaleColor{TPixel}.cs new file mode 100644 index 0000000000..d0396e3ea2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffFloatGrayscaleColor{TPixel}.cs @@ -0,0 +1,130 @@ +// 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; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; + +/// +/// Decodes 32-bit floating-point TIFF grayscale samples for both color interpretations. +/// +/// The destination pixel format. +internal abstract class TiffFloatGrayscaleColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel +{ + private readonly Configuration configuration; + private readonly bool isBigEndian; + private readonly bool whiteIsZero; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration used by bulk pixel conversion. + /// Whether the TIFF samples use big-endian byte order. + /// Whether stored intensities must be inverted. + protected TiffFloatGrayscaleColor(Configuration configuration, bool isBigEndian, bool whiteIsZero) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + this.whiteIsZero = whiteIsZero; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + const int BlockSize = 64; + Span vectors = stackalloc Vector4[BlockSize]; + bool reverseEndianness = this.isBigEndian == BitConverter.IsLittleEndian; + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < width; x += BlockSize) + { + int count = Math.Min(BlockSize, width - x); + int byteCount = count * sizeof(float); + Span block = vectors[..count]; + + // Expand the stored intensity before the pixel format applies its + // own numeric and alpha rules to the whole block. + ExpandFloatSingles(data.Slice(offset, byteCount), block, reverseEndianness, this.whiteIsZero); + PixelOperations.Instance.FromVector4Destructive( + this.configuration, + block, + pixelRow.Slice(x, count), + PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + offset += byteCount; + } + } + } + + /// + /// Expands single-component floating-point pixels into four-component vectors. + /// + /// The packed 32-bit intensity samples. + /// The destination vectors. + /// Whether each stored sample needs its bytes reversed. + /// Whether the stored intensity must be inverted. + private static void ExpandFloatSingles(ReadOnlySpan source, Span destination, bool reverseEndianness, bool whiteIsZero) + { + 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) + { + 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) + { + Vector128 packed = Vector128.LoadUnsafe(ref s, (nuint)i).AsByte(); + if (reverseEndianness) + { + packed = Vector128.ShuffleNative(packed, byteReverse); + } + + Vector128 intensities = packed.AsSingle(); + if (whiteIsZero) + { + // WhiteIsZero stores one minus intensity. Invert before broadcasting + // so all three color components receive the same decoded value. + intensities = Vector128.Create(1F) - intensities; + } + + // Each shuffle broadcasts one sample to X, Y, Z, and W. Replace W + // with opaque alpha without arithmetic on the stored intensity bits. + Vector128 pixel0 = Vector128_.ShuffleNative(intensities, 0x00).AsUInt32(); + Vector128 pixel1 = Vector128_.ShuffleNative(intensities, 0x55).AsUInt32(); + Vector128 pixel2 = Vector128_.ShuffleNative(intensities, 0xAA).AsUInt32(); + Vector128 pixel3 = Vector128_.ShuffleNative(intensities, 0xFF).AsUInt32(); + + 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)); + } + } + + for (; i < destination.Length; i++) + { + uint bits = reverseEndianness ? BinaryPrimitives.ReverseEndianness(samples[i]) : samples[i]; + float intensity = BitConverter.Int32BitsToSingle((int)bits); + intensity = whiteIsZero ? 1F - intensity : intensity; + destination[i] = new Vector4(intensity, intensity, intensity, 1F); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs index 7fe142d741..b008ba3b70 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{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 'WhiteIsZero' photometric interpretation for 32-bit float grayscale images. /// /// The type of pixel format. -internal class WhiteIsZero32FloatTiffColor : TiffBaseColorDecoder +internal class WhiteIsZero32FloatTiffColor : 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 WhiteIsZero32FloatTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; - - /// - public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + public WhiteIsZero32FloatTiffColor(Configuration configuration, bool isBigEndian) + : base(configuration, isBigEndian, whiteIsZero: true) { - 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 = 1.0f - 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 = 1.0f - BitConverter.ToSingle(data.Slice(offset, 4)); - offset += 4; - - pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(intensity, intensity, intensity, 1.0f)); - } - } - } } } diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 8e49669f20..d474b1c1d8 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -44,6 +44,12 @@ |Old Deflate (Technote 2) | | Y | | |Webp | | Y | | +For high-precision samples, the encoder and decoder support unsigned 16-bit and +32-bit RGB/RGBA samples (48, 64, 96, or 128 bits per pixel). They also support +32-bit floating-point grayscale, RGB, and RGBA samples (32, 96, or 128 bits per +pixel). Floating-point encoding supports None, PackBits, Lzw, and Deflate +compression. The floating-point predictor is not supported. + ### Photometric Interpretation Formats | |Encoder|Decoder|Comments | @@ -88,7 +94,7 @@ |MaxSampleValue | | | | |XResolution | Y | Y | | |YResolution | Y | Y | | -|PlanarConfiguration | | Y | Encoding support only chunky. | +|PlanarConfiguration | Y | Y | Encoder writes chunky; decoder supports chunky and planar. | |FreeOffsets | | | | |FreeByteCounts | | | | |GrayResponseUnit | | | | @@ -99,7 +105,7 @@ |Artist | Y | Y | | |HostComputer | Y | Y | | |ColorMap | Y | Y | | -|ExtraSamples | | Y | Unspecified alpha data is not supported. | +|ExtraSamples | Y | Y | Encoder writes unassociated integer alpha or associated/unassociated floating-point alpha. Unspecified alpha data is not supported. | |Copyright | Y | Y | | ### Extension TIFF Tags @@ -115,7 +121,7 @@ |T6Options | | | | |PageNumber | | | | |TransferFunction | | | | -|Predictor | Y | Y | only Horizontal | +|Predictor | Y | Y | Horizontal only; not used for floating-point samples. | |WhitePoint | | | | |PrimaryChromaticities | | | | |HalftoneHints | | | | @@ -132,7 +138,7 @@ |NumberOfInks | | - | | |DotRange | | | | |TargetPrinter | | | | -|SampleFormat | | - | | +|SampleFormat | Y | Y | Encoder writes IEEE floating-point; unsigned integer is the default. Signed and complex samples are not supported. | |SMinSampleValue | | | | |SMaxSampleValue | | | | |TransferRange | | | | diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs index 6cbacec354..79d3247921 100644 --- a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs @@ -48,8 +48,7 @@ public enum TiffBitsPerPixel Bit14 = 14, /// - /// 16 bits per pixel, for gray images. - /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 16 bits grayscale instead. + /// 16 bits per pixel, for grayscale images. /// Bit16 = 16, @@ -60,36 +59,44 @@ public enum TiffBitsPerPixel /// /// 30 bits per pixel. 10 bit for each color channel. - /// Note: The TiffEncoder does not yet support 10 bits per color channel and will default to 24 bits per pixel instead. + /// The TiffEncoder writes this request as 48 bits per pixel. /// Bit30 = 30, /// - /// 32 bits per pixel. One byte for each color channel. + /// 32 bits per pixel. Four 8-bit integer samples or one 32-bit floating-point sample. /// Bit32 = 32, /// /// 36 bits per pixel. 12 bit for each color channel. - /// Note: The TiffEncoder does not yet support 12 bits per color channel and will default to 24 bits per pixel instead. + /// The TiffEncoder writes this request as 48 bits per pixel. /// Bit36 = 36, /// /// 42 bits per pixel. 14 bit for each color channel. - /// Note: The TiffEncoder does not yet support 14 bits per color channel and will default to 24 bits per pixel instead. + /// The TiffEncoder writes this request as 48 bits per pixel. /// Bit42 = 42, /// - /// 48 bits per pixel. 16 bit for each color channel. - /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 24 bits per pixel instead. + /// 48 bits per pixel. Three 16-bit color samples. /// Bit48 = 48, /// - /// 64 bits per pixel. 16 bit for each color channel. - /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 32 bits per pixel instead. + /// 64 bits per pixel. Three 16-bit color samples and a 16-bit alpha sample. /// Bit64 = 64, + + /// + /// 96 bits per pixel, with three 32-bit color samples. + /// + Bit96 = 96, + + /// + /// 128 bits per pixel, with four 32-bit color and alpha samples. + /// + Bit128 = 128, } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs index e9dee4ee4d..0d0eba76f5 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -44,5 +45,37 @@ 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; + + TiffMetadata metadata = info.Metadata.GetTiffMetadata(); + if (metadata.SampleFormat == TiffSampleFormat.Float) + { + // TIFF samples are decoded once into the pixel type chosen from the root IFD. + return metadata.ExtraSampleType == TiffExtraSampleType.AssociatedAlphaData + ? this.Decode(options, stream, cancellationToken) + : this.Decode(options, stream, cancellationToken); + } + + if (metadata.SampleFormat == TiffSampleFormat.UnsignedInteger && + metadata.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb) + { + // Match the stored integer channel width when choosing the default pixel type. + return metadata.BitsPerPixel switch + { + TiffBitsPerPixel.Bit48 => this.Decode(options, stream, cancellationToken), + TiffBitsPerPixel.Bit64 => this.Decode(options, stream, cancellationToken), + TiffBitsPerPixel.Bit96 => this.Decode(options, stream, cancellationToken), + TiffBitsPerPixel.Bit128 => this.Decode(options, stream, cancellationToken), + _ => this.Decode(options, stream, cancellationToken), + }; + } + + return this.Decode(options, stream, cancellationToken); + } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 2136390328..413e970836 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -153,16 +153,16 @@ public TiffDecoderCore(DecoderOptions options) /// protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { + this.inputStream = stream; + DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); + IList directories = reader.Read(); + this.byteOrder = reader.ByteOrder; + List> frames = []; List framesMetadata = []; + try { - this.inputStream = stream; - DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); - - IList directories = reader.Read(); - this.byteOrder = reader.ByteOrder; - Size? size = null; uint frameCount = 0; foreach (ExifProfile ifd in directories) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index f7cdd1bfc4..4a035594ba 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -68,6 +68,8 @@ private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ImageFr TiffFrameMetadata tiffFrameMetadata = rootFrameMetadata.GetTiffMetadata(); tiffMetadata.BitsPerPixel = tiffFrameMetadata.BitsPerPixel; tiffMetadata.BitsPerSample = tiffFrameMetadata.BitsPerSample; + tiffMetadata.SampleFormat = tiffFrameMetadata.SampleFormat; + tiffMetadata.ExtraSampleType = tiffFrameMetadata.ExtraSampleType; tiffMetadata.Compression = tiffFrameMetadata.Compression; tiffMetadata.PhotometricInterpretation = tiffFrameMetadata.PhotometricInterpretation; tiffMetadata.Predictor = tiffFrameMetadata.Predictor; diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 454030599d..0d84081cc8 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -111,6 +111,11 @@ public static bool VerifyAndParse(this TiffDecoderCore options, ExifProfile exif options.Predictor = frameMetadata.Predictor; options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation; options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger; + frameMetadata.SampleFormat = options.SampleFormat; + + // The typed frame metadata now owns this value. Remove the raw IFD tag so + // re-encoding cannot retain a stale SampleFormat when encoder options change it. + exifProfile.RemoveValue(ExifTag.SampleFormat); options.BitsPerPixel = (int)frameMetadata.BitsPerPixel; options.BitsPerSample = frameMetadata.BitsPerSample; diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 7859b2c902..4dc2a91686 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -22,6 +22,12 @@ public class TiffEncoder : QuantizingImageEncoder /// public TiffBitsPerPixel? BitsPerPixel { get; init; } + /// + /// Gets the format of the samples written to the TIFF image. + /// Floating-point samples use 32 bits per component. + /// + public TiffSampleFormat? SampleFormat { get; init; } + /// /// Gets the compression type to use. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index e5e47166e9..4177eabc6b 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -74,6 +74,7 @@ public TiffEncoderCore(TiffEncoder encoder, Configuration configuration) this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.BitsPerPixel = encoder.BitsPerPixel; + this.SampleFormat = encoder.SampleFormat; this.HorizontalPredictor = encoder.HorizontalPredictor; this.CompressionType = encoder.Compression; this.compressionLevel = encoder.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression; @@ -101,6 +102,16 @@ public TiffEncoderCore(TiffEncoder encoder, Configuration configuration) /// internal TiffBitsPerPixel? BitsPerPixel { get; private set; } + /// + /// Gets the selected format of the written samples. + /// + internal TiffSampleFormat? SampleFormat { get; private set; } + + /// + /// Gets the source pixel's alpha representation for the floating-point extra sample tag. + /// + internal PixelAlphaRepresentation FloatAlphaRepresentation { get; private set; } + /// /// Encodes the image to the specified stream from the . /// @@ -118,6 +129,15 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata; TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata(); + PixelTypeInfo pixelInfo = TPixel.GetPixelTypeInfo(); + TiffSampleFormat sampleFormat = this.SampleFormat ?? rootFrameTiffMetaData.SampleFormat; + + // The encoder writes unsigned integer or IEEE floating-point samples. Match the + // existing TIFF option policy by using unsigned samples for unsupported values. + if (sampleFormat is not TiffSampleFormat.Float and not TiffSampleFormat.UnsignedInteger) + { + sampleFormat = TiffSampleFormat.UnsignedInteger; + } // Determine the correct values to encode with. // EncoderOptions > Metadata > Default. @@ -130,7 +150,41 @@ public void Encode(Image image, Stream stream, CancellationToken TiffCompression compression = this.CompressionType ?? rootFrameTiffMetaData.Compression; // Make sure the Encoder options makes sense in combination with each other. - this.SanitizeAndSetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor); + if (sampleFormat == TiffSampleFormat.Float) + { + // Fresh images have integer TIFF metadata defaults. An explicit float request without + // an explicit depth instead follows the source pixel's color and alpha capabilities. + if (this.BitsPerPixel is null && rootFrameTiffMetaData.SampleFormat != TiffSampleFormat.Float) + { + bool hasColorComponents = (pixelInfo.ColorType & PixelColorType.RGB) == PixelColorType.RGB + || (pixelInfo.ColorType & PixelColorType.BGR) == PixelColorType.BGR; + + bitsPerPixel = hasColorComponents + ? pixelInfo.AlphaRepresentation == PixelAlphaRepresentation.None ? TiffBitsPerPixel.Bit96 : TiffBitsPerPixel.Bit128 + : TiffBitsPerPixel.Bit32; + } + + if (this.PhotometricInterpretation is null && rootFrameTiffMetaData.SampleFormat != TiffSampleFormat.Float) + { + photometricInterpretation = bitsPerPixel == TiffBitsPerPixel.Bit32 + ? TiffPhotometricInterpretation.BlackIsZero + : TiffPhotometricInterpretation.Rgb; + } + + this.FloatAlphaRepresentation = pixelInfo.AlphaRepresentation; + } + else + { + // An explicit change from floating-point samples must not inherit their 32-bit + // component depth when the caller did not choose an integer output depth. + if (this.BitsPerPixel is null && rootFrameTiffMetaData.SampleFormat == TiffSampleFormat.Float) + { + bitsPerPixel = TiffConstants.DefaultBitsPerPixel; + } + } + + this.SanitizeAndSetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor, sampleFormat); + this.SampleFormat = sampleFormat; using TiffStreamWriter writer = new(stream); Span buffer = stackalloc byte[4]; @@ -148,7 +202,7 @@ public void Encode(Image image, Stream stream, CancellationToken // TODO: Try to avoid cloning the frame if possible. // We should be cloning individual scanlines instead. - if (EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode)) + if (sampleFormat != TiffSampleFormat.Float && EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode)) { clonedFrame = frame.Clone(); EncodingUtilities.ReplaceTransparentPixels(clonedFrame); @@ -156,7 +210,7 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrame encodingFrame = clonedFrame ?? frame; - ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, imageMetadata, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); + ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, imageMetadata, this.BitsPerPixel.Value, this.CompressionType.Value, sampleFormat, ifdMarker); imageMetadata = null; } finally @@ -199,6 +253,7 @@ public static long WriteHeader(TiffStreamWriter writer, Span buffer) /// The image (common metadata for root frame). /// The bits per pixel. /// The compression type. + /// The sample format. /// The marker to write this IFD offset. /// /// The next IFD offset value. @@ -210,6 +265,7 @@ private long WriteFrame( Image? image, TiffBitsPerPixel bitsPerPixel, TiffCompression compression, + TiffSampleFormat sampleFormat, long ifdOffset) where TPixel : unmanaged, IPixel { @@ -225,16 +281,29 @@ private long WriteFrame( Size encodingSize = new(width, height); TiffEncoderEntriesCollector entriesCollector = new(); - using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( - this.PhotometricInterpretation, - frame, - encodingSize, - this.quantizer, - this.pixelSamplingStrategy, - this.memoryAllocator, - this.configuration, - entriesCollector, - (int)bitsPerPixel); + + // Float samples need their own writer. The existing factory selects the integer + // writer from the photometric interpretation and encoded pixel depth. + using TiffBaseColorWriter colorWriter = sampleFormat == TiffSampleFormat.Float + ? new TiffFloatWriter( + frame, + encodingSize, + this.memoryAllocator, + this.configuration, + entriesCollector, + (int)bitsPerPixel, + this.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero, + EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode)) + : TiffColorWriterFactory.Create( + this.PhotometricInterpretation, + frame, + encodingSize, + this.quantizer, + this.pixelSamplingStrategy, + this.memoryAllocator, + this.configuration, + entriesCollector, + (int)bitsPerPixel); using TiffBaseCompressor compressor = TiffCompressorFactory.Create( compression, @@ -373,8 +442,50 @@ private void SanitizeAndSetEncoderOptions( TiffBitsPerPixel bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression, - TiffPredictor predictor) + TiffPredictor predictor, + TiffSampleFormat sampleFormat) { + if (sampleFormat == TiffSampleFormat.Float) + { + // Preserve supported float depths. For an unsupported depth, select a layout + // from the requested color interpretation and the source alpha representation. + if (bitsPerPixel is not (TiffBitsPerPixel.Bit32 or TiffBitsPerPixel.Bit96 or TiffBitsPerPixel.Bit128)) + { + bitsPerPixel = photometricInterpretation is TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero + ? TiffBitsPerPixel.Bit32 + : this.FloatAlphaRepresentation == PixelAlphaRepresentation.None ? TiffBitsPerPixel.Bit96 : TiffBitsPerPixel.Bit128; + } + + if (bitsPerPixel == TiffBitsPerPixel.Bit32) + { + photometricInterpretation = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero + ? TiffPhotometricInterpretation.WhiteIsZero + : TiffPhotometricInterpretation.BlackIsZero; + } + else + { + photometricInterpretation = TiffPhotometricInterpretation.Rgb; + } + + // Fax and JPEG compressors cannot write these sample layouts. Deflate is the + // existing TIFF fallback for a non-bilevel image with incompatible compression. + if (compression is not (TiffCompression.None or TiffCompression.PackBits or TiffCompression.Deflate or TiffCompression.Lzw)) + { + compression = TiffCompression.Deflate; + } + + // The TIFF floating-point predictor is not implemented, so write samples directly. + this.SetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, TiffPredictor.None); + return; + } + + // The packed integer depths still use the existing 16-bit color fallback. + bitsPerPixel = bitsPerPixel switch + { + TiffBitsPerPixel.Bit30 or TiffBitsPerPixel.Bit36 or TiffBitsPerPixel.Bit42 => TiffBitsPerPixel.Bit48, + _ => bitsPerPixel + }; + // Ensure 1 Bit compression is only used with 1 bit pixel type. // Choose a sensible default based on the bits per pixel. if (IsOneBitCompression(compression) && bitsPerPixel != TiffBitsPerPixel.Bit1) @@ -389,6 +500,13 @@ private void SanitizeAndSetEncoderOptions( // Ensure predictor is only used with compression that supports it. predictor = HasPredictor(compression) ? predictor : TiffPredictor.None; + // JPEG cannot write the requested high-precision integer samples. + if (compression == TiffCompression.Jpeg && + bitsPerPixel is (TiffBitsPerPixel.Bit48 or TiffBitsPerPixel.Bit64 or TiffBitsPerPixel.Bit96 or TiffBitsPerPixel.Bit128)) + { + compression = TiffCompression.Deflate; + } + // BitsPerPixel should be the primary source of truth for the encoder options. switch (bitsPerPixel) { @@ -426,16 +544,12 @@ or TiffPhotometricInterpretation.WhiteIsZero case TiffBitsPerPixel.Bit10: case TiffBitsPerPixel.Bit12: case TiffBitsPerPixel.Bit14: - case TiffBitsPerPixel.Bit30: - case TiffBitsPerPixel.Bit36: - case TiffBitsPerPixel.Bit42: - case TiffBitsPerPixel.Bit48: // Encoding not yet supported bits per pixel will default to 24 bits. this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor); break; + case TiffBitsPerPixel.Bit48: case TiffBitsPerPixel.Bit64: - // Encoding not yet supported bits per pixel will default to 32 bits. - this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, predictor); + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); break; default: this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 86c6b0c4a6..f8b2e6c1d7 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -191,6 +192,14 @@ private void ProcessExifProfile(bool skipMetadata, ExifProfile exifProfile) { foreach (IExifValue entry in exifProfile.Values) { + // This encoder writes strips. Source tile dimensions and offsets + // would make readers use the old layout in the new file. + ExifTagValue tag = (ExifTagValue)(ushort)entry.Tag; + if (tag is ExifTagValue.TileWidth or ExifTagValue.TileLength or ExifTagValue.TileOffsets or ExifTagValue.TileByteCounts) + { + continue; + } + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) { ExifParts entryPart = ExifTags.GetPart(entry.Tag); @@ -318,6 +327,13 @@ public ImageFormatProcessor(TiffEncoderEntriesCollector collector) public void Process(TiffEncoderCore encoder) { + // These tags describe the samples and predictor written by this encoder. Source EXIF + // values can describe a different layout, so replace them from the selected options. + this.Collector.Entries.RemoveAll( + entry => entry.Tag == ExifTag.SampleFormat + || entry.Tag == ExifTag.ExtraSamples + || entry.Tag == ExifTag.Predictor); + ExifShort planarConfig = new(ExifTagValue.PlanarConfiguration) { Value = (ushort)TiffPlanarConfiguration.Chunky @@ -351,6 +367,38 @@ public void Process(TiffEncoderCore encoder) this.Collector.AddOrReplace(compression); this.Collector.AddOrReplace(photometricInterpretation); + if (encoder.SampleFormat == TiffSampleFormat.Float) + { + ushort[] sampleFormats = new ushort[samplesPerPixel.Value]; + Array.Fill(sampleFormats, (ushort)TiffSampleFormat.Float); + this.Collector.AddOrReplace(new ExifShortArray(ExifTagValue.SampleFormat) + { + Value = sampleFormats + }); + + if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit128) + { + // The extra sample describes association; the binary32 sample layout is + // identical for straight and premultiplied source pixels. + TiffExtraSampleType alphaType = encoder.FloatAlphaRepresentation == PixelAlphaRepresentation.Associated + ? TiffExtraSampleType.AssociatedAlphaData + : TiffExtraSampleType.UnassociatedAlphaData; + + this.Collector.AddOrReplace(new ExifShortArray(ExifTagValue.ExtraSamples) + { + Value = [(ushort)alphaType] + }); + } + } + else if (encoder.BitsPerPixel is TiffBitsPerPixel.Bit64 or TiffBitsPerPixel.Bit128) + { + // Integer color writers emit straight color with a fourth alpha sample. + this.Collector.AddOrReplace(new ExifShortArray(ExifTagValue.ExtraSamples) + { + Value = [(ushort)TiffExtraSampleType.UnassociatedAlphaData] + }); + } + if (encoder.HorizontalPredictor == TiffPredictor.Horizontal && (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or TiffPhotometricInterpretation.PaletteColor or @@ -363,16 +411,30 @@ TiffPhotometricInterpretation.PaletteColor or } private static ushort GetSamplesPerPixel(TiffEncoderCore encoder) - => encoder.PhotometricInterpretation switch + { + if (encoder.SampleFormat == TiffSampleFormat.Float) + { + return (ushort)((int)encoder.BitsPerPixel!.Value / 32); + } + + return encoder.PhotometricInterpretation switch { TiffPhotometricInterpretation.PaletteColor or TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => 1, - _ => 3, + _ => encoder.BitsPerPixel is TiffBitsPerPixel.Bit64 or TiffBitsPerPixel.Bit128 ? (ushort)4 : (ushort)3, }; + } private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) { + if (encoder.SampleFormat == TiffSampleFormat.Float) + { + ushort[] bits = new ushort[(int)encoder.BitsPerPixel!.Value / 32]; + Array.Fill(bits, (ushort)32); + return bits; + } + switch (encoder.PhotometricInterpretation) { case TiffPhotometricInterpretation.PaletteColor: @@ -384,7 +446,14 @@ private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) return TiffConstants.BitsPerSample8Bit.ToArray(); case TiffPhotometricInterpretation.Rgb: - return TiffConstants.BitsPerSampleRgb8Bit.ToArray(); + return encoder.BitsPerPixel switch + { + TiffBitsPerPixel.Bit48 => [16, 16, 16], + TiffBitsPerPixel.Bit64 => [16, 16, 16, 16], + TiffBitsPerPixel.Bit96 => [32, 32, 32], + TiffBitsPerPixel.Bit128 => [32, 32, 32, 32], + _ => TiffConstants.BitsPerSampleRgb8Bit.ToArray(), + }; case TiffPhotometricInterpretation.WhiteIsZero: return encoder.BitsPerPixel switch @@ -418,14 +487,9 @@ private static ushort GetCompressionType(TiffEncoderCore encoder) // PackBits is allowed for all modes. return (ushort)TiffCompression.PackBits; case TiffCompression.Lzw: - if (encoder.PhotometricInterpretation is TiffPhotometricInterpretation.Rgb or - TiffPhotometricInterpretation.PaletteColor or - TiffPhotometricInterpretation.BlackIsZero) - { - return (ushort)TiffCompression.Lzw; - } - - break; + // LZW compresses the encoded sample bytes independently of whether + // grayscale zero represents white or black. + return (ushort)TiffCompression.Lzw; case TiffCompression.CcittGroup3Fax: return (ushort)TiffCompression.CcittGroup3Fax; diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index d4815ebdd4..d977b5f29f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -27,6 +27,9 @@ public TiffFrameMetadata() private TiffFrameMetadata(TiffFrameMetadata other) { this.BitsPerPixel = other.BitsPerPixel; + this.BitsPerSample = other.BitsPerSample; + this.SampleFormat = other.SampleFormat; + this.ExtraSampleType = other.ExtraSampleType; this.Compression = other.Compression; this.PhotometricInterpretation = other.PhotometricInterpretation; this.Predictor = other.Predictor; @@ -50,6 +53,16 @@ private TiffFrameMetadata(TiffFrameMetadata other) /// public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample; + /// + /// Gets or sets the format of the samples stored in this frame. + /// + public TiffSampleFormat SampleFormat { get; set; } = TiffSampleFormat.UnsignedInteger; + + /// + /// Gets or sets the TIFF interpretation of an extra sample in this frame. + /// + internal TiffExtraSampleType? ExtraSampleType { get; set; } + /// /// Gets or sets the compression scheme used on the image data. /// @@ -100,11 +113,54 @@ public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnecti /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() - => new() + { + int bitsPerPixel = (int)this.BitsPerPixel; + TiffBitsPerSample samples = this.BitsPerSample; + PixelComponentInfo components = samples.Channels switch + { + 1 => PixelComponentInfo.Create(1, bitsPerPixel, samples.Channel0), + 2 => PixelComponentInfo.Create(2, bitsPerPixel, samples.Channel0, samples.Channel1), + 3 => PixelComponentInfo.Create(3, bitsPerPixel, samples.Channel0, samples.Channel1, samples.Channel2), + _ => PixelComponentInfo.Create(4, bitsPerPixel, samples.Channel0, samples.Channel1, samples.Channel2, samples.Channel3) + }; + + // A total depth of 32 bits can mean one float sample or four 8-bit samples. + // PhotometricInterpretation and BitsPerSample describe the actual layout. + bool isGrayscale = this.PhotometricInterpretation is TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero; + PixelColorType colorType = this.PhotometricInterpretation switch + { + TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel == 1 ? PixelColorType.Binary : PixelColorType.Luminance, + TiffPhotometricInterpretation.PaletteColor => PixelColorType.Indexed, + TiffPhotometricInterpretation.Rgb => PixelColorType.RGB, + TiffPhotometricInterpretation.Separated => PixelColorType.CMYK, + TiffPhotometricInterpretation.YCbCr => PixelColorType.YCbCr, + _ => PixelColorType.Other + }; + + bool hasAlpha = (isGrayscale && samples.Channels == 2) + || (this.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb && samples.Channels == 4); + + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + if (hasAlpha) + { + colorType |= PixelColorType.Alpha; + alpha = this.ExtraSampleType == TiffExtraSampleType.AssociatedAlphaData + ? PixelAlphaRepresentation.Associated + : PixelAlphaRepresentation.Unassociated; + } + + return new FormatConnectingFrameMetadata { + PixelTypeInfo = new PixelTypeInfo(bitsPerPixel) + { + ComponentInfo = components, + ColorType = colorType, + AlphaRepresentation = alpha + }, EncodingWidth = this.EncodingWidth, EncodingHeight = this.EncodingHeight }; + } /// public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) @@ -168,6 +224,15 @@ private static void Parse(TiffFrameMetadata meta, ExifProfile profile) meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel(); + if (profile.TryGetValue(ExifTag.SampleFormat, out IExifValue? sampleFormatValue)) + { + ushort[]? values = sampleFormatValue.Value; + if (values is not null && values.Length > 0) + { + meta.SampleFormat = (TiffSampleFormat)values[0]; + } + } + if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue)) { meta.Compression = (TiffCompression)compressionValue.Value; @@ -188,6 +253,27 @@ private static void Parse(TiffFrameMetadata meta, ExifProfile profile) meta.InkSet = (TiffInkSet)inkSetValue.Value; } + if (profile.TryGetValue(ExifTag.ExtraSamples, out IExifValue? extraSamples)) + { + ushort[]? values = extraSamples.Value; + if (values is not null && values.Length > 0) + { + TiffExtraSampleType sampleType = (TiffExtraSampleType)values[0]; + + // CorelDRAW uses a nonstandard value for straight alpha. Decode it with the + // same association as the standard unassociated-alpha tag. + if (sampleType == TiffExtraSampleType.CorelDrawUnassociatedAlphaData) + { + sampleType = TiffExtraSampleType.UnassociatedAlphaData; + } + + if (sampleType is TiffExtraSampleType.UnassociatedAlphaData or TiffExtraSampleType.AssociatedAlphaData) + { + meta.ExtraSampleType = sampleType; + } + } + } + // Remove values, we've explicitly captured them and they could change on encode. profile.RemoveValue(ExifTag.BitsPerSample); profile.RemoveValue(ExifTag.Compression); diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index 69b03f36fb..51939cb1f3 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -29,6 +29,8 @@ private TiffMetadata(TiffMetadata other) this.FormatType = other.FormatType; this.BitsPerPixel = other.BitsPerPixel; this.BitsPerSample = other.BitsPerSample; + this.SampleFormat = other.SampleFormat; + this.ExtraSampleType = other.ExtraSampleType; this.Compression = other.Compression; this.PhotometricInterpretation = other.PhotometricInterpretation; this.Predictor = other.Predictor; @@ -54,6 +56,16 @@ private TiffMetadata(TiffMetadata other) /// public TiffBitsPerSample BitsPerSample { get; set; } = TiffConstants.DefaultBitsPerSample; + /// + /// Gets or sets the root frame's TIFF sample format. + /// + public TiffSampleFormat SampleFormat { get; set; } = TiffSampleFormat.UnsignedInteger; + + /// + /// Gets or sets the root frame's interpretation of an extra sample. + /// + internal TiffExtraSampleType? ExtraSampleType { get; set; } + /// /// Gets or sets the compression scheme used on the image data. Derived from the root frame. /// @@ -135,35 +147,35 @@ public PixelTypeInfo GetPixelTypeInfo() TiffBitsPerSample samples = this.BitsPerSample; PixelComponentInfo info = samples.Channels switch { - 1 => PixelComponentInfo.Create(1, bpp, bpp), - 2 => PixelComponentInfo.Create(2, bpp, bpp, samples.Channel0, samples.Channel1), + 1 => PixelComponentInfo.Create(1, bpp, samples.Channel0), + 2 => PixelComponentInfo.Create(2, bpp, samples.Channel0, samples.Channel1), 3 => PixelComponentInfo.Create(3, bpp, samples.Channel0, samples.Channel1, samples.Channel2), _ => PixelComponentInfo.Create(4, bpp, samples.Channel0, samples.Channel1, samples.Channel2, samples.Channel3) }; - PixelColorType colorType; + // Total bit depth alone cannot distinguish one 32-bit grayscale sample from + // four 8-bit color samples. The photometric tag and sample count define the layout. + bool isGrayscale = this.PhotometricInterpretation is TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero; + PixelColorType colorType = this.PhotometricInterpretation switch + { + TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bpp == 1 ? PixelColorType.Binary : PixelColorType.Luminance, + TiffPhotometricInterpretation.PaletteColor => PixelColorType.Indexed, + TiffPhotometricInterpretation.Rgb => PixelColorType.RGB, + TiffPhotometricInterpretation.Separated => PixelColorType.CMYK, + TiffPhotometricInterpretation.YCbCr => PixelColorType.YCbCr, + _ => PixelColorType.Other + }; + + bool hasAlpha = (isGrayscale && samples.Channels == 2) + || (this.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb && samples.Channels == 4); + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; - switch (this.BitsPerPixel) + if (hasAlpha) { - case TiffBitsPerPixel.Bit1: - colorType = PixelColorType.Binary; - break; - case TiffBitsPerPixel.Bit4: - case TiffBitsPerPixel.Bit6: - case TiffBitsPerPixel.Bit8: - colorType = PixelColorType.Indexed; - break; - case TiffBitsPerPixel.Bit16: - colorType = PixelColorType.Luminance; - break; - case TiffBitsPerPixel.Bit32: - case TiffBitsPerPixel.Bit64: - colorType = PixelColorType.RGB | PixelColorType.Alpha; - alpha = PixelAlphaRepresentation.Unassociated; - break; - default: - colorType = PixelColorType.RGB; - break; + colorType |= PixelColorType.Alpha; + alpha = this.ExtraSampleType == TiffExtraSampleType.AssociatedAlphaData + ? PixelAlphaRepresentation.Associated + : PixelAlphaRepresentation.Unassociated; } return new PixelTypeInfo(bpp) diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs index 31a1b0e414..9a64e6756c 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs @@ -21,15 +21,32 @@ public static TiffBaseColorWriter Create( TiffEncoderEntriesCollector entriesCollector, int bitsPerPixel) where TPixel : unmanaged, IPixel - => photometricInterpretation switch + { + switch (photometricInterpretation) { - TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel), - TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch - { - 1 => new TiffBiColorWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), - 16 => new TiffGrayL16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector), - _ => new TiffGrayWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector) - }, - _ => new TiffRgbWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), - }; + case TiffPhotometricInterpretation.PaletteColor: + return new TiffPaletteWriter(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel); + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + return bitsPerPixel switch + { + 1 => new TiffBiColorWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), + 16 => new TiffGrayL16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector), + _ => new TiffGrayWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector) + }; + + default: + if (bitsPerPixel == 48 || bitsPerPixel == 64) + { + return new TiffRgb16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector, bitsPerPixel); + } + + if (bitsPerPixel == 96 || bitsPerPixel == 128) + { + return new TiffRgb32Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector, bitsPerPixel); + } + + return new TiffRgbWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector); + } + } } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffFloatWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffFloatWriter{TPixel}.cs new file mode 100644 index 0000000000..004e3eeac7 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffFloatWriter{TPixel}.cs @@ -0,0 +1,228 @@ +// 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; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers; + +/// +/// Writes chunky IEEE binary32 grayscale, RGB, or RGBA TIFF samples. +/// +/// The source pixel format. +internal sealed class TiffFloatWriter : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel +{ + private readonly int bitsPerPixel; + private readonly bool whiteIsZero; + private readonly bool clearTransparentPixels; + private readonly bool singleComponentSource; + private readonly PixelConversionModifiers modifiers; + + /// + /// Initializes a new instance of the class. + /// + /// The source frame. + /// The encoded region size. + /// The memory allocator. + /// The configuration. + /// The TIFF directory collector. + /// The total bits per pixel. + /// Whether a grayscale value of one represents black. + /// Whether zero-alpha color is cleared. + public TiffFloatWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector, + int bitsPerPixel, + bool whiteIsZero, + bool clearTransparentPixels) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + this.bitsPerPixel = bitsPerPixel; + this.whiteIsZero = whiteIsZero; + this.clearTransparentPixels = clearTransparentPixels; + + PixelTypeInfo info = TPixel.GetPixelTypeInfo(); + this.singleComponentSource = info.ComponentInfo?.ComponentCount == 1; + this.modifiers = bitsPerPixel == 128 && info.AlphaRepresentation == PixelAlphaRepresentation.Associated + ? PixelConversionModifiers.Premultiply + : PixelConversionModifiers.UnPremultiply; + + this.modifiers |= PixelConversionModifiers.Scale; + } + + /// + public override int BitsPerPixel => this.bitsPerPixel; + + /// + protected override void EncodePixels(Span pixels, Span buffer) + { + const int BlockSize = 64; + int samplesPerPixel = this.bitsPerPixel / 32; + Span output = MemoryMarshal.Cast(buffer); + Span vectors = stackalloc Vector4[BlockSize]; + + for (int offset = 0; offset < pixels.Length; offset += BlockSize) + { + int count = Math.Min(BlockSize, pixels.Length - offset); + Span block = vectors[..count]; + PixelOperations.Instance.ToVector4(this.Configuration, pixels.Slice(offset, count), block, this.modifiers); + + // Clear in the native row buffer, after conversion, so clearing transparent color + // never normalizes unrelated HDR pixels in the source image. + if (samplesPerPixel == 4 && this.clearTransparentPixels) + { + EncodingUtilities.ReplaceTransparentPixels(block); + } + + Span samples = output.Slice(offset * samplesPerPixel, count * samplesPerPixel); + if (samplesPerPixel == 4) + { + // The four-component vector layout matches chunky TIFF and DXGI float storage. + MemoryMarshal.Cast(block).CopyTo(samples); + } + else if (samplesPerPixel == 3) + { + // TIFF's three-component layout omits Vector4's fourth value. + // The packer removes those words in full registers and handles + // the final short block without changing floating-point bits. + PackFloatTriplets(block, samples); + } + else + { + // A one-component source already stores intensity in component 0. + // Other sources use the library's BT.709 luminance calculation. + // The packer applies WhiteIsZero during the same SIMD traversal. + PackFloatSingles(block, samples, this.singleComponentSource, this.whiteIsZero); + } + } + } + + /// + /// Packs the first three components of four-component vectors into consecutive + /// three-component floating-point pixels without changing their bits. + /// + /// The source vectors. + /// The packed destination samples. + private static void PackFloatTriplets(ReadOnlySpan source, Span destination) + { + ref byte s = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref byte d = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int i = 0; + + if (Vector128.IsHardwareAccelerated) + { + Vector128 firstThree = Vector128.Create(uint.MaxValue, uint.MaxValue, uint.MaxValue, 0U).AsByte(); + Vector128 firstTwo = Vector128.Create(ulong.MaxValue, 0UL).AsByte(); + Vector128 firstOne = Vector128.Create(uint.MaxValue, 0U, 0U, 0U).AsByte(); + + for (; i <= source.Length - 4; i += 4) + { + nuint sourceByte = (nuint)(i * 16); + Vector128 pixel0 = Vector128.LoadUnsafe(ref s, sourceByte); + Vector128 pixel1 = Vector128.LoadUnsafe(ref s, sourceByte + 16); + Vector128 pixel2 = Vector128.LoadUnsafe(ref s, sourceByte + 32); + Vector128 pixel3 = Vector128.LoadUnsafe(ref s, sourceByte + 48); + + // Four input registers hold [c0,c1,c2,c3] for four pixels. The + // masks discard each fourth component; byte shifts join retained + // words across pixel boundaries into exactly three output registers. + Vector128 packed0 = (pixel0 & firstThree) | Vector128_.ShiftLeftBytesInVector(pixel1, 12); + Vector128 packed1 = (Vector128_.ShiftRightBytesInVector(pixel1, 4) & firstTwo) | Vector128_.ShiftLeftBytesInVector(pixel2, 8); + Vector128 packed2 = (Vector128_.ShiftRightBytesInVector(pixel2, 8) & firstOne) | Vector128_.ShiftLeftBytesInVector(pixel3, 4); + + nuint destinationByte = (nuint)(i * 12); + Vector128.StoreUnsafe(packed0, ref d, destinationByte); + Vector128.StoreUnsafe(packed1, ref d, destinationByte + 16); + Vector128.StoreUnsafe(packed2, ref d, destinationByte + 32); + } + } + + for (; i < source.Length; i++) + { + Vector4 pixel = source[i]; + int sample = i * 3; + destination[sample] = pixel.X; + destination[sample + 1] = pixel.Y; + destination[sample + 2] = pixel.Z; + } + } + + /// + /// Packs four-component vectors into one floating-point sample per pixel. + /// + /// The source vectors. + /// The destination intensity samples. + /// Whether the source's first component is already its intensity. + /// Whether to store one minus the intensity. + private static void PackFloatSingles(ReadOnlySpan source, Span destination, bool useFirstComponent, bool invert) + { + ref float s = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref float d = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector128.IsHardwareAccelerated) + { + Vector128 one = Vector128.Create(1F); + Vector4 weights = ColorNumerics.Bt709; + Vector128 weight0 = Vector128.Create(weights.X); + Vector128 weight1 = Vector128.Create(weights.Y); + Vector128 weight2 = Vector128.Create(weights.Z); + Vector128 weight3 = Vector128.Create(weights.W); + + for (; i <= source.Length - 4; i += 4) + { + Vector128 pixel0 = Vector128.LoadUnsafe(ref s, (nuint)(i * 4)); + Vector128 pixel1 = Vector128.LoadUnsafe(ref s, (nuint)((i + 1) * 4)); + Vector128 pixel2 = Vector128.LoadUnsafe(ref s, (nuint)((i + 2) * 4)); + Vector128 pixel3 = Vector128.LoadUnsafe(ref s, (nuint)((i + 3) * 4)); + + // Transpose four pixels into component vectors. A grayscale source + // uses the first component directly; a color source needs BT.709. + Vector128 firstPair = Vector128_.UnpackLow(pixel0, pixel1); + Vector128 secondPair = Vector128_.UnpackLow(pixel2, pixel3); + Vector128 component0 = Vector128_.UnpackLow(firstPair.AsDouble(), secondPair.AsDouble()).AsSingle(); + Vector128 intensity; + + if (useFirstComponent) + { + intensity = component0; + } + else + { + Vector128 thirdPair = Vector128_.UnpackHigh(pixel0, pixel1); + Vector128 fourthPair = Vector128_.UnpackHigh(pixel2, pixel3); + Vector128 component1 = Vector128_.UnpackHigh(firstPair.AsDouble(), secondPair.AsDouble()).AsSingle(); + Vector128 component2 = Vector128_.UnpackLow(thirdPair.AsDouble(), fourthPair.AsDouble()).AsSingle(); + Vector128 component3 = Vector128_.UnpackHigh(thirdPair.AsDouble(), fourthPair.AsDouble()).AsSingle(); + + // Include the zero-weight fourth component. As with Vector4.Dot, + // a nonfinite fourth component then yields nonfinite luminance. + intensity = (((component0 * weight0) + (component1 * weight1)) + + (component2 * weight2)) + (component3 * weight3); + } + + if (invert) + { + intensity = one - intensity; + } + + Vector128.StoreUnsafe(intensity, ref d, (nuint)i); + } + } + + for (; i < source.Length; i++) + { + float intensity = useFirstComponent ? source[i].X : ColorNumerics.GetBT709Luminance(source[i]); + destination[i] = invert ? 1F - intensity : intensity; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgb16Writer{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgb16Writer{TPixel}.cs new file mode 100644 index 0000000000..f865ec6a45 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgb16Writer{TPixel}.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers; + +/// +/// Writes chunky 16-bit RGB or RGBA samples in the TIFF file's native byte order. +/// +/// The source pixel format. +internal sealed class TiffRgb16Writer : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel +{ + private readonly int bitsPerPixel; + + /// + /// Initializes a new instance of the class. + /// + /// The source frame. + /// The encoded region size. + /// The memory allocator. + /// The configuration. + /// The TIFF directory collector. + /// The total bits per pixel, 48 or 64. + public TiffRgb16Writer( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector, + int bitsPerPixel) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + this.bitsPerPixel = bitsPerPixel; + } + + /// + public override int BitsPerPixel => this.bitsPerPixel; + + /// + protected override void EncodePixels(Span pixels, Span buffer) + { + // TIFF's header declares the host byte order, so the existing bulk conversions + // can write native-endian 16-bit samples without a second pass over the strip. + if (this.bitsPerPixel == 48) + { + PixelOperations.Instance.ToRgb48Bytes(this.Configuration, pixels, buffer, pixels.Length); + } + else + { + PixelOperations.Instance.ToRgba64Bytes(this.Configuration, pixels, buffer, pixels.Length); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgb32Writer{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgb32Writer{TPixel}.cs new file mode 100644 index 0000000000..978444e697 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgb32Writer{TPixel}.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers; + +/// +/// Writes chunky unsigned 32-bit color samples in the TIFF file's native byte order. +/// +/// The source pixel format. +internal sealed class TiffRgb32Writer : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel +{ + private readonly int bitsPerPixel; + + /// + /// Initializes a new instance of the class. + /// + /// The source frame. + /// The encoded region size. + /// The memory allocator. + /// The configuration. + /// The TIFF directory collector. + /// The total bits per pixel, 96 or 128. + public TiffRgb32Writer( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector, + int bitsPerPixel) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) + { + this.bitsPerPixel = bitsPerPixel; + } + + /// + public override int BitsPerPixel => this.bitsPerPixel; + + /// + protected override void EncodePixels(Span pixels, Span buffer) + { + // The TIFF header declares the host byte order, so the destination pixels + // can be written directly to the strip in their native integer layout. + if (this.bitsPerPixel == 96) + { + // The shared bulk operation copies matching pixels exactly. Other source + // formats use the established pixel conversion path. + PixelOperations.Instance.To(this.Configuration, pixels, MemoryMarshal.Cast(buffer)); + } + else + { + PixelOperations.Instance.To(this.Configuration, pixels, MemoryMarshal.Cast(buffer)); + } + } +} diff --git a/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs b/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs index ad26eb4e61..df64012243 100644 --- a/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs @@ -65,57 +65,6 @@ protected abstract override void FromUnassociatedScaledVector4Destructive( /// protected abstract override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination); - /// - public override void From( - Configuration configuration, - ReadOnlySpan source, - Span destination) - { - if (source.IsEmpty) - { - return; - } - - // Cap large conversions at 1,024 vectors while avoiding a 16 KiB rental for short spans. - int sliceLength = Math.Min(source.Length, 1024); - int numberOfSlices = source.Length / sliceLength; - - using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength); - Span vectorSpan = tempVectors.GetSpan()[..sliceLength]; - - // Convert through unassociated vectors so the destination operation can quantize alpha to its own storage before associating RGB. - for (int i = 0; i < numberOfSlices; i++) - { - int start = i * sliceLength; - ReadOnlySpan sourceSlice = source.Slice(start, sliceLength); - Span destinationSlice = destination.Slice(start, sliceLength); - PixelOperations.Instance.ToVector4( - configuration, - sourceSlice, - vectorSpan, - PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); - - this.FromUnassociatedScaledVector4Destructive(configuration, vectorSpan, destinationSlice); - } - - int endOfCompleteSlices = numberOfSlices * sliceLength; - int remainder = source.Length - endOfCompleteSlices; - - if (remainder > 0) - { - ReadOnlySpan sourceSlice = source[endOfCompleteSlices..]; - Span destinationSlice = destination.Slice(endOfCompleteSlices, remainder); - vectorSpan = vectorSpan[..remainder]; - PixelOperations.Instance.ToVector4( - configuration, - sourceSlice, - vectorSpan, - PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); - - this.FromUnassociatedScaledVector4Destructive(configuration, vectorSpan, destinationSlice); - } - } - /// public override void FromVector4Destructive( Configuration configuration, @@ -369,7 +318,7 @@ private void ConvertToUnassociated( ReadOnlySpan sourceSlice = source.Slice(start, sliceLength); Span destinationSlice = destination.Slice(start, sliceLength); this.ToUnassociatedScaledVector4(configuration, sourceSlice, vectorSpan); - destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale); } int endOfCompleteSlices = numberOfSlices * sliceLength; @@ -381,7 +330,7 @@ private void ConvertToUnassociated( Span destinationSlice = destination.Slice(endOfCompleteSlices, remainder); vectorSpan = vectorSpan[..remainder]; this.ToUnassociatedScaledVector4(configuration, sourceSlice, vectorSpan); - destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale); } } } diff --git a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs index 6de4b30b9e..beae6c6dcd 100644 --- a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs +++ b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.PixelFormats; @@ -13,17 +14,10 @@ namespace SixLabors.ImageSharp.PixelFormats; /// internal static class HalfTypeHelper { - // IEEE 754 binary16 has a largest finite magnitude of 65504. Scaled pixel vectors map that complete finite - // interval to [0, 1], while native vectors continue to expose the stored floating-point value directly. - internal const float FiniteMinimum = -65504F; - internal const float FiniteMaximum = 65504F; - internal const float FiniteRange = FiniteMaximum - FiniteMinimum; - internal const float InverseFiniteRange = (float)(1D / FiniteRange); - internal const float ScaledMidpoint = .5F; - - // These constants mirror the binary16 conversion used by System.Half. Keeping the vector conversion + // These constants mirror the half-precision conversion used by System.Half. Keeping the vector conversion // bit-for-bit equivalent to the scalar runtime conversion makes SIMD a pure throughput optimization. private const uint HalfExponentMask = 0x7C00; + private const uint HalfQuietNaNMask = 0x0200; private const uint HalfSignMask = 0x8000; private const uint HalfToSingleBitsMask = 0x0FFF_E000; private const uint SingleExponentLowerBound = 0x3880_0000; @@ -51,326 +45,543 @@ internal static class HalfTypeHelper internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value); /// - /// Normalizes a binary16 value to [0, 1], saturating infinities and mapping NaN to zero. + /// Unpacks eight half-precision values into two vectors of single-precision values. /// - /// The native binary16 value represented as a . - /// The normalized value. + /// The packed half-precision values. + /// The unpacked lower and upper values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float ToScaled(float value) + internal static (Vector128 Lower, Vector128 Upper) Unpack(Vector128 value) { - // Clamp after mapping so native infinities reach the scaled endpoints and NaN becomes zero. - return Numerics.Clamp((value * InverseFiniteRange) + ScaledMidpoint, 0F, 1F); + (Vector128 lower, Vector128 upper) = Vector128.Widen(value); + return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); } /// - /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// Unpacks sixteen half-precision values into two vectors of single-precision values. /// - /// The native binary16 values. - /// The normalized values. + /// The packed half-precision values. + /// The unpacked lower and upper values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 ToScaled(Vector2 value) => ToScaled(value.AsVector128()).AsVector2(); + internal static (Vector256 Lower, Vector256 Upper) Unpack(Vector256 value) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(value); + return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + } /// - /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// Unpacks thirty-two half-precision values into two vectors of single-precision values. /// - /// The native binary16 values. - /// The normalized values. + /// The packed half-precision values. + /// The unpacked lower and upper values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector4 ToScaled(Vector4 value) => ToScaled(value.AsVector128()).AsVector4(); + internal static (Vector512 Lower, Vector512 Upper) Unpack(Vector512 value) + { + (Vector512 lower, Vector512 upper) = Vector512.Widen(value); + return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + } /// - /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// Packs eight single-precision values into half-precision storage. /// - /// The component values. - /// The converted values. + /// The lower single-precision values. + /// The upper single-precision values. + /// The packed half-precision 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); - } + internal static Vector128 Pack(Vector128 lower, Vector128 upper) + => Vector128.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); /// - /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// Packs sixteen single-precision values into half-precision storage. /// - /// The component values. - /// The converted values. + /// The lower single-precision values. + /// The upper single-precision values. + /// The packed half-precision 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); - } + internal static Vector256 Pack(Vector256 lower, Vector256 upper) + => Vector256.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); /// - /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// Packs thirty-two single-precision values into half-precision storage. /// - /// The component values. - /// The converted values. + /// The lower single-precision values. + /// The upper single-precision values. + /// The packed half-precision 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); - } + internal static Vector512 Pack(Vector512 lower, Vector512 upper) + => Vector512.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); /// - /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// Expands a span of half-precision components into single-precision components without changing their values. /// - /// The component values to convert in place. - public static void ToScaled(Span values) + /// The packed half-precision components. + /// The expanded components. + internal static void Unpack(ReadOnlySpan source, Span destination) { - ref Vector4 source = ref MemoryMarshal.GetReference(values); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref float destinationBase = ref MemoryMarshal.GetReference(destination); 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. + // Widening retains component order across each register. Full registers use + // SIMD; the scalar tail handles any remaining components in that same order. if (Vector512.IsHardwareAccelerated) { - int pixelsPerRegister = Vector512.Count / Vector128.Count; - - for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + nuint vectorCount = source[i..].Vector512Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector512.Count) { - ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); - - vector = ToScaled(vector); + (Vector512 lower, Vector512 upper) = Unpack(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector512.Count)); } } if (Vector256.IsHardwareAccelerated) { - int pixelsPerRegister = Vector256.Count / Vector128.Count; - - for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + nuint vectorCount = source[i..].Vector256Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector256.Count) { - ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); - - vector = ToScaled(vector); + (Vector256 lower, Vector256 upper) = Unpack(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector256.Count)); } } - // 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++) + if (Vector128.IsHardwareAccelerated) { - ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i); + nuint vectorCount = source[i..].Vector128Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector128.Count) + { + (Vector128 lower, Vector128 upper) = Unpack(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector128.Count)); + } + } - vector = ToScaled(vector); + for (; i < source.Length; i++) + { + Unsafe.Add(ref destinationBase, (uint)i) = Unpack(Unsafe.Add(ref sourceBase, (uint)i)); } } /// - /// Normalizes a scaled value, mapping NaN to zero, and expands it to the finite binary16 range. + /// Narrows single-precision components into half-precision storage using the same rounding as . /// - /// The normalized value. - /// The native binary16 value represented as a . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float FromScaled(float value) + /// The single-precision components. + /// The packed half-precision components. + internal static void Pack(ReadOnlySpan source, Span destination) { - // Clamp before expanding so nonfinite scaled input cannot become nonfinite half storage. - return (Numerics.Clamp(value, 0F, 1F) * FiniteRange) + FiniteMinimum; - } + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ReadOnlySpan packedDestination = destination[..source.Length]; + int i = 0; - /// - /// 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)] - public static Vector2 FromScaled(Vector2 value) => FromScaled(value.AsVector128()).AsVector2(); + // Each narrowing operation consumes two float registers and writes one + // half register, preserving the original component order and bit pattern. + if (Vector512.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector512Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector512.Count) + { + Vector512 lower = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + Vector512 upper = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)); + Vector512.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + } + } - /// - /// 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)] - public static Vector4 FromScaled(Vector4 value) => FromScaled(value.AsVector128()).AsVector4(); + if (Vector256.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector256Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector256.Count) + { + Vector256 lower = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + Vector256 upper = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)); + Vector256.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + } + } - /// - /// 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); + if (Vector128.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector128Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector128.Count) + { + Vector128 lower = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + Vector128 upper = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)); + Vector128.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + } + } - return (scaled * Vector128.Create(FiniteRange)) + Vector128.Create(FiniteMinimum); + for (; i < source.Length; i++) + { + Unsafe.Add(ref destinationBase, (uint)i) = Pack(Unsafe.Add(ref sourceBase, (uint)i)); + } } /// - /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// Expands four-component half-precision pixels and associates their first three components with the fourth. /// - /// The component values. - /// The converted values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 FromScaled(Vector256 value) + /// The packed components, grouped in fours. + /// The expanded components. + internal static void UnpackAssociated(ReadOnlySpan source, Span destination) { - Vector256 scaled = Numerics.Clamp(value, Vector256.Zero, Vector256.One); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref float destinationBase = ref MemoryMarshal.GetReference(destination); + int i = 0; + + // A register contains complete four-component pixels. Replicate the fourth + // component within each pixel, multiply the first three, then restore the fourth. + if (Vector512.IsHardwareAccelerated) + { + nuint vectorCount = source[i..].Vector512Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector512.Count) + { + (Vector512 lower, Vector512 upper) = Unpack(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); + } + } - return (scaled * Vector256.Create(FiniteRange)) + Vector256.Create(FiniteMinimum); + if (Vector256.IsHardwareAccelerated) + { + nuint vectorCount = source[i..].Vector256Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector256.Count) + { + (Vector256 lower, Vector256 upper) = Unpack(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint vectorCount = source[i..].Vector128Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector128.Count) + { + (Vector128 lower, Vector128 upper) = Unpack(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); + } + } + + for (; i < source.Length; i += 4) + { + Vector4 vector = new( + Unpack(Unsafe.Add(ref sourceBase, (uint)i)), + Unpack(Unsafe.Add(ref sourceBase, (uint)(i + 1))), + Unpack(Unsafe.Add(ref sourceBase, (uint)(i + 2))), + Unpack(Unsafe.Add(ref sourceBase, (uint)(i + 3)))); + + Numerics.Premultiply(ref vector); + Unsafe.Add(ref destinationBase, (uint)i) = vector.X; + Unsafe.Add(ref destinationBase, (uint)(i + 1)) = vector.Y; + Unsafe.Add(ref destinationBase, (uint)(i + 2)) = vector.Z; + Unsafe.Add(ref destinationBase, (uint)(i + 3)) = vector.W; + } } /// - /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// Unassociates four-component vectors and packs their native values into half-precision storage. /// - /// The component values. - /// The converted values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector512 FromScaled(Vector512 value) + /// The associated components, grouped in fours. + /// The packed components. + internal static void PackFromAssociated(ReadOnlySpan source, Span destination) { - Vector512 scaled = Numerics.Clamp(value, Vector512.Zero, Vector512.One); + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ReadOnlySpan packedDestination = destination[..source.Length]; + int i = 0; + + // The fourth component is replicated before division. UnPremultiply restores + // that component and preserves the first three when it is zero. + if (Vector512.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector512Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector512.Count) + { + Vector512 lower = Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector256Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector256.Count) + { + Vector256 lower = Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector128Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++, i += Vector128.Count) + { + Vector128 lower = Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + } + } - return (scaled * Vector512.Create(FiniteRange)) + Vector512.Create(FiniteMinimum); + for (; i < source.Length; i += 4) + { + Vector4 vector = new( + Unsafe.Add(ref sourceBase, (uint)i), + Unsafe.Add(ref sourceBase, (uint)(i + 1)), + Unsafe.Add(ref sourceBase, (uint)(i + 2)), + Unsafe.Add(ref sourceBase, (uint)(i + 3))); + + Numerics.UnPremultiply(ref vector); + Unsafe.Add(ref destinationBase, (uint)i) = Pack(vector.X); + Unsafe.Add(ref destinationBase, (uint)(i + 1)) = Pack(vector.Y); + Unsafe.Add(ref destinationBase, (uint)(i + 2)) = Pack(vector.Z); + Unsafe.Add(ref destinationBase, (uint)(i + 3)) = Pack(vector.W); + } } /// - /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// Expands associated four-component half-precision pixels and unassociates their first three components. /// - /// The component values to convert in place. - public static void FromScaled(Span values) + /// The packed components, grouped in fours. + /// The expanded components. + internal static void UnpackUnassociated(ReadOnlySpan source, Span destination) { - ref Vector4 source = ref MemoryMarshal.GetReference(values); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref float destinationBase = ref MemoryMarshal.GetReference(destination); 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. + // Each widened register contains complete four-component pixels, so every + // alpha stays with its own color components during unassociation. if (Vector512.IsHardwareAccelerated) { - int pixelsPerRegister = Vector512.Count / Vector128.Count; + nuint vectorCount = source[i..].Vector512Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++) + { + (Vector512 lower, Vector512 upper) = Unpack(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); + i += Vector512.Count; + } + } - for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + if (Vector256.IsHardwareAccelerated) + { + nuint vectorCount = source[i..].Vector256Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++) { - ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + (Vector256 lower, Vector256 upper) = Unpack(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); + i += Vector256.Count; + } + } - vector = FromScaled(vector); + if (Vector128.IsHardwareAccelerated) + { + nuint vectorCount = source[i..].Vector128Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++) + { + (Vector128 lower, Vector128 upper) = Unpack(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); + i += Vector128.Count; } } - if (Vector256.IsHardwareAccelerated) + // The pixel operations own the four-component invariant. Only whole pixels + // reach this tail, including a single pixel after the 128-bit path. + for (; i < source.Length; i += 4) { - int pixelsPerRegister = Vector256.Count / Vector128.Count; + Vector4 vector = new( + Unpack(Unsafe.Add(ref sourceBase, (uint)i)), + Unpack(Unsafe.Add(ref sourceBase, (uint)(i + 1))), + Unpack(Unsafe.Add(ref sourceBase, (uint)(i + 2))), + Unpack(Unsafe.Add(ref sourceBase, (uint)(i + 3)))); + + Numerics.UnPremultiply(ref vector); + Unsafe.Add(ref destinationBase, (uint)i) = vector.X; + Unsafe.Add(ref destinationBase, (uint)(i + 1)) = vector.Y; + Unsafe.Add(ref destinationBase, (uint)(i + 2)) = vector.Z; + Unsafe.Add(ref destinationBase, (uint)(i + 3)) = vector.W; + } + } - for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + /// + /// Associates four-component vectors with their stored half-precision alpha and packs them. + /// + /// The unassociated components, grouped in fours. + /// The packed components. + internal static void PackAssociated(ReadOnlySpan source, Span destination) + { + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ReadOnlySpan packedDestination = destination[..source.Length]; + int i = 0; + + // Round alpha before multiplication so all SIMD widths associate color + // with the exact alpha that the destination will store. + if (Vector512.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector512Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++) { - ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + Vector512 lower = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + i += Vector512.Count; + } + } - vector = FromScaled(vector); + if (Vector256.IsHardwareAccelerated) + { + nuint vectorCount = packedDestination[i..].Vector256Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++) + { + Vector256 lower = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + i += Vector256.Count; } } - // 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++) + if (Vector128.IsHardwareAccelerated) { - ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i); + nuint vectorCount = packedDestination[i..].Vector128Count(); + for (nuint vectorIndex = 0; vectorIndex < vectorCount; vectorIndex++) + { + Vector128 lower = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128.StoreUnsafe(Pack(lower, upper), ref destinationBase, (nuint)i); + i += Vector128.Count; + } + } - vector = FromScaled(vector); + for (; i < source.Length; i += 4) + { + Vector4 vector = new( + Unsafe.Add(ref sourceBase, (uint)i), + Unsafe.Add(ref sourceBase, (uint)(i + 1)), + Unsafe.Add(ref sourceBase, (uint)(i + 2)), + Unsafe.Add(ref sourceBase, (uint)(i + 3))); + + vector.W = Unpack(Pack(vector.W)); + Numerics.Premultiply(ref vector); + Unsafe.Add(ref destinationBase, (uint)i) = Pack(vector.X); + Unsafe.Add(ref destinationBase, (uint)(i + 1)) = Pack(vector.Y); + Unsafe.Add(ref destinationBase, (uint)(i + 2)) = Pack(vector.Z); + Unsafe.Add(ref destinationBase, (uint)(i + 3)) = Pack(vector.W); } } /// - /// Unpacks eight binary16 values into two vectors of single-precision values. + /// Associates complete four-component vectors while preserving their fourth component. /// - /// The packed binary16 values. - /// The unpacked lower and upper values. + /// The unassociated components. + /// The associated components. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static (Vector128 Lower, Vector128 Upper) Unpack(Vector128 value) + private static Vector128 Associate(Vector128 source) { - (Vector128 lower, Vector128 upper) = Vector128.Widen(value); - return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), alpha, source * alpha); } - /// - /// Unpacks sixteen binary16 values into two vectors of single-precision values. - /// - /// The packed binary16 values. - /// The unpacked lower and upper values. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static (Vector256 Lower, Vector256 Upper) Unpack(Vector256 value) + private static Vector256 Associate(Vector256 source) { - (Vector256 lower, Vector256 upper) = Vector256.Widen(value); - return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), alpha, source * alpha); } - /// - /// Unpacks thirty-two binary16 values into two vectors of single-precision values. - /// - /// The packed binary16 values. - /// The unpacked lower and upper values. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static (Vector512 Lower, Vector512 Upper) Unpack(Vector512 value) + private static Vector512 Associate(Vector512 source) { - (Vector512 lower, Vector512 upper) = Vector512.Widen(value); - return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + return Vector512.ConditionalSelect(alphaMask, alpha, source * alpha); } /// - /// Packs eight single-precision values into binary16 storage. + /// Associates the first three components with alpha rounded to half-precision storage. /// - /// The lower single-precision values. - /// The upper single-precision values. - /// The packed binary16 values. + /// The unassociated components. + /// The associated components with their stored alpha. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector128 Pack(Vector128 lower, Vector128 upper) - => Vector128.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); + private static Vector128 AssociateForStorage(Vector128 source) + { + Vector128 alpha = RoundToHalf(Vector128_.ShuffleNative(source, 0b_11_11_11_11)); + return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), alpha, source * alpha); + } - /// - /// Packs sixteen single-precision values into binary16 storage. - /// - /// The lower single-precision values. - /// The upper single-precision values. - /// The packed binary16 values. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector256 Pack(Vector256 lower, Vector256 upper) - => Vector256.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); + private static Vector256 AssociateForStorage(Vector256 source) + { + Vector256 alpha = RoundToHalf(Vector256_.ShuffleNative(source, 0b_11_11_11_11)); + return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), alpha, source * alpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 AssociateForStorage(Vector512 source) + { + Vector512 alpha = RoundToHalf(Vector512_.ShuffleNative(source, 0b_11_11_11_11)); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + return Vector512.ConditionalSelect(alphaMask, alpha, source * alpha); + } /// - /// Packs thirty-two single-precision values into binary16 storage. + /// Unassociates complete four-component vectors while preserving their fourth component. /// - /// The lower single-precision values. - /// The upper single-precision values. - /// The packed binary16 values. + /// The associated components. + /// The unassociated components. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector512 Pack(Vector512 lower, Vector512 upper) - => Vector512.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); + private static Vector128 Unassociate(Vector128 source) + => Numerics.UnPremultiply(source, Vector128_.ShuffleNative(source, 0b_11_11_11_11)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Unassociate(Vector256 source) + => Numerics.UnPremultiply(source, Vector256_.ShuffleNative(source, 0b_11_11_11_11)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Unassociate(Vector512 source) + => Numerics.UnPremultiply(source, Vector512_.ShuffleNative(source, 0b_11_11_11_11)); /// - /// Rounds single-precision values through binary16 without changing the vector width. + /// Rounds single-precision values through half-precision without changing the vector width. /// /// The single-precision values. - /// The values after binary16 quantization. + /// The values after half-precision quantization. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static Vector128 RoundToHalf(Vector128 value) => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value)); /// - /// Rounds single-precision values through binary16 without changing the vector width. + /// Rounds single-precision values through half-precision without changing the vector width. /// /// The single-precision values. - /// The values after binary16 quantization. + /// The values after half-precision quantization. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static Vector256 RoundToHalf(Vector256 value) => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value)); /// - /// Rounds single-precision values through binary16 without changing the vector width. + /// Rounds single-precision values through half-precision without changing the vector width. /// /// The single-precision values. - /// The values after binary16 quantization. + /// The values after half-precision quantization. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static Vector512 RoundToHalf(Vector512 value) => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value)); /// - /// Converts zero-extended binary16 bit patterns to single-precision values. + /// Converts zero-extended half-precision bit patterns to single-precision values. /// - /// The binary16 bit patterns. + /// The half-precision bit patterns. /// The converted single-precision values. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 ConvertHalfBitsToSingle(Vector128 value) @@ -382,7 +593,7 @@ private static Vector128 ConvertHalfBitsToSingle(Vector128 value) Vector128 maskedExponentLowerBound = subnormalMask & Vector128.Create(SingleExponentLowerBound); Vector128 exponentOffset = Vector128.Create(SingleExponentOffset) | maskedExponentLowerBound; - // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values + // Half and float fraction fields differ by thirteen bits. Subnormals and special values // need different exponent offsets before that shared field layout can be reinterpreted as float. Vector128 bits = Vector128.ShiftLeft(value, 13) & Vector128.Create(HalfToSingleBitsMask); exponentOffset = Vector128.ConditionalSelect(infinityOrNaNMask, Vector128.ShiftLeft(exponentOffset, 1), exponentOffset); @@ -392,9 +603,9 @@ private static Vector128 ConvertHalfBitsToSingle(Vector128 value) } /// - /// Converts zero-extended binary16 bit patterns to single-precision values. + /// Converts zero-extended half-precision bit patterns to single-precision values. /// - /// The binary16 bit patterns. + /// The half-precision bit patterns. /// The converted single-precision values. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector256 ConvertHalfBitsToSingle(Vector256 value) @@ -406,7 +617,7 @@ private static Vector256 ConvertHalfBitsToSingle(Vector256 value) Vector256 maskedExponentLowerBound = subnormalMask & Vector256.Create(SingleExponentLowerBound); Vector256 exponentOffset = Vector256.Create(SingleExponentOffset) | maskedExponentLowerBound; - // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values + // Half and float fraction fields differ by thirteen bits. Subnormals and special values // need different exponent offsets before that shared field layout can be reinterpreted as float. Vector256 bits = Vector256.ShiftLeft(value, 13) & Vector256.Create(HalfToSingleBitsMask); exponentOffset = Vector256.ConditionalSelect(infinityOrNaNMask, Vector256.ShiftLeft(exponentOffset, 1), exponentOffset); @@ -416,9 +627,9 @@ private static Vector256 ConvertHalfBitsToSingle(Vector256 value) } /// - /// Converts zero-extended binary16 bit patterns to single-precision values. + /// Converts zero-extended half-precision bit patterns to single-precision values. /// - /// The binary16 bit patterns. + /// The half-precision bit patterns. /// The converted single-precision values. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector512 ConvertHalfBitsToSingle(Vector512 value) @@ -430,7 +641,7 @@ private static Vector512 ConvertHalfBitsToSingle(Vector512 value) Vector512 maskedExponentLowerBound = subnormalMask & Vector512.Create(SingleExponentLowerBound); Vector512 exponentOffset = Vector512.Create(SingleExponentOffset) | maskedExponentLowerBound; - // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values + // Half and float fraction fields differ by thirteen bits. Subnormals and special values // need different exponent offsets before that shared field layout can be reinterpreted as float. Vector512 bits = Vector512.ShiftLeft(value, 13) & Vector512.Create(HalfToSingleBitsMask); exponentOffset = Vector512.ConditionalSelect(infinityOrNaNMask, Vector512.ShiftLeft(exponentOffset, 1), exponentOffset); @@ -440,10 +651,10 @@ private static Vector512 ConvertHalfBitsToSingle(Vector512 value) } /// - /// Converts single-precision values to zero-extended binary16 bit patterns. + /// Converts single-precision values to zero-extended half-precision bit patterns. /// /// The single-precision values. - /// The binary16 bit patterns in 32-bit lanes. + /// The half-precision bit patterns in 32-bit lanes. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 ConvertSingleToHalfBits(Vector128 value) { @@ -456,12 +667,14 @@ private static Vector128 ConvertSingleToHalfBits(Vector128 value) exponentOffset &= Vector128.Create(SingleBiasedExponentMask); exponentOffset += Vector128.Create(SingleExponent13); - // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE + // Adding an exponent-sized float rounds the significand to half precision using IEEE // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields. value += exponentOffset.AsSingle(); bits = value.AsUInt32() - Vector128.Create(SingleExponent126); Vector128 newExponent = Vector128.ShiftRightLogical(bits, 13); - Vector128 maskedHalfExponentForNaN = ~realMask & Vector128.Create(HalfExponentMask); + + // A NaN needs a nonzero fraction; an all-ones exponent alone encodes infinity. + Vector128 maskedHalfExponentForNaN = ~realMask & Vector128.Create(HalfExponentMask | HalfQuietNaNMask); bits &= realMask; bits += newExponent; bits &= ~maskedHalfExponentForNaN; @@ -469,10 +682,10 @@ private static Vector128 ConvertSingleToHalfBits(Vector128 value) } /// - /// Converts single-precision values to zero-extended binary16 bit patterns. + /// Converts single-precision values to zero-extended half-precision bit patterns. /// /// The single-precision values. - /// The binary16 bit patterns in 32-bit lanes. + /// The half-precision bit patterns in 32-bit lanes. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector256 ConvertSingleToHalfBits(Vector256 value) { @@ -485,12 +698,14 @@ private static Vector256 ConvertSingleToHalfBits(Vector256 value) exponentOffset &= Vector256.Create(SingleBiasedExponentMask); exponentOffset += Vector256.Create(SingleExponent13); - // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE + // Adding an exponent-sized float rounds the significand to half precision using IEEE // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields. value += exponentOffset.AsSingle(); bits = value.AsUInt32() - Vector256.Create(SingleExponent126); Vector256 newExponent = Vector256.ShiftRightLogical(bits, 13); - Vector256 maskedHalfExponentForNaN = ~realMask & Vector256.Create(HalfExponentMask); + + // A NaN needs a nonzero fraction; an all-ones exponent alone encodes infinity. + Vector256 maskedHalfExponentForNaN = ~realMask & Vector256.Create(HalfExponentMask | HalfQuietNaNMask); bits &= realMask; bits += newExponent; bits &= ~maskedHalfExponentForNaN; @@ -498,10 +713,10 @@ private static Vector256 ConvertSingleToHalfBits(Vector256 value) } /// - /// Converts single-precision values to zero-extended binary16 bit patterns. + /// Converts single-precision values to zero-extended half-precision bit patterns. /// /// The single-precision values. - /// The binary16 bit patterns in 32-bit lanes. + /// The half-precision bit patterns in 32-bit lanes. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector512 ConvertSingleToHalfBits(Vector512 value) { @@ -514,12 +729,14 @@ private static Vector512 ConvertSingleToHalfBits(Vector512 value) exponentOffset &= Vector512.Create(SingleBiasedExponentMask); exponentOffset += Vector512.Create(SingleExponent13); - // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE + // Adding an exponent-sized float rounds the significand to half precision using IEEE // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields. value += exponentOffset.AsSingle(); bits = value.AsUInt32() - Vector512.Create(SingleExponent126); Vector512 newExponent = Vector512.ShiftRightLogical(bits, 13); - Vector512 maskedHalfExponentForNaN = ~realMask & Vector512.Create(HalfExponentMask); + + // A NaN needs a nonzero fraction; an all-ones exponent alone encodes infinity. + Vector512 maskedHalfExponentForNaN = ~realMask & Vector512.Create(HalfExponentMask | HalfQuietNaNMask); bits &= realMask; bits += newExponent; bits &= ~maskedHalfExponentForNaN; diff --git a/src/ImageSharp/PixelFormats/IPixel.cs b/src/ImageSharp/PixelFormats/IPixel.cs index a4f6312dd2..68e3720747 100644 --- a/src/ImageSharp/PixelFormats/IPixel.cs +++ b/src/ImageSharp/PixelFormats/IPixel.cs @@ -23,7 +23,6 @@ public interface IPixel : IPixel, IEquatable /// /// Initializes the pixel instance from a generic ("scaled") representation using the pixel type's native alpha representation. - /// The scaled representation uses 0 and 1 as its nominal component bounds. /// /// The vector to load the pixel from. /// The . @@ -31,7 +30,6 @@ public interface IPixel : IPixel, IEquatable /// /// Initializes the pixel instance from a generic ("scaled") whose color components use unassociated alpha. - /// The scaled representation uses 0 and 1 as its nominal component bounds. /// /// The vector to load the pixel from. /// The . @@ -40,7 +38,6 @@ public interface IPixel : IPixel, IEquatable /// /// Initializes the pixel instance from a generic ("scaled") whose color components use associated alpha, /// representing color multiplied by the logical opacity represented by alpha. - /// The scaled representation uses 0 and 1 as its nominal component bounds. /// /// The vector to load the pixel from. /// The . @@ -179,7 +176,6 @@ public interface IPixel /// /// Expands the pixel into a generic ("scaled") representation using the pixel type's native alpha representation. - /// The scaled representation uses 0 and 1 as its nominal component bounds. /// The vector components are typically expanded in least to greatest significance order. /// /// The . @@ -187,7 +183,6 @@ public interface IPixel /// /// Expands the pixel into a generic ("scaled") whose color components use unassociated alpha. - /// The scaled representation uses 0 and 1 as its nominal component bounds. /// When alpha is zero and the pixel's native representation is associated, the color components remain unchanged /// because no unassociated value can be recovered. /// @@ -197,7 +192,6 @@ public interface IPixel /// /// Expands the pixel into a generic ("scaled") whose color components use associated alpha, /// representing color multiplied by the logical opacity represented by alpha. - /// The scaled representation uses 0 and 1 as its nominal component bounds. /// /// The . public Vector4 ToAssociatedScaledVector4(); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs index 338c55fe36..2a63b25012 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs @@ -208,7 +208,10 @@ public override readonly string ToString() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ushort Pack(Vector3 vector) { - vector = Vector3.Clamp(vector, Vector3.Zero, Vector3.One); + vector = new Vector3( + Numerics.Clamp(vector.X, 0F, 1F), + Numerics.Clamp(vector.Y, 0F, 1F), + Numerics.Clamp(vector.Z, 0F, 1F)); return (ushort)((((int)Math.Round(vector.X * 31F) & 0x1F) << 11) | (((int)Math.Round(vector.Y * 63F) & 0x3F) << 5) diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs index cbb0cf0866..a44547b76a 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs @@ -7,11 +7,11 @@ namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing a single IEEE 754 binary16 floating-point value. +/// Packed pixel type containing a single IEEE 754 half-precision floating-point value. /// /// -/// returns the stored IEEE 754 binary16 value directly. Scaled vector conversions normalize -/// the finite range [-65504, 65504] to [0, 1]. The packed representation is binary-compatible with +/// Native and scaled vector conversions return the stored IEEE 754 half-precision value directly. +/// The packed representation is binary-compatible with /// DXGI_FORMAT_R16_FLOAT. /// public partial struct HalfSingle : IPixel, IPackedVector @@ -53,11 +53,7 @@ public partial struct HalfSingle : IPixel, IPackedVector /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToScaledVector4() - { - float scaled = HalfTypeHelper.ToScaled(this.ToSingle()); - return new Vector4(scaled, 0, 0, 1F); - } + public readonly Vector4 ToScaledVector4() => this.ToVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -110,15 +106,13 @@ public static HalfSingle FromAssociatedScaledVector4(Vector4 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfSingle FromAssociatedVector4(Vector4 source) { - // This format has no native alpha component: X uses the binary16 finite range, while W remains the source opacity. - source.X = HalfTypeHelper.ToScaled(source.X); - return FromAssociatedScaledVector4(source); + Numerics.UnPremultiply(ref source); + return FromVector4(source); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HalfSingle FromScaledVector4(Vector4 source) - => new() { PackedValue = HalfTypeHelper.Pack(HalfTypeHelper.FromScaled(source.X)) }; + public static HalfSingle FromScaledVector4(Vector4 source) => FromVector4(source); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs index ad6c7aefab..bd7b8144ac 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs @@ -7,11 +7,11 @@ namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two IEEE 754 binary16 floating-point values. +/// Packed pixel type containing two IEEE 754 half-precision floating-point values. /// /// -/// and return the stored IEEE 754 binary16 values directly. Scaled -/// vector conversions normalize the finite range [-65504, 65504] to [0, 1]. The packed representation is +/// Native and scaled vector conversions return the stored IEEE 754 half-precision values directly. +/// The packed representation is /// binary-compatible with DXGI_FORMAT_R16G16_FLOAT. /// public partial struct HalfVector2 : IPixel, IPackedVector @@ -60,11 +60,7 @@ public partial struct HalfVector2 : IPixel, IPackedVector /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToScaledVector4() - { - Vector2 scaled = HalfTypeHelper.ToScaled(this.ToVector2()); - return new Vector4(scaled, 0F, 1F); - } + public readonly Vector4 ToScaledVector4() => this.ToVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -121,20 +117,13 @@ public static HalfVector2 FromAssociatedScaledVector4(Vector4 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfVector2 FromAssociatedVector4(Vector4 source) { - // This format has no native alpha component: XY use the binary16 finite range, while W remains the source opacity. - Vector2 scaled = HalfTypeHelper.ToScaled(new Vector2(source.X, source.Y)); - source.X = scaled.X; - source.Y = scaled.Y; - return FromAssociatedScaledVector4(source); + Numerics.UnPremultiply(ref source); + return FromVector4(source); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HalfVector2 FromScaledVector4(Vector4 source) - { - Vector2 native = HalfTypeHelper.FromScaled(new Vector2(source.X, source.Y)); - return new HalfVector2 { PackedValue = Pack(native.X, native.Y) }; - } + public static HalfVector2 FromScaledVector4(Vector4 source) => FromVector4(source); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs index 42531937cb..b8df1319f2 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs @@ -7,12 +7,11 @@ namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four IEEE 754 binary16 floating-point values. +/// Packed pixel type containing four IEEE 754 half-precision floating-point values. /// /// -/// returns the stored IEEE 754 binary16 values directly. Scaled vector conversions normalize -/// the finite range [-65504, 65504] to [0, 1]. The packed representation is binary-compatible with -/// DXGI_FORMAT_R16G16B16A16_FLOAT. +/// Native and scaled vector conversions return the stored IEEE 754 half-precision values directly. +/// The packed representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. /// public partial struct HalfVector4 : IPixel, IPackedVector { @@ -65,7 +64,7 @@ public HalfVector4(float x, float y, float z, float w) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToScaledVector4() => HalfTypeHelper.ToScaled(this.ToVector4()); + public readonly Vector4 ToScaledVector4() => this.ToVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -106,10 +105,9 @@ public readonly Vector4 ToAssociatedScaledVector4() [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Vector4 ToAssociatedVector4() { - Vector4 vector = this.ToAssociatedScaledVector4(); - - // Association is defined in scaled color space, so map the associated result back to the native binary16 range. - return HalfTypeHelper.FromScaled(vector); + Vector4 vector = this.ToVector4(); + Numerics.Premultiply(ref vector); + return vector; } /// @@ -132,14 +130,13 @@ public static HalfVector4 FromAssociatedScaledVector4(Vector4 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfVector4 FromAssociatedVector4(Vector4 source) { - // Restore scaled opacity before unassociating the color channels. - source = HalfTypeHelper.ToScaled(source); - return FromAssociatedScaledVector4(source); + Numerics.UnPremultiply(ref source); + return FromVector4(source); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HalfVector4 FromScaledVector4(Vector4 source) => FromVector4(HalfTypeHelper.FromScaled(source)); + public static HalfVector4 FromScaledVector4(Vector4 source) => FromVector4(source); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs index ca90afcf75..2ceb6898da 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs @@ -10,9 +10,8 @@ namespace SixLabors.ImageSharp.PixelFormats; /// Packed pixel type containing four associated 16-bit floating-point values. /// /// -/// returns the stored associated IEEE 754 binary16 values directly. Scaled vector conversions -/// normalize the finite range [-65504, 65504] to [0, 1] while preserving associated alpha. The packed -/// representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. +/// Native and scaled vector conversions preserve the stored associated IEEE 754 half-precision values. +/// The packed representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. /// public partial struct HalfVector4P : IPixel, IPackedVector { @@ -61,16 +60,11 @@ public readonly Rgba32 ToRgba32() /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToScaledVector4() => HalfTypeHelper.ToScaled(this.ToVector4()); + public readonly Vector4 ToScaledVector4() => this.ToVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToUnassociatedScaledVector4() - { - Vector4 vector = this.ToScaledVector4(); - Numerics.UnPremultiply(ref vector); - return vector; - } + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToUnassociatedVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -87,8 +81,9 @@ public readonly Vector4 ToUnassociatedScaledVector4() [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Vector4 ToUnassociatedVector4() { - Vector4 vector = this.ToUnassociatedScaledVector4(); - return HalfTypeHelper.FromScaled(vector); + Vector4 vector = this.ToVector4(); + Numerics.UnPremultiply(ref vector); + return vector; } /// @@ -115,25 +110,25 @@ public static PixelTypeInfo GetPixelTypeInfo() [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfVector4P FromUnassociatedVector4(Vector4 source) { - source = HalfTypeHelper.ToScaled(source); - return FromUnassociatedScaledVector4(source); + // Association uses the alpha that half-precision stores so the color components + // retain their straight values when the supplied alpha rounds on packing. + source.W = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(source.W)); + Numerics.Premultiply(ref source); + return new HalfVector4P(source); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfVector4P FromAssociatedVector4(Vector4 source) - { - source = HalfTypeHelper.ToScaled(source); - return FromAssociatedScaledVector4(source); - } + => new(source); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HalfVector4P FromUnassociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Associate(source)); + public static HalfVector4P FromUnassociatedScaledVector4(Vector4 source) => FromUnassociatedVector4(source); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HalfVector4P FromAssociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Reassociate(source)); + public static HalfVector4P FromAssociatedScaledVector4(Vector4 source) => FromAssociatedVector4(source); /// public static HalfVector4P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); @@ -191,74 +186,11 @@ public override readonly string ToString() } /// - /// Converts an unassociated scaled vector to the associated representation of a half-precision destination. - /// - /// The unassociated scaled vector. - /// The associated scaled vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector4 Associate(Vector4 source) - { - source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); - - // RGB must use the scaled alpha that the binary16 representation can reproduce. - source.W = QuantizeScaledAlpha(source.W); - Numerics.Premultiply(ref source); - return source; - } - - /// - /// Reassociates a scaled vector with the alpha value the destination stores. - /// - /// The associated scaled vector. - /// The reassociated scaled vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector4 Reassociate(Vector4 source) - { - float alpha = source.W; - - if (alpha <= 0) - { - return Vector4.Zero; - } - - float storedAlpha = QuantizeScaledAlpha(alpha); - - // Associated RGB scales by the same ratio as alpha. Applying that ratio directly avoids the extra division and multiplication of an unpremultiply/premultiply round trip and preserves exact midpoints when alpha needs no quantization. - source *= storedAlpha / alpha; - source.W = storedAlpha; - Numerics.ClampRgbToAlpha(ref source); - return source; - } - - /// - /// Packs an associated scaled vector into the native binary16 representation. + /// Packs the four native half-precision components in DirectX component order. /// - /// The associated scaled vector. - /// The packed pixel. + /// The component values. + /// The packed half-precision value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static HalfVector4P PackAssociatedScaledVector4(Vector4 source) - { - source = HalfTypeHelper.FromScaled(source); - return new HalfVector4P { PackedValue = Pack(source) }; - } - - /// - /// Quantizes scaled alpha through the native binary16 representation. - /// - /// The scaled alpha value. - /// The scaled value represented by the stored binary16 component. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float QuantizeScaledAlpha(float alpha) - { - float nativeAlpha = HalfTypeHelper.FromScaled(alpha); - return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha))); - } - - /// - /// Packs native half-precision components into a 64-bit value. - /// - /// The native component values. - /// The packed value. private static ulong Pack(Vector4 vector) { ulong x = HalfTypeHelper.Pack(vector.X); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs index 6039bd9d7c..3df42dc808 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs @@ -240,7 +240,7 @@ public override readonly string ToString() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ushort Pack(Vector2 vector) { - vector = Vector2.Clamp(vector, MinusOne, Vector2.One) * Half; + vector = Numerics.Clamp(vector, MinusOne, Vector2.One) * Half; int byte2 = ((ushort)Convert.ToInt16(Math.Round(vector.X)) & 0xFF) << 0; int byte1 = ((ushort)Convert.ToInt16(Math.Round(vector.Y)) & 0xFF) << 8; diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs index 3da0f7f0b9..1c212fc732 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs @@ -242,7 +242,7 @@ public override readonly string ToString() private static uint Pack(Vector2 vector) { vector *= Max; - vector = Vector2.Clamp(vector, Min, Max); + vector = Numerics.Clamp(vector, Min, Max); // Round rather than truncate. uint word2 = (uint)((int)MathF.Round(vector.X) & 0xFFFF); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs index a921f99bae..26bcce19e7 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.PixelFormats; @@ -21,8 +22,7 @@ protected override void ToUnassociatedVector4(Configuration configuration, ReadO { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - // The half-vector layouts are identical, so the shared expansion kernel can process the source without copying it. - RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination[..source.Length]); + HalfTypeHelper.Unpack(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -32,37 +32,24 @@ protected override void ToAssociatedVector4(Configuration configuration, ReadOnl destination = destination[..source.Length]; - // Association uses normalized opacity, not the native binary16 alpha value. - RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - HalfTypeHelper.ToScaled(destination); - Numerics.Premultiply(destination); - HalfTypeHelper.FromScaled(destination); + HalfTypeHelper.UnpackAssociated(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination)); } /// protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) - { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - destination = destination[..source.Length]; - RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - HalfTypeHelper.ToScaled(destination); - } + => this.ToUnassociatedVector4(configuration, source, destination); /// protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) - { - this.ToUnassociatedScaledVector4(configuration, source, destination); - Numerics.Premultiply(destination[..source.Length]); - } + => this.ToAssociatedVector4(configuration, source, destination); /// protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - // DirectX half vectors are not normalized formats, so values outside the nominal color range must reach storage unchanged. - RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); + // DirectX half vectors store the half-precision result, including finite values outside [0, 1]. + HalfTypeHelper.Pack(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -70,30 +57,15 @@ 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. - HalfTypeHelper.ToScaled(source); - Numerics.UnPremultiply(source); - HalfTypeHelper.FromScaled(source); - RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); + HalfTypeHelper.PackFromAssociated(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination[..source.Length])); } /// protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) - { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - HalfTypeHelper.FromScaled(source); - RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); - } + => this.FromUnassociatedVector4Destructive(configuration, source, destination); /// protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) - { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - Numerics.UnPremultiply(source); - HalfTypeHelper.FromScaled(source); - RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); - } + => this.FromAssociatedVector4Destructive(configuration, source, destination); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs index 172159a49c..4e6c0bed17 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs @@ -2,11 +2,8 @@ // 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; -using SixLabors.ImageSharp.PixelFormats.Utils; namespace SixLabors.ImageSharp.PixelFormats; @@ -16,15 +13,18 @@ namespace SixLabors.ImageSharp.PixelFormats; public partial struct HalfVector4P { /// - /// Provides optimized bulk operations for . + /// Provides bulk operations for associated half-precision pixels. /// internal class PixelOperations : AssociatedAlphaPixelOperations { /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { - this.ToUnassociatedScaledVector4(configuration, source, destination); - HalfTypeHelper.FromScaled(destination[..source.Length]); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + HalfTypeHelper.UnpackUnassociated( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -32,34 +32,27 @@ protected override void ToAssociatedVector4(Configuration configuration, ReadOnl { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination[..source.Length]); + HalfTypeHelper.Unpack( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) - { - this.ToAssociatedScaledVector4(configuration, source, destination); - Numerics.UnPremultiply(destination[..source.Length]); - } + => this.ToUnassociatedVector4(configuration, source, destination); /// protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) - { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - destination = destination[..source.Length]; - RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - HalfTypeHelper.ToScaled(destination); - } + => this.ToAssociatedVector4(configuration, source, destination); /// protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - HalfTypeHelper.ToScaled(source); - Associate(source); - PackAssociatedScaled(source, destination[..source.Length]); + HalfTypeHelper.PackAssociated( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -67,298 +60,17 @@ protected override void FromAssociatedVector4Destructive(Configuration configura { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - HalfTypeHelper.ToScaled(source); - Reassociate(source); - PackAssociatedScaled(source, destination[..source.Length]); + HalfTypeHelper.Pack( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) - { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - Associate(source); - PackAssociatedScaled(source, destination[..source.Length]); - } + => this.FromUnassociatedVector4Destructive(configuration, source, destination); /// protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) - { - Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - - Reassociate(source); - PackAssociatedScaled(source, destination[..source.Length]); - } - - /// - /// Associates scaled vectors with the alpha values representable by native binary16 storage. - /// - /// The vectors to convert in place. - private static void Associate(Span source) - { - ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - int vectorsPerRegister = Vector512.Count / Vector128.Count; - - for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) - { - ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); - vector = Associate(vector); - } - } - - if (Vector256.IsHardwareAccelerated) - { - int vectorsPerRegister = Vector256.Count / Vector128.Count; - - for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) - { - ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); - vector = Associate(vector); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i < source.Length; i++) - { - ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); - vector = Associate(vector); - } - - return; - } - - for (; i < source.Length; i++) - { - Unsafe.Add(ref sourceBase, (uint)i) = HalfVector4P.Associate(Unsafe.Add(ref sourceBase, (uint)i)); - } - } - - /// - /// Reassociates scaled vectors with the alpha values representable by native binary16 storage. - /// - /// The vectors to convert in place. - private static void Reassociate(Span source) - { - ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - int vectorsPerRegister = Vector512.Count / Vector128.Count; - - for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) - { - ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); - vector = Reassociate(vector); - } - } - - if (Vector256.IsHardwareAccelerated) - { - int vectorsPerRegister = Vector256.Count / Vector128.Count; - - for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) - { - ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); - vector = Reassociate(vector); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i < source.Length; i++) - { - ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); - vector = Reassociate(vector); - } - - return; - } - - for (; i < source.Length; i++) - { - Unsafe.Add(ref sourceBase, (uint)i) = HalfVector4P.Reassociate(Unsafe.Add(ref sourceBase, (uint)i)); - } - } - - /// - /// Converts an unassociated scaled vector to associated scaled components. - /// - /// The unassociated vectors. - /// The associated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 Associate(Vector128 source) - { - source = ClampUnit(source); - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 storedAlpha = QuantizeScaledAlpha(alpha); - Vector128 result = source * storedAlpha; - return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - } - - /// - /// Converts unassociated scaled vectors to associated scaled components. - /// - /// The unassociated vectors. - /// The associated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 Associate(Vector256 source) - { - source = ClampUnit(source); - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 storedAlpha = QuantizeScaledAlpha(alpha); - Vector256 result = source * storedAlpha; - return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); - } - - /// - /// Converts unassociated scaled vectors to associated scaled components. - /// - /// The unassociated vectors. - /// The associated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 Associate(Vector512 source) - { - source = ClampUnit(source); - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 storedAlpha = QuantizeScaledAlpha(alpha); - Vector512 result = source * storedAlpha; - Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); - return Vector512.ConditionalSelect(alphaMask, storedAlpha, result); - } - - /// - /// Reassociates an associated scaled vector after alpha quantization. - /// - /// The associated vectors. - /// The reassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 Reassociate(Vector128 source) - { - Vector128 zero = Vector128.Zero; - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 storedAlpha = QuantizeScaledAlpha(alpha); - Vector128 result = source * (storedAlpha / alpha); - result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - - // 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); - } - - /// - /// Reassociates associated scaled vectors after alpha quantization. - /// - /// The associated vectors. - /// The reassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 Reassociate(Vector256 source) - { - Vector256 zero = Vector256.Zero; - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - 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); - - // 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); - } - - /// - /// Reassociates associated scaled vectors after alpha quantization. - /// - /// The associated vectors. - /// The reassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 Reassociate(Vector512 source) - { - Vector512 zero = Vector512.Zero; - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 storedAlpha = QuantizeScaledAlpha(alpha); - 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); - - // 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); - } - - /// - /// Quantizes scaled alpha through the native binary16 representation. - /// - /// The scaled alpha lanes. - /// The scaled alpha values represented by binary16 storage. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 QuantizeScaledAlpha(Vector128 alpha) - { - Vector128 native = HalfTypeHelper.FromScaled(alpha); - return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); - } - - /// - /// Quantizes scaled alpha through the native binary16 representation. - /// - /// The scaled alpha lanes. - /// The scaled alpha values represented by binary16 storage. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 QuantizeScaledAlpha(Vector256 alpha) - { - Vector256 native = HalfTypeHelper.FromScaled(alpha); - return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); - } - - /// - /// Quantizes scaled alpha through the native binary16 representation. - /// - /// The scaled alpha lanes. - /// The scaled alpha values represented by binary16 storage. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 QuantizeScaledAlpha(Vector512 alpha) - { - Vector512 native = HalfTypeHelper.FromScaled(alpha); - return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); - } - - /// - /// 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) => Numerics.Clamp(source, Vector128.Zero, Vector128.One); - - /// - /// 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) => Numerics.Clamp(source, Vector256.Zero, Vector256.One); - - /// - /// 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) => Numerics.Clamp(source, Vector512.Zero, Vector512.One); - - /// - /// Maps associated scaled vectors to native components and packs them as binary16 values. - /// - /// The associated scaled vectors. - /// The destination pixels. - private static void PackAssociatedScaled(Span source, Span destination) - { - HalfTypeHelper.FromScaled(source); - RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination)); - } + => this.FromAssociatedVector4Destructive(configuration, source, destination); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs index 5928cbe522..c5bef6abab 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs @@ -448,9 +448,9 @@ private static Vector512 Associate(Vector512 source, bool scaled) source = (source + one) / Vector512.Create(2F); } - source = Vector512.Min(Vector512.Max(source, zero), one); + source = Numerics.Clamp(source, zero, one); Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 nativeAlpha = Vector512.Min(Vector512.Max((alpha * Vector512.Create(2F)) - one, -one), one); + Vector512 nativeAlpha = Numerics.Clamp((alpha * Vector512.Create(2F)) - one, -one, one); Vector512 storedAlpha = Vector512.Round(nativeAlpha * Vector512.Create(MaxPos)); storedAlpha += Vector512.Create(MaxPos); storedAlpha /= Vector512.Create(ScaledMagnitude); @@ -477,9 +477,9 @@ private static Vector256 Associate(Vector256 source, bool scaled) source = (source + one) / Vector256.Create(2F); } - source = Vector256.Min(Vector256.Max(source, zero), one); + source = Numerics.Clamp(source, zero, one); Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 nativeAlpha = Vector256.Min(Vector256.Max((alpha * Vector256.Create(2F)) - one, -one), one); + Vector256 nativeAlpha = Numerics.Clamp((alpha * Vector256.Create(2F)) - one, -one, one); Vector256 storedAlpha = Vector256.Round(nativeAlpha * Vector256.Create(MaxPos)); storedAlpha += Vector256.Create(MaxPos); storedAlpha /= Vector256.Create(ScaledMagnitude); @@ -506,9 +506,9 @@ private static Vector128 Associate(Vector128 source, bool scaled) source = (source + one) / Vector128.Create(2F); } - source = Vector128.Min(Vector128.Max(source, zero), one); + source = Numerics.Clamp(source, zero, one); Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 nativeAlpha = Vector128.Min(Vector128.Max((alpha * Vector128.Create(2F)) - one, -one), one); + Vector128 nativeAlpha = Numerics.Clamp((alpha * Vector128.Create(2F)) - one, -one, one); Vector128 storedAlpha = Vector128.Round(nativeAlpha * Vector128.Create(MaxPos)); storedAlpha += Vector128.Create(MaxPos); storedAlpha /= Vector128.Create(ScaledMagnitude); @@ -609,14 +609,14 @@ private static Vector512 Reassociate(Vector512 source, bool scaled } Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 nativeAlpha = Vector512.Min(Vector512.Max((alpha * Vector512.Create(2F)) - one, -one), one); + Vector512 nativeAlpha = Numerics.Clamp((alpha * Vector512.Create(2F)) - one, -one, one); Vector512 storedAlpha = Vector512.Round(nativeAlpha * Vector512.Create(MaxPos)); storedAlpha += Vector512.Create(MaxPos); storedAlpha /= Vector512.Create(ScaledMagnitude); 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); + result = Numerics.Clamp(result, zero, storedAlpha); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); } @@ -639,14 +639,14 @@ private static Vector256 Reassociate(Vector256 source, bool scaled } Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 nativeAlpha = Vector256.Min(Vector256.Max((alpha * Vector256.Create(2F)) - one, -one), one); + Vector256 nativeAlpha = Numerics.Clamp((alpha * Vector256.Create(2F)) - one, -one, one); Vector256 storedAlpha = Vector256.Round(nativeAlpha * Vector256.Create(MaxPos)); storedAlpha += Vector256.Create(MaxPos); storedAlpha /= Vector256.Create(ScaledMagnitude); Vector256 result = source * (storedAlpha / alpha); Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); result = Vector256.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + result = Numerics.Clamp(result, zero, storedAlpha); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); } @@ -669,14 +669,14 @@ private static Vector128 Reassociate(Vector128 source, bool scaled } Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 nativeAlpha = Vector128.Min(Vector128.Max((alpha * Vector128.Create(2F)) - one, -one), one); + Vector128 nativeAlpha = Numerics.Clamp((alpha * Vector128.Create(2F)) - one, -one, one); Vector128 storedAlpha = Vector128.Round(nativeAlpha * Vector128.Create(MaxPos)); storedAlpha += Vector128.Create(MaxPos); storedAlpha /= Vector128.Create(ScaledMagnitude); Vector128 result = source * (storedAlpha / alpha); Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); result = Vector128.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + result = Numerics.Clamp(result, zero, storedAlpha); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); } @@ -765,7 +765,7 @@ private static Vector512 ConvertToPackedInt32(Vector512 source, bool source -= one; } - source = Vector512.Min(Vector512.Max(source, -one), one) * Vector512.Create(MaxPos); + source = Numerics.Clamp(source, -one, one) * Vector512.Create(MaxPos); return Vector512_.ConvertToInt32RoundToEven(source); } @@ -786,7 +786,7 @@ private static Vector256 ConvertToPackedInt32(Vector256 source, bool source -= one; } - source = Vector256.Min(Vector256.Max(source, -one), one) * Vector256.Create(MaxPos); + source = Numerics.Clamp(source, -one, one) * Vector256.Create(MaxPos); return Vector256_.ConvertToInt32RoundToEven(source); } @@ -807,7 +807,7 @@ private static Vector128 ConvertToPackedInt32(Vector128 source, bool source -= one; } - source = Vector128.Min(Vector128.Max(source, -one), one) * Vector128.Create(MaxPos); + source = Numerics.Clamp(source, -one, one) * Vector128.Create(MaxPos); return Vector128_.ConvertToInt32RoundToEven(source); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs index f3c01e082a..894f55347e 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.PixelFormats; @@ -21,9 +22,7 @@ protected override void ToUnassociatedVector4(Configuration configuration, ReadO { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - // RgbaHalf and RgbaHalfP have the same four-half layout. Sharing the binary16 expansion kernel keeps this path - // vectorized without changing the unassociated meaning of the source components. - RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination[..source.Length]); + HalfTypeHelper.Unpack(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -32,7 +31,7 @@ protected override void ToAssociatedVector4(Configuration configuration, ReadOnl Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); // Association is fused with half expansion so the conversion reads and writes each span once. - RgbaHalfP.PixelOperations.UnpackAssociated(MemoryMarshal.Cast(source), destination[..source.Length]); + HalfTypeHelper.UnpackAssociated(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -48,8 +47,7 @@ protected override void FromUnassociatedVector4Destructive(Configuration configu { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - // The layouts are identical, so the shared packer can write RgbaHalf storage without an intermediate buffer. - RgbaHalfP.PixelOperations.Pack(source, MemoryMarshal.Cast(destination[..source.Length])); + HalfTypeHelper.Pack(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -57,8 +55,8 @@ protected override void FromAssociatedVector4Destructive(Configuration configura { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - // Unassociation is fused with binary16 packing so processors do not pay for another pass over their vector buffer. - RgbaHalfP.PixelOperations.PackFromAssociated(source, MemoryMarshal.Cast(destination[..source.Length])); + // Unassociation is fused with half-precision packing so processors do not pay for another pass over their vector buffer. + HalfTypeHelper.PackFromAssociated(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination[..source.Length])); } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs index a9f3081f1a..86f08b84af 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs @@ -2,9 +2,7 @@ // 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.PixelFormats; @@ -15,7 +13,7 @@ namespace SixLabors.ImageSharp.PixelFormats; public partial struct RgbaHalfP { /// - /// Provides optimized bulk operations for . + /// Provides bulk operations for associated half-precision pixels. /// internal class PixelOperations : AssociatedAlphaPixelOperations { @@ -24,7 +22,9 @@ protected override void ToUnassociatedVector4(Configuration configuration, ReadO { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - UnpackUnassociated(source, destination[..source.Length]); + HalfTypeHelper.UnpackUnassociated( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -32,7 +32,9 @@ protected override void ToAssociatedVector4(Configuration configuration, ReadOnl { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - Unpack(source, destination[..source.Length]); + HalfTypeHelper.Unpack( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -48,7 +50,9 @@ protected override void FromUnassociatedVector4Destructive(Configuration configu { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - PackUnassociated(source, destination[..source.Length]); + HalfTypeHelper.PackAssociated( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -56,7 +60,9 @@ protected override void FromAssociatedVector4Destructive(Configuration configura { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - PackAssociated(source, destination[..source.Length]); + HalfTypeHelper.Pack( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination[..source.Length])); } /// @@ -66,694 +72,5 @@ protected override void FromUnassociatedScaledVector4Destructive(Configuration c /// protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) => this.FromAssociatedVector4Destructive(configuration, source, destination); - - /// - /// Expands binary16 components without changing their unit-range representation. - /// - /// The packed source pixels. - /// The destination vectors. - internal static void Unpack(ReadOnlySpan source, Span destination) - { - ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); - Vector512.StoreUnsafe(lower, ref destinationBase, (nuint)i); - Vector512.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector512.Count)); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); - Vector256.StoreUnsafe(lower, ref destinationBase, (nuint)i); - Vector256.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector256.Count)); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); - Vector128.StoreUnsafe(lower, ref destinationBase, (nuint)i); - Vector128.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector128.Count)); - } - - if (i < componentCount) - { - // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. - ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); - Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); - Vector128.StoreUnsafe(HalfTypeHelper.Unpack(packed).Lower, ref destinationBase, (nuint)i); - } - - return; - } - - ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); - ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToVector4(); - } - } - - /// - /// Expands unassociated binary16 components and associates RGB in the same pass. - /// - /// The packed source pixels. - /// The destination vectors. - internal static void UnpackAssociated(ReadOnlySpan source, Span destination) - { - ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); - Vector512.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); - Vector512.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); - Vector256.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); - Vector256.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); - Vector128.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); - Vector128.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); - } - - if (i < componentCount) - { - // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. - ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); - Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); - Vector128.StoreUnsafe(Associate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i); - } - - return; - } - - ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); - ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Vector4 vector = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToVector4(); - Numerics.Premultiply(ref vector); - Unsafe.Add(ref vectorBase, (uint)pixelIndex) = vector; - } - } - - /// - /// Expands associated binary16 components and unassociates RGB in the same pass. - /// - /// The packed source pixels. - /// The destination vectors. - internal static void UnpackUnassociated(ReadOnlySpan source, Span destination) - { - ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); - Vector512.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); - Vector512.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); - Vector256.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); - Vector256.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); - Vector128.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); - Vector128.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); - } - - if (i < componentCount) - { - // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. - ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); - Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); - Vector128.StoreUnsafe(Unassociate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i); - } - - return; - } - - ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); - ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToUnassociatedVector4(); - } - } - - /// - /// Packs unassociated unit-range vectors directly into binary16 storage. - /// - /// The source vectors. - /// The destination pixels. - internal static void Pack(Span source, Span destination) - { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; 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); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; 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); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; 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 = 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)); - } - - return; - } - - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Vector4 vector = Numerics.Clamp(Unsafe.Add(ref vectorBase, (uint)pixelIndex), Vector4.Zero, Vector4.One); - Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W); - } - } - - /// - /// Packs vectors directly into IEEE 754 binary16 storage without applying unit-range color constraints. - /// - /// The source vectors. - /// The destination pixels. - internal static void PackUnclamped(Span source, Span destination) - { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 lower = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); - Vector512 upper = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)); - Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 lower = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); - Vector256 upper = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)); - Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 lower = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - Vector128 upper = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)); - 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 = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); - Vector128 packed = HalfTypeHelper.Pack(vector, vector); - Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); - } - - return; - } - - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex); - Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W); - } - } - - /// - /// Unassociates vectors and packs unassociated unit-range binary16 storage in the same pass. - /// - /// The associated source vectors. - /// The destination pixels. - internal static void PackFromAssociated(Span source, Span destination) - { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; 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); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; 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); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; 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 = 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)); - } - - return; - } - - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex); - Numerics.UnPremultiply(ref vector); - vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One); - Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W); - } - } - - /// - /// Associates unassociated vectors with their stored binary16 alpha and packs them in one pass. - /// - /// The unassociated source vectors. - /// The destination pixels. - internal static void PackUnassociated(Span source, Span destination) - { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - // Alpha is rounded to the exact binary16 value that will be stored before RGB is associated with it. - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 lower = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector512 upper = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); - Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 lower = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector256 upper = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); - Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 lower = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector128 upper = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); - 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 = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector128 packed = HalfTypeHelper.Pack(vector, vector); - Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); - } - - return; - } - - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromUnassociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex)); - } - } - - /// - /// Reassociates vectors with their stored binary16 alpha and packs them in one pass. - /// - /// The associated source vectors. - /// The destination pixels. - internal static void PackAssociated(Span source, Span destination) - { - ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); - ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - int componentCount = source.Length * Vector128.Count; - int i = 0; - - // Scaling RGB by storedAlpha / inputAlpha preserves straight color when binary16 rounds the alpha channel. - if (Vector512.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector512.Count; i += Vector512.Count) - { - Vector512 lower = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector512 upper = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); - Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector256.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector256.Count; i += Vector256.Count) - { - Vector256 lower = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector256 upper = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); - Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); - } - } - - if (Vector128.IsHardwareAccelerated) - { - for (; i <= componentCount - Vector128.Count; i += Vector128.Count) - { - Vector128 lower = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector128 upper = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); - 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 = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector128 packed = HalfTypeHelper.Pack(vector, vector); - Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); - } - - return; - } - - ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); - ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); - - for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) - { - Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromAssociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex)); - } - } - - /// - /// Associates RGB with alpha while preserving each alpha lane. - /// - /// The unassociated vectors. - /// The associated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 Associate(Vector128 source) - { - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 result = source * alpha; - return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), alpha, result); - } - - /// - /// Associates RGB with alpha while preserving each alpha lane. - /// - /// The unassociated vectors. - /// The associated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 Associate(Vector256 source) - { - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 result = source * alpha; - return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), alpha, result); - } - - /// - /// Associates RGB with alpha while preserving each alpha lane. - /// - /// The unassociated vectors. - /// The associated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 Associate(Vector512 source) - { - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 result = source * alpha; - Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); - return Vector512.ConditionalSelect(alphaMask, alpha, result); - } - - /// - /// Unassociates RGB while preserving each alpha lane. - /// - /// The associated vectors. - /// The unassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 Unassociate(Vector128 source) - { - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - return Numerics.UnPremultiply(source, alpha); - } - - /// - /// Unassociates RGB while preserving each alpha lane. - /// - /// The associated vectors. - /// The unassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 Unassociate(Vector256 source) - { - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - return Numerics.UnPremultiply(source, alpha); - } - - /// - /// Unassociates RGB while preserving each alpha lane. - /// - /// The associated vectors. - /// The unassociated vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 Unassociate(Vector512 source) - { - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - return Numerics.UnPremultiply(source, alpha); - } - - /// - /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. - /// - /// The unassociated vectors. - /// The associated vectors ready for packing. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 AssociateForStorage(Vector128 source) - { - source = Numerics.Clamp(source, Vector128.Zero, Vector128.One); - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); - Vector128 result = source * storedAlpha; - return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - } - - /// - /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. - /// - /// The unassociated vectors. - /// The associated vectors ready for packing. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 AssociateForStorage(Vector256 source) - { - source = Numerics.Clamp(source, Vector256.Zero, Vector256.One); - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); - Vector256 result = source * storedAlpha; - return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); - } - - /// - /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. - /// - /// The unassociated vectors. - /// The associated vectors ready for packing. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 AssociateForStorage(Vector512 source) - { - source = Numerics.Clamp(source, Vector512.Zero, Vector512.One); - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); - Vector512 result = source * storedAlpha; - Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); - return Vector512.ConditionalSelect(alphaMask, storedAlpha, result); - } - - /// - /// Reassociates vectors with the alpha value binary16 storage can reproduce. - /// - /// The associated vectors. - /// The associated vectors ready for packing. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ReassociateForStorage(Vector128 source) - { - Vector128 zero = Vector128.Zero; - Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 clampedAlpha = Numerics.Clamp(alpha, Vector128.Zero, Vector128.One); - Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); - Vector128 result = source * (storedAlpha / alpha); - result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - - // 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); - } - - /// - /// Reassociates vectors with the alpha value binary16 storage can reproduce. - /// - /// The associated vectors. - /// The associated vectors ready for packing. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ReassociateForStorage(Vector256 source) - { - Vector256 zero = Vector256.Zero; - Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 clampedAlpha = Numerics.Clamp(alpha, Vector256.Zero, Vector256.One); - Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); - Vector256 result = source * (storedAlpha / alpha); - result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); - - // 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); - } - - /// - /// Reassociates vectors with the alpha value binary16 storage can reproduce. - /// - /// The associated vectors. - /// The associated vectors ready for packing. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 ReassociateForStorage(Vector512 source) - { - Vector512 zero = Vector512.Zero; - Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 clampedAlpha = Numerics.Clamp(alpha, Vector512.Zero, Vector512.One); - Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); - Vector512 result = source * (storedAlpha / alpha); - Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); - result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); - - // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. - result = Numerics.Clamp(result, zero, storedAlpha); - return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); - } } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs index d4b868871e..1928b98322 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs @@ -17,24 +17,6 @@ public partial struct RgbaVector /// internal class PixelOperations : PixelOperations { - /// - public override void From( - Configuration configuration, - ReadOnlySpan sourcePixels, - Span destinationPixels) - { - Guard.DestinationShouldNotBeTooShort(sourcePixels, destinationPixels, nameof(destinationPixels)); - - destinationPixels = destinationPixels[..sourcePixels.Length]; - Span destinationVectors = MemoryMarshal.Cast(destinationPixels); - - // Cross-format conversion uses public dispatch so associated source pixels are unassociated before entering RgbaVector storage. - PixelOperations.Instance.ToVector4(configuration, sourcePixels, destinationVectors, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); - - // RgbaVector.FromScaledVector4 clamps scaled input, so the optimized bulk path must preserve that behavior after unassociating. - Numerics.Clamp(MemoryMarshal.Cast(destinationVectors), 0F, 1F); - } - /// public override void FromVector4Destructive( Configuration configuration, @@ -46,9 +28,6 @@ public override void FromVector4Destructive( Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers); - // RgbaVector.FromVector4 and FromScaledVector4 both clamp to the representable [0, 1] range. Preserve that scalar - // contract before the zero-copy representation cast so bulk processor output cannot retain HDR or negative values. - Numerics.Clamp(MemoryMarshal.Cast(sourceVectors), 0F, 1F); MemoryMarshal.Cast(sourceVectors).CopyTo(destinationPixels[..sourceVectors.Length]); } @@ -63,6 +42,7 @@ public override void ToVector4( destinationVectors = destinationVectors[..sourcePixels.Length]; MemoryMarshal.Cast(sourcePixels).CopyTo(destinationVectors); + Vector4Converters.ApplyForwardConversionModifiers(destinationVectors, modifiers); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVectorP.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVectorP.PixelOperations.cs new file mode 100644 index 0000000000..519afebc2f --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVectorP.PixelOperations.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides bulk conversion for associated binary32 pixels. +/// +public partial struct RgbaVectorP +{ + /// + /// Bulk conversion for associated binary32 pixels. + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + MemoryMarshal.Cast(source).CopyTo(destination); + + // Unassociate the destination after copying so the stored source components are unchanged. + Numerics.UnPremultiply(destination[..source.Length]); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // The source is destructive by contract, so associate it before copying to storage. + Numerics.Premultiply(source); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToAssociatedVector4(configuration, source, destination); + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromAssociatedVector4Destructive(configuration, source, destination); + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromUnassociatedVector4Destructive(configuration, source, destination); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs index a88823069d..1735235dd4 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs @@ -214,7 +214,7 @@ public override readonly string ToString() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint Pack(Vector2 vector) { - vector = Vector2.Clamp(vector, Vector2.Zero, Vector2.One) * Max; + vector = Numerics.Clamp(vector, Vector2.Zero, Vector2.One) * Max; return (uint)(((int)Math.Round(vector.X) & 0xFFFF) | (((int)Math.Round(vector.Y) & 0xFFFF) << 16)); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs index 573bfca55f..af52326985 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs @@ -8,12 +8,12 @@ namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 16-bit floating-point values typically ranging from 0 to 1. +/// Packed pixel type containing four 16-bit floating-point values. /// The color components are stored in red, green, blue, and alpha order. /// /// -/// and scaled vector conversions return the same component values in the nominal color range -/// [0, 1]. The packed representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. +/// Native and scaled vector conversions return the stored floating-point values. +/// The packed representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. /// [StructLayout(LayoutKind.Sequential)] public partial struct RgbaHalf : IPixel, IPackedVector @@ -140,7 +140,12 @@ public readonly Vector4 ToAssociatedScaledVector4() /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + public readonly Vector4 ToAssociatedVector4() + { + Vector4 vector = this.ToVector4(); + Numerics.Premultiply(ref vector); + return vector; + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -160,7 +165,11 @@ public static RgbaHalf FromAssociatedScaledVector4(Vector4 source) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaHalf FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + public static RgbaHalf FromAssociatedVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -168,11 +177,7 @@ public static RgbaHalf FromAssociatedScaledVector4(Vector4 source) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaHalf FromVector4(Vector4 source) - { - source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); - return new RgbaHalf(source); - } + public static RgbaHalf FromVector4(Vector4 source) => new(source); /// public static RgbaHalf FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs index 75fe4907ce..797514bdb1 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs @@ -8,13 +8,12 @@ namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four associated 16-bit floating-point values typically ranging from 0 to 1. +/// Packed pixel type containing four associated 16-bit floating-point values. /// The color components are stored in red, green, blue, and alpha order. /// /// -/// and scaled vector conversions return the same associated component values in the nominal -/// color range [0, 1]. The packed representation is binary-compatible with -/// DXGI_FORMAT_R16G16B16A16_FLOAT. +/// Native and scaled vectors preserve floating-point component values. +/// The packed representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. /// [StructLayout(LayoutKind.Sequential)] public partial struct RgbaHalfP : IPixel, IPackedVector @@ -112,12 +111,7 @@ public ulong PackedValue /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToUnassociatedScaledVector4() - { - Vector4 vector = this.ToScaledVector4(); - Numerics.UnPremultiply(ref vector); - return vector; - } + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToUnassociatedVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -125,7 +119,12 @@ public readonly Vector4 ToUnassociatedScaledVector4() /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4(); + public readonly Vector4 ToUnassociatedVector4() + { + Vector4 vector = this.ToVector4(); + Numerics.UnPremultiply(ref vector); + return vector; + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -151,43 +150,26 @@ public static PixelTypeInfo GetPixelTypeInfo() /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaHalfP FromUnassociatedScaledVector4(Vector4 source) - { - source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + public static RgbaHalfP FromUnassociatedScaledVector4(Vector4 source) => FromUnassociatedVector4(source); - // RGB must be associated with the alpha value that binary16 storage can reproduce, not the higher-precision input alpha. - source.W = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(source.W)); - Numerics.Premultiply(ref source); - return new RgbaHalfP(source.X, source.Y, source.Z, source.W); - } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromAssociatedScaledVector4(Vector4 source) => FromAssociatedVector4(source); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaHalfP FromAssociatedScaledVector4(Vector4 source) + public static RgbaHalfP FromUnassociatedVector4(Vector4 source) { - float alpha = source.W; - - if (alpha <= 0F) - { - return default; - } - - float storedAlpha = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(Numerics.Clamp(alpha, 0F, 1F))); - - // Preserve the represented straight color when binary16 rounds alpha, then restore the associated RGB <= alpha invariant. - source *= storedAlpha / alpha; - source.W = storedAlpha; - Numerics.ClampRgbToAlpha(ref source); + // Associate with the alpha actually stored as half-precision so a later native unassociation + // observes the same straight color when alpha rounds during packing. + source.W = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(source.W)); + Numerics.Premultiply(ref source); return new RgbaHalfP(source.X, source.Y, source.Z, source.W); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaHalfP FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaHalfP FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + public static RgbaHalfP FromAssociatedVector4(Vector4 source) => new(source.X, source.Y, source.Z, source.W); /// public static RgbaHalfP FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs index b1704d5267..315bf1c0c7 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs @@ -9,11 +9,9 @@ namespace SixLabors.ImageSharp.PixelFormats; /// -/// Unpacked pixel type containing four 32-bit floating-point values typically ranging from 0 to 1. +/// Unpacked pixel type containing four 32-bit floating-point values. /// The color components are stored in red, green, blue, and alpha order. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// +/// Native and scaled vectors preserve floating-point component values. /// /// /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, @@ -125,7 +123,12 @@ public readonly Vector4 ToAssociatedScaledVector4() /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + public readonly Vector4 ToAssociatedVector4() + { + Vector4 vector = this.ToVector4(); + Numerics.Premultiply(ref vector); + return vector; + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -145,7 +148,11 @@ public static RgbaVector FromAssociatedScaledVector4(Vector4 source) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaVector FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + public static RgbaVector FromAssociatedVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -153,11 +160,7 @@ public static RgbaVector FromAssociatedScaledVector4(Vector4 source) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaVector FromVector4(Vector4 source) - { - source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); - return new RgbaVector(source.X, source.Y, source.Z, source.W); - } + public static RgbaVector FromVector4(Vector4 source) => new(source.X, source.Y, source.Z, source.W); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVectorP.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVectorP.cs new file mode 100644 index 0000000000..97be6a7348 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVectorP.cs @@ -0,0 +1,181 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Pixel type containing four associated IEEE 754 binary32 components. +/// Native and scaled vector conversions preserve the stored floating-point values. +/// +/// +/// The component layout is binary-compatible with DXGI_FORMAT_R32G32B32A32_FLOAT. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct RgbaVectorP : IPixel +{ + /// + /// Gets or sets the first associated color component. + /// + public float R; + + /// + /// Gets or sets the second associated color component. + /// + public float G; + + /// + /// Gets or sets the third associated color component. + /// + public float B; + + /// + /// Gets or sets the alpha component. + /// + public float A; + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The first associated color component. + /// The second associated color component. + /// The third associated color component. + /// The alpha component. + public RgbaVectorP(float r, float g, float b, float a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Compares two pixels for equality. + /// + /// The left pixel. + /// The right pixel. + /// Whether the components are equal. + public static bool operator ==(RgbaVectorP left, RgbaVectorP right) => left.Equals(right); + + /// + /// Compares two pixels for inequality. + /// + /// The left pixel. + /// The right pixel. + /// Whether the components differ. + public static bool operator !=(RgbaVectorP left, RgbaVectorP right) => !left.Equals(right); + + /// + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToUnassociatedScaledVector4()); + + /// + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + public readonly Vector4 ToVector4() => new(this.R, this.G, this.B, this.A); + + /// + public readonly Vector4 ToAssociatedScaledVector4() => this.ToVector4(); + + /// + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToUnassociatedVector4(); + + /// + public readonly Vector4 ToUnassociatedVector4() + { + Vector4 vector = this.ToVector4(); + Numerics.UnPremultiply(ref vector); + return vector; + } + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 32, 32, 32, 32), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + public static RgbaVectorP FromScaledVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + public static RgbaVectorP FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + public static RgbaVectorP FromAssociatedScaledVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + public static RgbaVectorP FromAssociatedVector4(Vector4 source) => new(source.X, source.Y, source.Z, source.W); + + /// + public static RgbaVectorP FromUnassociatedScaledVector4(Vector4 source) => FromUnassociatedVector4(source); + + /// + public static RgbaVectorP FromUnassociatedVector4(Vector4 source) + { + Numerics.Premultiply(ref source); + return FromAssociatedVector4(source); + } + + /// + public static RgbaVectorP FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaVectorP FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is RgbaVectorP other && this.Equals(other); + + /// + public readonly bool Equals(RgbaVectorP other) + => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B) && this.A.Equals(other.A); + + /// + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + + /// + public override readonly string ToString() + => FormattableString.Invariant($"RgbaVectorP({this.R:#0.##}, {this.G:#0.##}, {this.B:#0.##}, {this.A:#0.##})"); +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs index d6ab2ac1a3..d402d4a0b1 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs @@ -232,7 +232,7 @@ public override readonly string ToString() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint Pack(Vector2 vector) { - vector = Vector2.Clamp(vector, Min, Max); + vector = Numerics.Clamp(vector, Min, Max); uint word2 = (uint)Convert.ToInt32(Math.Round(vector.X)) & 0xFFFF; uint word1 = ((uint)Convert.ToInt32(Math.Round(vector.Y)) & 0xFFFF) << 0x10; diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs index 63ece41881..6f3b94c43b 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.PixelFormats; @@ -278,6 +279,94 @@ public void ToVector4( Span destinationVectors) => this.ToVector4(configuration, source, destinationVectors, PixelConversionModifiers.None); + /// + /// Converts planar floating-point components to a contiguous row of pixels. + /// An empty fourth plane supplies a value of 1 for each pixel. + /// + /// The configuration. + /// The first component plane. + /// The second component plane. + /// The third component plane. + /// The fourth component plane, or an empty span to use 1 for every value. + /// The destination pixels. + /// The representation of the channel values. + internal void PackFromFloatPlanes( + Configuration configuration, + ReadOnlySpan component0, + ReadOnlySpan component1, + ReadOnlySpan component2, + ReadOnlySpan component3, + Span destination, + PixelConversionModifiers modifiers) + { + Guard.NotNull(configuration, nameof(configuration)); + 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)); + + const int BlockSize = 64; + Span vectors = stackalloc Vector4[BlockSize]; + + // The transposer and pixel converter share one bounded scratch block. Each pixel is + // converted once, and the final short block follows the same numeric-domain path. + for (int offset = 0; offset < component0.Length; offset += BlockSize) + { + int count = Math.Min(BlockSize, component0.Length - offset); + Span block = vectors[..count]; + SimdUtils.InterleaveFloatPlanes( + component0.Slice(offset, count), + component1.Slice(offset, count), + component2.Slice(offset, count), + component3.IsEmpty ? ReadOnlySpan.Empty : component3.Slice(offset, count), + block); + + this.FromVector4Destructive(configuration, block, destination.Slice(offset, count), modifiers); + } + } + + /// + /// Converts a contiguous row of pixels to planar floating-point components. + /// + /// The configuration. + /// The source pixels. + /// The first component plane. + /// The second component plane. + /// The third component plane. + /// The fourth component plane. + /// The requested representation of the channel values. + internal void UnpackToFloatPlanes( + Configuration configuration, + ReadOnlySpan source, + Span component0, + Span component1, + Span component2, + Span component3, + PixelConversionModifiers modifiers) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, component0, nameof(component0)); + Guard.DestinationShouldNotBeTooShort(source, component1, nameof(component1)); + Guard.DestinationShouldNotBeTooShort(source, component2, nameof(component2)); + Guard.DestinationShouldNotBeTooShort(source, component3, nameof(component3)); + + const int BlockSize = 64; + Span vectors = stackalloc Vector4[BlockSize]; + + for (int offset = 0; offset < source.Length; offset += BlockSize) + { + int count = Math.Min(BlockSize, source.Length - offset); + Span block = vectors[..count]; + this.ToVector4(configuration, source.Slice(offset, count), block, modifiers); + SimdUtils.DeinterleaveFloatPlanes( + block, + component0.Slice(offset, count), + component1.Slice(offset, count), + component2.Slice(offset, count), + component3.Slice(offset, count)); + } + } + /// /// Bulk operation that converts pixels from format to /// destination pixels. @@ -292,19 +381,42 @@ public virtual void From( Span destination) where TSourcePixel : unmanaged, IPixel { - const int sliceLength = 1024; + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + if (source.IsEmpty) + { + return; + } + + int sliceLength = Math.Min(source.Length, 1024); int numberOfSlices = source.Length / sliceLength; using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength); - Span vectorSpan = tempVectors.GetSpan(); + Span vectorSpan = tempVectors.GetSpan()[..sliceLength]; + PixelConversionModifiers sourceModifiers = PixelConversionModifiers.Scale; + PixelConversionModifiers destinationModifiers = PixelConversionModifiers.Scale; + bool sourceAssociated = TSourcePixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated; + bool destinationAssociated = TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated; + + // A straight destination needs an associated source unassociated before storage. + // An associated destination must round its own alpha before associating straight input. + if (sourceAssociated && !destinationAssociated) + { + sourceModifiers |= PixelConversionModifiers.UnPremultiply; + } + else if (!sourceAssociated && destinationAssociated) + { + destinationModifiers |= PixelConversionModifiers.UnPremultiply; + } for (int i = 0; i < numberOfSlices; i++) { int start = i * sliceLength; ReadOnlySpan s = source.Slice(start, sliceLength); Span d = destination.Slice(start, sliceLength); - PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); - this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, sourceModifiers); + this.FromVector4Destructive(configuration, vectorSpan, d, destinationModifiers); } int endOfCompleteSlices = numberOfSlices * sliceLength; @@ -314,8 +426,8 @@ public virtual void From( ReadOnlySpan s = source[endOfCompleteSlices..]; Span d = destination[endOfCompleteSlices..]; vectorSpan = vectorSpan[..remainder]; - PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); - this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, sourceModifiers); + this.FromVector4Destructive(configuration, vectorSpan, d, destinationModifiers); } } diff --git a/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs b/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs index 22d1ba4ace..06ae3b6faa 100644 --- a/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs @@ -327,7 +327,7 @@ private static Vector512 ConvertToInt32(Vector512 source, bool norma } source *= Vector512.Create(ShortMaximum); - source = Vector512.Min(Vector512.Max(source, Vector512.Create(-ShortMaximum)), Vector512.Create(ShortMaximum)); + source = Numerics.Clamp(source, Vector512.Create(-ShortMaximum), Vector512.Create(ShortMaximum)); } else { @@ -337,7 +337,7 @@ private static Vector512 ConvertToInt32(Vector512 source, bool norma source += Vector512.Create(SignedIntegerMinimum); } - source = Vector512.Min(Vector512.Max(source, Vector512.Create((float)short.MinValue)), Vector512.Create((float)short.MaxValue)); + source = Numerics.Clamp(source, Vector512.Create((float)short.MinValue), Vector512.Create((float)short.MaxValue)); } return Vector512_.ConvertToInt32RoundToEven(source); @@ -362,7 +362,7 @@ private static Vector256 ConvertToInt32(Vector256 source, bool norma } source *= Vector256.Create(ShortMaximum); - source = Vector256.Min(Vector256.Max(source, Vector256.Create(-ShortMaximum)), Vector256.Create(ShortMaximum)); + source = Numerics.Clamp(source, Vector256.Create(-ShortMaximum), Vector256.Create(ShortMaximum)); } else { @@ -372,7 +372,7 @@ private static Vector256 ConvertToInt32(Vector256 source, bool norma source += Vector256.Create(SignedIntegerMinimum); } - source = Vector256.Min(Vector256.Max(source, Vector256.Create((float)short.MinValue)), Vector256.Create((float)short.MaxValue)); + source = Numerics.Clamp(source, Vector256.Create((float)short.MinValue), Vector256.Create((float)short.MaxValue)); } return Vector256_.ConvertToInt32RoundToEven(source); @@ -397,7 +397,7 @@ private static Vector128 ConvertToInt32(Vector128 source, bool norma } source *= Vector128.Create(ShortMaximum); - source = Vector128.Min(Vector128.Max(source, Vector128.Create(-ShortMaximum)), Vector128.Create(ShortMaximum)); + source = Numerics.Clamp(source, Vector128.Create(-ShortMaximum), Vector128.Create(ShortMaximum)); } else { @@ -407,7 +407,7 @@ private static Vector128 ConvertToInt32(Vector128 source, bool norma source += Vector128.Create(SignedIntegerMinimum); } - source = Vector128.Min(Vector128.Max(source, Vector128.Create((float)short.MinValue)), Vector128.Create((float)short.MaxValue)); + source = Numerics.Clamp(source, Vector128.Create((float)short.MinValue), Vector128.Create((float)short.MaxValue)); } return Vector128_.ConvertToInt32RoundToEven(source); diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs index b47779df8d..a88bbf5267 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs @@ -238,7 +238,7 @@ private static Vector512 AssociateToByte(Vector512 source) Vector512 zero = Vector512.Zero; Vector512 one = Vector512.One; Vector512 byteMax = Vector512.Create((float)byte.MaxValue); - source = Vector512.Min(Vector512.Max(source, zero), one); + source = Numerics.Clamp(source, zero, one); Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512 storedAlpha = Vector512.Floor((alpha * byteMax) + Vector512.Create(.5F)); Vector512 result = source * storedAlpha; @@ -259,7 +259,7 @@ private static Vector256 AssociateToByte(Vector256 source) Vector256 zero = Vector256.Zero; Vector256 one = Vector256.One; Vector256 byteMax = Vector256.Create((float)byte.MaxValue); - source = Vector256.Min(Vector256.Max(source, zero), one); + source = Numerics.Clamp(source, zero, one); Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256 storedAlpha = Vector256.Floor((alpha * byteMax) + Vector256.Create(.5F)); Vector256 result = source * storedAlpha; @@ -280,7 +280,7 @@ private static Vector128 AssociateToByte(Vector128 source) Vector128 zero = Vector128.Zero; Vector128 one = Vector128.One; Vector128 byteMax = Vector128.Create((float)byte.MaxValue); - source = Vector128.Min(Vector128.Max(source, zero), one); + source = Numerics.Clamp(source, zero, one); Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128 storedAlpha = Vector128.Floor((alpha * byteMax) + Vector128.Create(.5F)); Vector128 result = source * storedAlpha; @@ -459,16 +459,16 @@ private static Vector512 ReassociateToByte(Vector512 source) { Vector512 zero = Vector512.Zero; Vector512 byteMax = Vector512.Create((float)byte.MaxValue); - Vector512 alpha = Vector512.Max(Vector512_.ShuffleNative(source, 0b_11_11_11_11), zero); + Vector512 alpha = Numerics.Clamp(Vector512_.ShuffleNative(source, 0b_11_11_11_11), zero, Vector512.Create(float.PositiveInfinity)); Vector512 byteAlpha = alpha * byteMax; - Vector512 storedAlpha = Vector512.Floor(Vector512.Min(Vector512.Max(byteAlpha + Vector512.Create(.5F), zero), byteMax)); + Vector512 storedAlpha = Vector512.Floor(Numerics.Clamp(byteAlpha + Vector512.Create(.5F), zero, byteMax)); Vector512 result = (source / alpha) * storedAlpha; // Exact byte alpha values need no reassociation. Multiplying by 255 directly preserves RGB values that already lie on byte midpoints. result = Vector512.ConditionalSelect(Vector512.Equals(byteAlpha, storedAlpha), source * byteMax, result); 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); + result = Numerics.Clamp(result, zero, storedAlpha); return Vector512.ConditionalSelect(Vector512.Equals(alpha, zero), zero, result); } @@ -482,16 +482,16 @@ private static Vector256 ReassociateToByte(Vector256 source) { Vector256 zero = Vector256.Zero; Vector256 byteMax = Vector256.Create((float)byte.MaxValue); - Vector256 alpha = Vector256.Max(Vector256_.ShuffleNative(source, 0b_11_11_11_11), zero); + Vector256 alpha = Numerics.Clamp(Vector256_.ShuffleNative(source, 0b_11_11_11_11), zero, Vector256.Create(float.PositiveInfinity)); Vector256 byteAlpha = alpha * byteMax; - Vector256 storedAlpha = Vector256.Floor(Vector256.Min(Vector256.Max(byteAlpha + Vector256.Create(.5F), zero), byteMax)); + Vector256 storedAlpha = Vector256.Floor(Numerics.Clamp(byteAlpha + Vector256.Create(.5F), zero, byteMax)); Vector256 result = (source / alpha) * storedAlpha; // Exact byte alpha values need no reassociation. Multiplying by 255 directly preserves RGB values that already lie on byte midpoints. result = Vector256.ConditionalSelect(Vector256.Equals(byteAlpha, storedAlpha), source * byteMax, result); Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); result = Vector256.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + result = Numerics.Clamp(result, zero, storedAlpha); return Vector256.ConditionalSelect(Vector256.Equals(alpha, zero), zero, result); } @@ -505,16 +505,16 @@ private static Vector128 ReassociateToByte(Vector128 source) { Vector128 zero = Vector128.Zero; Vector128 byteMax = Vector128.Create((float)byte.MaxValue); - Vector128 alpha = Vector128.Max(Vector128_.ShuffleNative(source, 0b_11_11_11_11), zero); + Vector128 alpha = Numerics.Clamp(Vector128_.ShuffleNative(source, 0b_11_11_11_11), zero, Vector128.Create(float.PositiveInfinity)); Vector128 byteAlpha = alpha * byteMax; - Vector128 storedAlpha = Vector128.Floor(Vector128.Min(Vector128.Max(byteAlpha + Vector128.Create(.5F), zero), byteMax)); + Vector128 storedAlpha = Vector128.Floor(Numerics.Clamp(byteAlpha + Vector128.Create(.5F), zero, byteMax)); Vector128 result = (source / alpha) * storedAlpha; // Exact byte alpha values need no reassociation. Multiplying by 255 directly preserves RGB values that already lie on byte midpoints. result = Vector128.ConditionalSelect(Vector128.Equals(byteAlpha, storedAlpha), source * byteMax, result); Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); result = Vector128.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + result = Numerics.Clamp(result, zero, storedAlpha); return Vector128.ConditionalSelect(Vector128.Equals(alpha, zero), zero, result); } diff --git a/src/ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs b/src/ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs index 815b059cf5..c2674e0fc4 100644 --- a/src/ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs @@ -16,7 +16,7 @@ public static class BinaryThresholdExtensions /// Luminance as the color component to be compared to threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The . public static IImageProcessingContext BinaryThreshold(this IImageProcessingContext source, float threshold) => source.ApplyProcessor(new BinaryThresholdProcessor(threshold, BinaryThresholdMode.Luminance)); @@ -25,7 +25,7 @@ public static IImageProcessingContext BinaryThreshold(this IImageProcessingConte /// Applies binarization to the image splitting the pixels at the given threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// Selects the value to be compared to threshold. /// The . public static IImageProcessingContext BinaryThreshold( @@ -39,7 +39,7 @@ public static IImageProcessingContext BinaryThreshold( /// Luminance as the color component to be compared to threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// /// The structure that specifies the portion of the image object to alter. /// @@ -54,7 +54,7 @@ public static IImageProcessingContext BinaryThreshold( /// Applies binarization to the image splitting the pixels at the given threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// Selects the value to be compared to threshold. /// /// The structure that specifies the portion of the image object to alter. @@ -72,7 +72,7 @@ public static IImageProcessingContext BinaryThreshold( /// Luminance as the color component to be compared to threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The color to use for pixels that are above the threshold. /// The color to use for pixels that are below the threshold /// The . @@ -87,7 +87,7 @@ public static IImageProcessingContext BinaryThreshold( /// Applies binarization to the image splitting the pixels at the given threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The color to use for pixels that are above the threshold. /// The color to use for pixels that are below the threshold /// Selects the value to be compared to threshold. @@ -105,7 +105,7 @@ public static IImageProcessingContext BinaryThreshold( /// Luminance as the color component to be compared to threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The color to use for pixels that are above the threshold. /// The color to use for pixels that are below the threshold /// @@ -124,7 +124,7 @@ public static IImageProcessingContext BinaryThreshold( /// Applies binarization to the image splitting the pixels at the given threshold. /// /// The current image processing context. - /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The color to use for pixels that are above the threshold. /// The color to use for pixels that are below the threshold /// Selects the value to be compared to threshold. diff --git a/src/ImageSharp/Processing/Extensions/Normalization/AutoLevelExtensions.cs b/src/ImageSharp/Processing/Extensions/Normalization/AutoLevelExtensions.cs new file mode 100644 index 0000000000..15709a00f6 --- /dev/null +++ b/src/ImageSharp/Processing/Extensions/Normalization/AutoLevelExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Normalization; + +namespace SixLabors.ImageSharp.Processing; + +/// +/// Defines extensions for automatic level adjustment. +/// +public static class AutoLevelExtensions +{ + /// + /// Automatically adjusts the image's brightness levels. + /// + /// The current image processing context. + /// The . + public static IImageProcessingContext AutoLevel(this IImageProcessingContext source) + { + HistogramEqualizationOptions options = new() { Method = HistogramEqualizationMethod.AutoLevel }; + return source.HistogramEqualization(options); + } +} diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs index d6c3881174..020263a83c 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs @@ -13,7 +13,7 @@ public class BinaryThresholdProcessor : IImageProcessor /// /// Initializes a new instance of the class. /// - /// The threshold to split the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The color component to be compared to threshold. public BinaryThresholdProcessor(float threshold, BinaryThresholdMode mode) : this(threshold, Color.White, Color.Black, mode) @@ -24,7 +24,7 @@ public BinaryThresholdProcessor(float threshold, BinaryThresholdMode mode) /// Initializes a new instance of the class with /// Luminance as color component to be compared to threshold. /// - /// The threshold to split the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. public BinaryThresholdProcessor(float threshold) : this(threshold, Color.White, Color.Black, BinaryThresholdMode.Luminance) { @@ -33,7 +33,7 @@ public BinaryThresholdProcessor(float threshold) /// /// Initializes a new instance of the class. /// - /// The threshold to split the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The color to use for pixels that are above the threshold. /// The color to use for pixels that are below the threshold. /// The color component to be compared to threshold. @@ -50,7 +50,7 @@ public BinaryThresholdProcessor(float threshold, Color upperColor, Color lowerCo /// Initializes a new instance of the class with /// Luminance as color component to be compared to threshold. /// - /// The threshold to split the image. Must be between 0 and 1. + /// The threshold as a fraction of the selected region's observed metric range. Must be between 0 and 1. /// The color to use for pixels that are above the threshold. /// The color to use for pixels that are below the threshold. public BinaryThresholdProcessor(float threshold, Color upperColor, Color lowerColor) @@ -59,7 +59,7 @@ public BinaryThresholdProcessor(float threshold, Color upperColor, Color lowerCo } /// - /// Gets the threshold value. + /// Gets the threshold as a fraction of the selected region's observed metric range. /// public float Threshold { get; } diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs index ad87f36c1c..b33d2eaf4f 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -31,49 +33,142 @@ public BinaryThresholdProcessor(Configuration configuration, BinaryThresholdProc /// protected override void OnFrameApply(ImageFrame source) { - byte threshold = (byte)MathF.Round(this.definition.Threshold * 255F); - TPixel upper = this.definition.UpperColor.ToPixel(); - TPixel lower = this.definition.LowerColor.ToPixel(); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); + if (interest.IsEmpty) + { + return; + } - Rectangle sourceRectangle = this.SourceRectangle; Configuration configuration = this.Configuration; + PixelOperations operations = PixelOperations.Instance; + PixelConversionModifiers modifiers = PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply; + using IMemoryOwner rowBuffer = configuration.MemoryAllocator.Allocate(interest.Width); + Span vectors = rowBuffer.GetSpan()[..interest.Width]; + + float minimum = float.PositiveInfinity; + float maximum = float.NegativeInfinity; + for (int y = interest.Top; y < interest.Bottom; y++) + { + Span row = source.PixelBuffer.DangerousGetRowSpan(y).Slice(interest.X, interest.Width); + operations.ToVector4(configuration, row, vectors, modifiers); + + for (int x = 0; x < vectors.Length; x++) + { + float metric = GetMetric(vectors[x], this.definition.Mode); + if (float.IsFinite(metric)) + { + minimum = MathF.Min(minimum, metric); + maximum = MathF.Max(maximum, metric); + } + } + } + + // Compute in double precision so a finite range spanning large negative and + // positive HDR values does not overflow before its percentage is applied. + bool hasFiniteValue = minimum <= maximum; + float threshold = hasFiniteValue + ? (float)(minimum + (this.definition.Threshold * ((double)maximum - minimum))) + : 0F; - Rectangle interest = Rectangle.Intersect(sourceRectangle, source.Bounds); RowOperation operation = new( interest.X, source.PixelBuffer, - upper, - lower, + this.definition.UpperColor.ToPixel(), + this.definition.LowerColor.ToPixel(), threshold, + hasFiniteValue, this.definition.Mode, configuration); - ParallelRowIterator.IterateRows( - configuration, - interest, - in operation); + ParallelRowIterator.IterateRows(configuration, interest, in operation); } /// - /// A implementing the clone logic for . + /// Gets the selected threshold metric from a pixel vector. /// - private readonly struct RowOperation : IRowOperation + /// The source pixel components. + /// The selected threshold metric. + /// The metric value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetMetric(Vector4 vector, BinaryThresholdMode mode) + => mode switch + { + BinaryThresholdMode.Saturation => GetSaturation(vector), + BinaryThresholdMode.MaxChroma => GetMaxChroma(vector), + _ => ColorNumerics.GetBT709Luminance(vector) + }; + + /// + /// Gets HSL saturation from the original color components. + /// + /// The source pixel components. + /// The saturation value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetSaturation(Vector4 vector) + { + // Retain the existing HSL saturation metric while reading original float values. + float max = MathF.Max(vector.X, MathF.Max(vector.Y, vector.Z)); + float min = MathF.Min(vector.X, MathF.Min(vector.Y, vector.Z)); + float chroma = max - min; + + if (MathF.Abs(chroma) < Constants.Epsilon) + { + return 0F; + } + + float lightness = (max + min) * 0.5F; + return lightness <= .5F + ? chroma / (max + min) + : chroma / (2F - max - min); + } + + /// + /// Gets the larger magnitude of the YCbCr chroma components. + /// + /// The source pixel components. + /// The chroma magnitude. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetMaxChroma(Vector4 vector) + { + // Remove the neutral chroma offsets from the existing YCbCr formula. + float cb = (-0.168736F * vector.X) - (0.331264F * vector.Y) + (0.5F * vector.Z); + float cr = (0.5F * vector.X) - (0.418688F * vector.Y) - (0.081312F * vector.Z); + return MathF.Max(MathF.Abs(cb), MathF.Abs(cr)); + } + + /// + /// Applies the measured threshold to one pixel row. + /// + private readonly struct RowOperation : IRowOperation { private readonly Buffer2D source; private readonly TPixel upper; private readonly TPixel lower; - private readonly byte threshold; + private readonly float threshold; + private readonly bool hasFiniteValue; private readonly BinaryThresholdMode mode; private readonly int startX; private readonly Configuration configuration; + /// + /// Initializes a new instance of the struct with the measured threshold. + /// + /// The first pixel of the selected region. + /// The source pixel buffer. + /// The color for values at or above the threshold. + /// The color for values below the threshold. + /// The threshold in the selected metric's range. + /// Whether the selected region contained a finite metric. + /// The selected threshold metric. + /// The shared configuration. [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( int startX, Buffer2D source, TPixel upper, TPixel lower, - byte threshold, + float threshold, + bool hasFiniteValue, BinaryThresholdMode mode, Configuration configuration) { @@ -82,110 +177,33 @@ public RowOperation( this.upper = upper; this.lower = lower; this.threshold = threshold; + this.hasFiniteValue = hasFiniteValue; this.mode = mode; this.configuration = configuration; } /// [MethodImpl(InliningOptions.ShortMethod)] - public int GetRequiredBufferLength(Rectangle bounds) - => bounds.Width; + public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invoke(int y, Span span) + public void Invoke(int y, Span span) { - TPixel upper = this.upper; - TPixel lower = this.lower; - - Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); - PixelOperations.Instance.ToRgb24(this.configuration, rowSpan, span); - - switch (this.mode) - { - case BinaryThresholdMode.Luminance: - { - byte threshold = this.threshold; - for (int x = 0; x < rowSpan.Length; x++) - { - Rgb24 rgb = span[x]; - byte luminance = ColorNumerics.Get8BitBT709Luminance(rgb.R, rgb.G, rgb.B); - ref TPixel color = ref rowSpan[x]; - color = luminance >= threshold ? upper : lower; - } - - break; - } - - case BinaryThresholdMode.Saturation: - { - float threshold = this.threshold / 255F; - for (int x = 0; x < rowSpan.Length; x++) - { - float saturation = GetSaturation(span[x]); - ref TPixel color = ref rowSpan[x]; - color = saturation >= threshold ? upper : lower; - } - - break; - } - - case BinaryThresholdMode.MaxChroma: - { - float threshold = this.threshold * 0.5F; // /2 - for (int x = 0; x < rowSpan.Length; x++) - { - float chroma = GetMaxChroma(span[x]); - ref TPixel color = ref rowSpan[x]; - color = chroma >= threshold ? upper : lower; - } - - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float GetSaturation(Rgb24 rgb) - { - // Slimmed down RGB => HSL formula. See HslAndRgbConverter. - const float inv255 = 1 / 255F; - float r = rgb.R * inv255; - float g = rgb.G * inv255; - float b = rgb.B * inv255; - - float max = MathF.Max(r, MathF.Max(g, b)); - float min = MathF.Min(r, MathF.Min(g, b)); - float chroma = max - min; - - if (MathF.Abs(chroma) < Constants.Epsilon) - { - return 0F; - } - - float l = (max + min) * 0.5F; // /2 - - if (l <= .5F) + Span row = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.Instance.ToVector4( + this.configuration, + row, + span, + PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + for (int x = 0; x < row.Length; x++) { - return chroma / (max + min); + float metric = GetMetric(span[x], this.mode); + row[x] = this.hasFiniteValue && float.IsFinite(metric) && metric >= this.threshold + ? this.upper + : this.lower; } - - return chroma / (2F - max - min); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float GetMaxChroma(Rgb24 rgb) - { - // Slimmed down RGB => YCbCr formula. See YCbCrAndRgbConverter. - float r = rgb.R; - float g = rgb.G; - float b = rgb.B; - const float achromatic = 127.5F; - - float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b)); - float cr = 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b)); - - return MathF.Max(MathF.Abs(cb - achromatic), MathF.Abs(cr - achromatic)); } } } diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs index ddb6d15457..02801b63c4 100644 --- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs @@ -157,7 +157,9 @@ public void Invoke(in RowInterval rows) float sourceBlue = vector.Z; float sourceGreen = vector.Y; - int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1)); + // The computed intensity can be nonfinite or outside [0, 1], so bound it before indexing the bins. + float intensity = Numerics.Clamp((sourceBlue + sourceGreen + sourceRed) / 3F, 0F, 1F); + int currentIntensity = (int)MathF.Round(intensity * (this.levels - 1)); intensityBinsSpan[currentIntensity]++; redBinSpan[currentIntensity] += sourceRed; diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs index b18a52bec2..f4b5c3c845 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs @@ -51,6 +51,20 @@ public AdaptiveHistogramEqualizationProcessor( /// private int Tiles { get; } + /// + /// Gets a histogram index from BT.709 luminance, saturating out-of-range values and mapping NaN to zero. + /// + /// The source color. + /// The number of histogram bins. + /// The bounded histogram index. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetLuminanceIndex(Vector4 vector, int levels) + { + // Unbounded luminance can address outside the histogram; NaN maps to its first bin. + float luminance = Numerics.Clamp(ColorNumerics.GetBT709Luminance(vector), 0F, 1F); + return (int)MathF.Round(luminance * (levels - 1)); + } + /// protected override void OnFrameApply(ImageFrame source) { @@ -145,7 +159,7 @@ private static void ProcessCornerTile( { ref TPixel pixel = ref rowSpan[dx]; Vector4 vector = pixel.ToUnassociatedScaledVector4(); - int luminance = ColorNumerics.GetBT709Luminance(vector, luminanceLevels); + int luminance = GetLuminanceIndex(vector, luminanceLevels); float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, luminance); vector.X = vector.Y = vector.Z = luminanceEqualized; pixel = TPixel.FromUnassociatedScaledVector4(vector); @@ -292,7 +306,7 @@ private static float InterpolateBetweenFourTiles( int tileHeight, int luminanceLevels) { - int luminance = ColorNumerics.GetBT709Luminance(sourceVector, luminanceLevels); + int luminance = GetLuminanceIndex(sourceVector, luminanceLevels); float tx = tileX / (float)(tileWidth - 1); float ty = tileY / (float)(tileHeight - 1); @@ -336,7 +350,7 @@ private static float InterpolateBetweenTwoTiles( int tileWidth, int luminanceLevels) { - int luminance = ColorNumerics.GetBT709Luminance(sourceVector, luminanceLevels); + int luminance = GetLuminanceIndex(sourceVector, luminanceLevels); float tx = tilePos / (float)(tileWidth - 1); float cdfLuminance1 = cdfData.RemapGreyValue(tileX1, tileY1, luminance); diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs index 624820d2f2..9f1487721f 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs @@ -49,6 +49,20 @@ public AdaptiveHistogramEqualizationSlidingWindowProcessor( /// private int Tiles { get; } + /// + /// Gets a histogram index from BT.709 luminance, saturating out-of-range values and mapping NaN to zero. + /// + /// The source color. + /// The number of histogram bins. + /// The bounded histogram index. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetLuminanceIndex(Vector4 vector, int levels) + { + // Unbounded luminance can address outside the histogram; NaN maps to its first bin. + float luminance = Numerics.Clamp(ColorNumerics.GetBT709Luminance(vector), 0F, 1F); + return (int)MathF.Round(luminance * (levels - 1)); + } + /// protected override void OnFrameApply(ImageFrame source) { @@ -256,7 +270,7 @@ private static void AddPixelsToHistogram(ref Vector4 greyValuesBase, ref int his { for (nuint idx = 0; idx < (uint)length; idx++) { - int luminance = ColorNumerics.GetBT709Luminance(Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = GetLuminanceIndex(Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, (uint)luminance)++; } } @@ -273,7 +287,7 @@ private static void RemovePixelsFromHistogram(ref Vector4 greyValuesBase, ref in { for (nuint idx = 0; idx < (uint)length; idx++) { - int luminance = ColorNumerics.GetBT709Luminance(Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = GetLuminanceIndex(Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, (uint)luminance)--; } } @@ -382,7 +396,7 @@ public void Invoke(int x) // Map the current pixel to the new equalized value. Vector4 vector = this.source[x, y].ToUnassociatedScaledVector4(); - int luminance = ColorNumerics.GetBT709Luminance(vector, this.processor.LuminanceLevels); + int luminance = GetLuminanceIndex(vector, this.processor.LuminanceLevels); float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / numberOfPixelsMinusCdfMin; vector.X = vector.Y = vector.Z = luminanceEqualized; this.targetPixels[x, y] = TPixel.FromUnassociatedScaledVector4(vector); diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs index 9acca6d409..b5ba76a567 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -148,8 +148,19 @@ public void Invoke(int y, Span span) for (int x = 0; x < this.bounds.Width; x++) { Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); - int luminance = ColorNumerics.GetBT709Luminance(vector, levels); + + // Keep the CDF offset within the histogram even for HDR or nonfinite samples. + float boundedLuminance = Numerics.Clamp(ColorNumerics.GetBT709Luminance(vector), 0F, 1F); + int luminance = (int)MathF.Round(boundedLuminance * (levels - 1)); float scaledLuminance = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; + + // The first histogram bin maps to black; avoid dividing by its zero index. + if (luminance == 0) + { + Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(0F, 0F, 0F, vector.W); + continue; + } + float scalingFactor = scaledLuminance * levels / luminance; Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W); } @@ -212,13 +223,14 @@ public void Invoke(int y, Span span) float alpha = vector.W; // Alpha is not a histogram component and must not be scaled into the CDF index range. - vector *= levelsMinusOne; + // The CDF lookup uses bounded indices; the source vector can still contain HDR values. + Vector4 indexValues = Numerics.Clamp(vector, Vector4.Zero, Vector4.One) * levelsMinusOne; - uint originalX = (uint)MathF.Round(vector.X); + uint originalX = (uint)MathF.Round(indexValues.X); float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin; - uint originalY = (uint)MathF.Round(vector.Y); + uint originalY = (uint)MathF.Round(indexValues.Y); float scaledY = Unsafe.Add(ref cdfBase, originalY) / noOfPixelsMinusCdfMin; - uint originalZ = (uint)MathF.Round(vector.Z); + uint originalZ = (uint)MathF.Round(indexValues.Z); float scaledZ = Unsafe.Add(ref cdfBase, originalZ) / noOfPixelsMinusCdfMin; Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(scaledX, scaledY, scaledZ, alpha); } diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index 3c07d05c6b..a8226f2993 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -131,7 +131,10 @@ public void Invoke(int y, Span span) for (int x = 0; x < this.bounds.Width; x++) { Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); - int luminance = ColorNumerics.GetBT709Luminance(vector, levels); + + // Keep the CDF offset within the histogram even for HDR or nonfinite samples. + float boundedLuminance = Numerics.Clamp(ColorNumerics.GetBT709Luminance(vector), 0F, 1F); + int luminance = (int)MathF.Round(boundedLuminance * (levels - 1)); float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W); } diff --git a/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs index c7e40b420f..40447ba742 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs @@ -57,8 +57,22 @@ public void Invoke(int y, Span span) for (int x = 0; x < this.bounds.Width; x++) { Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); - int luminance = ColorNumerics.GetBT709Luminance(vector, levels); + int luminance = GetLuminanceIndex(vector, levels); Interlocked.Increment(ref Unsafe.Add(ref histogramBase, (uint)luminance)); } } + + /// + /// Gets a histogram index from BT.709 luminance, saturating out-of-range values and mapping NaN to zero. + /// + /// The source color. + /// The number of histogram bins. + /// The bounded histogram index. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetLuminanceIndex(Vector4 vector, int levels) + { + // A floating-point pixel can exceed the histogram range or produce NaN. + float luminance = Numerics.Clamp(ColorNumerics.GetBT709Luminance(vector), 0F, 1F); + return (int)MathF.Round(luminance * (levels - 1)); + } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs index 77b741e88d..ff107c77a2 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs @@ -133,7 +133,7 @@ public void ClipHistogram(Span histogram, int clipLimit) } /// - /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. + /// Gets the saturated BT.709 histogram index for a pixel. /// /// The pixel to get the luminance from /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) @@ -143,6 +143,9 @@ public static int GetLuminance(TPixel sourcePixel, int luminanceLevels) // TODO: We need a bulk per span equivalent. // Histogram bins describe logical color, so storage alpha association must not scale luminance. Vector4 vector = sourcePixel.ToUnassociatedScaledVector4(); - return ColorNumerics.GetBT709Luminance(vector, luminanceLevels); + + // Unbounded luminance can address outside the histogram; NaN maps to its first bin. + float luminance = Numerics.Clamp(ColorNumerics.GetBT709Luminance(vector), 0F, 1F); + return (int)MathF.Round(luminance * (luminanceLevels - 1)); } } diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.FloatPlanes.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.FloatPlanes.cs new file mode 100644 index 0000000000..628fe179b7 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.FloatPlanes.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Common; + +public partial class SimdUtilsTests +{ + [Theory] + [MemberData(nameof(ArbitraryArraySizes))] + public void FloatPlanes_PreserveComponentBitsAcrossSimdAndTail(int count) + { + static void RunTest(string serialized) => AssertFloatPlanes(FeatureTestRunner.Deserialize(serialized)); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + count, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks all four component planes and an omitted fourth plane against exact sample bits. + /// + /// The number of pixels to transpose. + private static void AssertFloatPlanes(int count) + { + float[] samples = + [ + BitConverter.Int32BitsToSingle(unchecked((int)0x7FC01234)), + float.PositiveInfinity, + float.NegativeInfinity, + BitConverter.Int32BitsToSingle(unchecked((int)0x80000000)), + -2.5F, + 0.1F, + 1F, + 65504F + ]; + + float[][] components = [new float[count], new float[count], new float[count], new float[count]]; + for (int i = 0; i < count; i++) + { + for (int component = 0; component < 4; component++) + { + components[component][i] = samples[(i + component) % samples.Length]; + } + } + + Vector4[] vectors = new Vector4[count]; + SimdUtils.InterleaveFloatPlanes(components[0], components[1], components[2], components[3], vectors); + + float[][] output = [new float[count], new float[count], new float[count], new float[count]]; + SimdUtils.DeinterleaveFloatPlanes(vectors, output[0], output[1], output[2], output[3]); + + for (int component = 0; component < 4; component++) + { + for (int i = 0; i < count; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(components[component][i]), BitConverter.SingleToInt32Bits(output[component][i])); + } + } + + // An absent fourth plane must supply opaque values in both full registers and tails. + SimdUtils.InterleaveFloatPlanes(components[0], components[1], components[2], ReadOnlySpan.Empty, vectors); + for (int i = 0; i < count; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(components[0][i]), BitConverter.SingleToInt32Bits(vectors[i].X)); + Assert.Equal(BitConverter.SingleToInt32Bits(components[1][i]), BitConverter.SingleToInt32Bits(vectors[i].Y)); + Assert.Equal(BitConverter.SingleToInt32Bits(components[2][i]), BitConverter.SingleToInt32Bits(vectors[i].Z)); + Assert.Equal(1F, vectors[i].W); + } + } +} diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs index b4fbf5fd80..d211f46298 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs @@ -368,6 +368,14 @@ private static void TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( { seed = seed > 0 ? seed : count; float[] source = new Random(seed).GenerateRandomFloatArray(count, -0.2f, 1.2f); + ReadOnlySpan specialValues = [float.NaN, float.NegativeInfinity, float.PositiveInfinity, 2F, -2F, -0F]; + + // Place exceptional values in successive SIMD blocks while retaining random values in the other lanes. + for (int i = 0; i < source.Length; i += 16) + { + source[i] = specialValues[(i / 16) % specialValues.Length]; + } + byte[] expected = [.. source.Select(NormalizedFloatToByte)]; byte[] actual = new byte[count]; @@ -376,7 +384,7 @@ private static void TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( Assert.Equal(expected, actual); } - private static byte NormalizedFloatToByte(float f) => (byte)Math.Min(255f, Math.Max(0f, (f * 255f) + 0.5f)); + private static byte NormalizedFloatToByte(float f) => float.IsNaN(f) ? (byte)0 : (byte)Math.Min(255f, Math.Max(0f, (f * 255f) + 0.5f)); private static void AssertEvenRoundIsCorrect(Vector r, Vector v) { diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 1ee0cc582b..df99f1cf53 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -15,6 +17,45 @@ public class ExrDecoderTests { private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Exr; + [Theory] + [InlineData(TestImages.Exr.UncompressedFloatRgb, typeof(Image))] + [InlineData(TestImages.Exr.UncompressedRgba, typeof(Image))] + [InlineData(TestImages.Exr.Rgb, typeof(Image))] + public void DefaultLoad_UsesFloatingStorageForExrSamples(string imagePath, Type expectedImageType) + { + TestFile file = TestFile.Create(imagePath); + using MemoryStream stream = new(file.Bytes, false); + using Image image = Image.Load(stream); + + Assert.Equal(expectedImageType, image.GetType()); + } + + [Fact] + public void ExrDecoder_PreservesHdrSamplesFromOpenExrFile() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertHdrSamplesFromOpenExrFile, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks decoded samples from the OpenEXR fixture at positions inside the vector body and scalar tail. + /// + private static void AssertHdrSamplesFromOpenExrFile() + { + TestFile file = TestFile.Create(TestImages.Exr.OpenExrHdrHalf); + using MemoryStream stream = new(file.Bytes, false); + using Image image = Image.Load(stream); + Image pixels = Assert.IsType>(image); + + Assert.Equal(587, pixels.Width); + Assert.Equal(675, pixels.Height); + + // These values come from the uncompressed half samples in OpenEXR's comp_none.exr. + Assert.Equal(new Vector4(319F, 423F, 501F, 1F), pixels[272, 180].ToVector4()); + Assert.Equal(new Vector4(0.001132965087890625F, -0.002262115478515625F, -0.0014476776123046875F, 1F), pixels[73, 639].ToVector4()); + Assert.Equal(new Vector4(0.91748046875F, 1.01953125F, 1.18359375F, 1F), pixels[59, 49].ToVector4()); + Assert.Equal(new Vector4(0.037841796875F, 0.021148681640625F, 0.0201263427734375F, 1F), pixels[586, 180].ToVector4()); + } + [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Half(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs index 9e7cfc6a10..bf37009886 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs @@ -46,9 +46,8 @@ public void EncoderOptions_SetPixelType_Works(ExrPixelType? pixelType, ExrPixelT public void Encode_PixelFormatWithNonUnitNativeRange_WritesScaledValues(ExrPixelType pixelType) { // arrange - // HalfVector4 stores the scaled range [0, 1] as the native range [-65504, 65504], so its native and scaled - // vectors differ. The alpha of 0.5 makes the test sensitive to the alpha channel too: a wrong alpha value - // larger than 1 would otherwise be hidden by the clamp in the decoder. + // The source's native and scaled vectors now carry the same numeric values. A half alpha + // keeps this sensitive to association when the encoder writes EXR's associated channels. Vector4 expected = new(0.25F, 0.5F, 0.75F, 0.5F); ExrEncoder exrEncoder = new() { PixelType = pixelType }; using Image input = new(2, 2, HalfVector4.FromScaledVector4(expected)); @@ -63,6 +62,100 @@ public void Encode_PixelFormatWithNonUnitNativeRange_WritesScaledValues(ExrPixel Assert.Equal(expected, output[0, 0].ToScaledVector4(), new ApproximateFloatComparer(1e-4F)); } + [Theory] + [InlineData(ExrCompression.None)] + [InlineData(ExrCompression.Zip)] + [InlineData(ExrCompression.Zips)] + public void FloatDefaultLoad_RetainsAssociatedColorAtZeroAlpha(ExrCompression compression) + { + using Image input = new(65, 1); + RgbaVectorP pixel = new(2.500123F, -.50001F, .250001F, 0F); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = pixel; + } + + using MemoryStream stream = new(); + input.Save(stream, new ExrEncoder { PixelType = ExrPixelType.Float, Compression = compression }); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + Image output = Assert.IsType>(decoded); + + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(pixel.ToVector4(), output[x, 0].ToVector4()); + } + + using MemoryStream second = new(); + decoded.Save(second, new ExrEncoder()); + + second.Position = 0; + using Image reloaded = Image.Load(second); + Image reloadedPixels = Assert.IsType>(reloaded); + Assert.Equal(ExrPixelType.Float, reloaded.Metadata.GetExrMetadata().PixelType); + Assert.Equal(pixel.ToVector4(), reloadedPixels[0, 0].ToVector4()); + } + + [Theory] + [InlineData(ExrCompression.None)] + [InlineData(ExrCompression.Zip)] + [InlineData(ExrCompression.Zips)] + public void HalfDefaultLoad_RetainsAssociatedColorAtZeroAlpha(ExrCompression compression) + { + using Image input = new(65, 1); + RgbaHalfP pixel = new(2.5F, -.5F, .25F, 0F); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = pixel; + } + + using MemoryStream stream = new(); + input.Save(stream, new ExrEncoder { PixelType = ExrPixelType.Half, Compression = compression }); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + Image output = Assert.IsType>(decoded); + + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(pixel.ToVector4(), output[x, 0].ToVector4()); + } + } + + [Theory] + [InlineData(ExrCompression.None)] + [InlineData(ExrCompression.Zip)] + [InlineData(ExrCompression.Zips)] + public void FloatDefaultLoad_PreservesNonfiniteSamplesAndSignedZero(ExrCompression compression) + { + using Image input = new(65, 1); + RgbaVectorP pixel = new(float.NaN, float.PositiveInfinity, float.NegativeInfinity, -0F); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = pixel; + } + + using MemoryStream stream = new(); + input.Save(stream, new ExrEncoder { PixelType = ExrPixelType.Float, Compression = compression }); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + Image output = Assert.IsType>(decoded); + + for (int x = 0; x < output.Width; x++) + { + RgbaVectorP actual = output[x, 0]; + Assert.True(float.IsNaN(actual.R)); + Assert.True(float.IsPositiveInfinity(actual.G)); + Assert.True(float.IsNegativeInfinity(actual.B)); + Assert.Equal(BitConverter.SingleToInt32Bits(-0F), BitConverter.SingleToInt32Bits(actual.A)); + } + } + [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrEncoder_WithNoCompression_Works(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 6868395542..bab79d8ecf 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -27,34 +27,45 @@ public class TiffDecoderTests : TiffDecoderBaseTester public static readonly string[] MultiframeTestImages = Multiframes; /// - /// Decoded floating-point components are normalized before they enter half-vector storage. + /// Decoded floating-point components retain their values in half-vector storage. /// /// The encoded floating-point TIFF. - /// The normalized intensity. + /// The decoded intensity. [Theory] [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + - "000020000000530103000100000003000000000000000000807F0000807F0000807F0000807F0000807F0000807F0000807F0000807F", 1F)] + "000020000000530103000100000003000000000000000000807F0000807F0000807F0000807F0000807F0000807F0000807F0000807F", float.PositiveInfinity)] [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + - "000020000000530103000100000003000000000000000000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F", 0F)] + "000020000000530103000100000003000000000000000000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F", float.NaN)] [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + - "000020000000530103000100000003000000000000000000004000000040000000400000004000000040000000400000004000000040", 1F)] + "000020000000530103000100000003000000000000000000004000000040000000400000004000000040000000400000004000000040", 2F)] [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + "000020000000530103000100000003000000000000000000003F0000003F0000003F0000003F0000003F0000003F0000003F0000003F", .5F)] - public void Decode_FloatingPointSamples_NormalizesHalfVector4(string hex, float intensity) + public void Decode_FloatingPointSamples_PreservesHalfVector4Values(string hex, float intensity) { byte[] data = Convert.FromHexString(hex); using Image image = Image.Load(data); Assert.Equal(new Size(8, 1), image.Size); - Vector4 expected = new(intensity, intensity, intensity, 1F); - for (int x = 0; x < image.Width; x++) { - Assert.Equal(expected, image[x, 0].ToScaledVector4()); + Vector4 actual = image[x, 0].ToScaledVector4(); + + if (float.IsNaN(intensity)) + { + // NaN needs an explicit assertion for each decoded component. + Assert.True(float.IsNaN(actual.X)); + Assert.True(float.IsNaN(actual.Y)); + Assert.True(float.IsNaN(actual.Z)); + Assert.Equal(1F, actual.W); + } + else + { + Assert.Equal(new Vector4(intensity, intensity, intensity, 1F), actual); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index cc1b78c076..e4a8eeebbb 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -5,6 +5,8 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff; @@ -15,6 +17,318 @@ public class TiffEncoderTests : TiffEncoderBaseTester [Fact] public void TiffEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(new TiffEncoder().Quantizer); + [Theory] + [InlineData(TiffCompression.None)] + [InlineData(TiffCompression.PackBits)] + [InlineData(TiffCompression.Deflate)] + [InlineData(TiffCompression.Lzw)] + public void FloatSingleComponent_RoundTripsWithoutChangingIntensity(TiffCompression compression) + { + using Image input = new(65, 1); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = new HalfSingle(x % 2 == 0 ? 2.5F : -.5F); + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.Float, Compression = compression }); + + stream.Position = 0; + using Image output = Image.Load(stream); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffSampleFormat.Float, metadata.SampleFormat); + Assert.Equal(TiffBitsPerPixel.Bit32, metadata.BitsPerPixel); + Assert.Equal((byte)1, metadata.BitsPerSample.Channels); + Assert.Equal((ushort)32, metadata.BitsPerSample.Channel0); + Assert.Equal(compression, metadata.Compression); + + stream.Position = 0; + ImageInfo identified = Image.Identify(stream); + Assert.Equal(TiffSampleFormat.Float, identified.Metadata.GetTiffMetadata().SampleFormat); + Assert.Equal(TiffSampleFormat.Float, identified.FrameMetadataCollection[0].GetTiffMetadata().SampleFormat); + + // The row crosses the writer's 64-pixel block boundary, so both blocks must retain the samples. + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, 0].ToSingle(), output[x, 0].ToSingle()); + } + + stream.Position = 0; + using Image defaultOutput = Image.Load(stream); + Image defaultPixels = Assert.IsType>(defaultOutput); + Assert.Equal(2.5F, defaultPixels[0, 0].R); + } + + [Theory] + [InlineData(TiffCompression.None, TiffPredictor.None)] + [InlineData(TiffCompression.PackBits, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.Horizontal)] + [InlineData(TiffCompression.Lzw, TiffPredictor.None)] + [InlineData(TiffCompression.Lzw, TiffPredictor.Horizontal)] + public void Rgb48_RoundTrips16BitSamples(TiffCompression compression, TiffPredictor predictor) + { + using Image input = new(65, 2); + + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + input[x, y] = new Rgb48((ushort)(0x1201 + x + y), (ushort)(0x3402 + (x * 3)), (ushort)(0x5603 + (y * 7))); + } + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { BitsPerPixel = TiffBitsPerPixel.Bit48, Compression = compression, HorizontalPredictor = predictor }); + + stream.Position = 0; + using Image output = Image.Load(stream); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffBitsPerPixel.Bit48, metadata.BitsPerPixel); + Assert.Equal(new TiffBitsPerSample(16, 16, 16), metadata.BitsPerSample); + Assert.Equal(TiffSampleFormat.UnsignedInteger, metadata.SampleFormat); + Assert.Equal(compression, metadata.Compression); + Assert.Equal(predictor, metadata.Predictor); + + // Distinct low bytes expose any conversion through an 8-bit pixel format. + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + + stream.Position = 0; + using Image defaultOutput = Image.Load(stream); + Image defaultPixels = Assert.IsType>(defaultOutput); + Assert.Equal(input[64, 1], defaultPixels[64, 1]); + } + + [Theory] + [InlineData(TiffCompression.None, TiffPredictor.None)] + [InlineData(TiffCompression.PackBits, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.Horizontal)] + [InlineData(TiffCompression.Lzw, TiffPredictor.None)] + [InlineData(TiffCompression.Lzw, TiffPredictor.Horizontal)] + public void Rgba64_RoundTrips16BitSamples(TiffCompression compression, TiffPredictor predictor) + { + using Image input = new(65, 2); + + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + input[x, y] = new Rgba64((ushort)(0x1201 + x + y), (ushort)(0x3402 + (x * 3)), (ushort)(0x5603 + (y * 7)), (ushort)(0x7804 + x)); + } + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { BitsPerPixel = TiffBitsPerPixel.Bit64, Compression = compression, HorizontalPredictor = predictor }); + + stream.Position = 0; + using Image output = Image.Load(stream); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffBitsPerPixel.Bit64, metadata.BitsPerPixel); + Assert.Equal(new TiffBitsPerSample(16, 16, 16, 16), metadata.BitsPerSample); + Assert.Equal(TiffSampleFormat.UnsignedInteger, metadata.SampleFormat); + Assert.Equal(TiffExtraSampleType.UnassociatedAlphaData, metadata.ExtraSampleType); + Assert.Equal(compression, metadata.Compression); + Assert.Equal(predictor, metadata.Predictor); + + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + + stream.Position = 0; + using Image defaultOutput = Image.Load(stream); + Image defaultPixels = Assert.IsType>(defaultOutput); + Assert.Equal(input[64, 1], defaultPixels[64, 1]); + } + + [Theory] + [WithFile(FlowerRgb161616Contiguous, PixelTypes.Rgb48, TiffBitsPerPixel.Bit48)] + [WithFile(FlowerRgb161616ContiguousLittleEndian, PixelTypes.Rgb48, TiffBitsPerPixel.Bit48)] + [WithFile(FlowerRgb161616PredictorBigEndian, PixelTypes.Rgb48, TiffBitsPerPixel.Bit48)] + [WithFile(FlowerRgb161616PredictorLittleEndian, PixelTypes.Rgb48, TiffBitsPerPixel.Bit48)] + [WithFile(Rgba16BitUnassociatedAlphaBigEndian, PixelTypes.Rgba64, TiffBitsPerPixel.Bit64)] + [WithFile(Rgba16BitUnassociatedAlphaLittleEndian, PixelTypes.Rgba64, TiffBitsPerPixel.Bit64)] + [WithFile(Rgba16BitUnassociatedAlphaBigEndianWithPredictor, PixelTypes.Rgba64, TiffBitsPerPixel.Bit64)] + [WithFile(Rgba16BitUnassociatedAlphaLittleEndianWithPredictor, PixelTypes.Rgba64, TiffBitsPerPixel.Bit64)] + public void SixteenBitReferenceImages_RoundTrip(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + + // Check the independent source decode first, so a decoder error cannot make + // the subsequent encoder comparison appear correct by repeating that error. + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + + TiffEncoder encoder = new() + { + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Deflate, + HorizontalPredictor = TiffPredictor.Horizontal + }; + + image.VerifyEncoder(provider, "tiff", bitsPerPixel, encoder, ImageComparer.Exact, referenceDecoder: ReferenceDecoder); + } + + [Theory] + [InlineData(TiffBitsPerPixel.Bit96, TiffCompression.None)] + [InlineData(TiffBitsPerPixel.Bit96, TiffCompression.PackBits)] + [InlineData(TiffBitsPerPixel.Bit96, TiffCompression.Deflate)] + [InlineData(TiffBitsPerPixel.Bit96, TiffCompression.Lzw)] + [InlineData(TiffBitsPerPixel.Bit128, TiffCompression.None)] + [InlineData(TiffBitsPerPixel.Bit128, TiffCompression.PackBits)] + [InlineData(TiffBitsPerPixel.Bit128, TiffCompression.Deflate)] + [InlineData(TiffBitsPerPixel.Bit128, TiffCompression.Lzw)] + public void FloatColor_RoundTripsSamplesAndLayout(TiffBitsPerPixel bitsPerPixel, TiffCompression compression) + { + using Image input = new(65, 1); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = new RgbaVector(2.500123F + (x * .25F), -.50001F, .250001F, .5F); + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.Float, BitsPerPixel = bitsPerPixel, Compression = compression }); + + stream.Position = 0; + using Image output = Image.Load(stream); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffSampleFormat.Float, metadata.SampleFormat); + Assert.Equal(bitsPerPixel, metadata.BitsPerPixel); + Assert.Equal((byte)((int)bitsPerPixel / 32), metadata.BitsPerSample.Channels); + Assert.Equal(TiffPhotometricInterpretation.Rgb, metadata.PhotometricInterpretation); + Assert.Equal(compression, metadata.Compression); + Assert.Equal(bitsPerPixel == TiffBitsPerPixel.Bit128 ? TiffExtraSampleType.UnassociatedAlphaData : null, metadata.ExtraSampleType); + + for (int x = 0; x < input.Width; x++) + { + RgbaVector actual = output[x, 0]; + Assert.Equal(2.500123F + (x * .25F), actual.R); + Assert.Equal(-.50001F, actual.G); + Assert.Equal(.250001F, actual.B); + Assert.Equal(bitsPerPixel == TiffBitsPerPixel.Bit128 ? .5F : 1F, actual.A); + } + + stream.Position = 0; + using Image defaultOutput = Image.Load(stream); + Image defaultPixels = Assert.IsType>(defaultOutput); + Assert.Equal(2.500123F, defaultPixels[0, 0].R); + } + + [Theory] + [InlineData(TiffCompression.None)] + [InlineData(TiffCompression.PackBits)] + [InlineData(TiffCompression.Deflate)] + [InlineData(TiffCompression.Lzw)] + public void FloatAssociatedColor_RetainsStoredColorAtZeroAlpha(TiffCompression compression) + { + using Image input = new(65, 1); + RgbaVectorP pixel = new(2.500123F, -.50001F, .250001F, 0F); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = pixel; + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.Float, Compression = compression }); + + stream.Position = 0; + using Image output = Image.Load(stream); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffSampleFormat.Float, metadata.SampleFormat); + Assert.Equal(TiffBitsPerPixel.Bit128, metadata.BitsPerPixel); + Assert.Equal(TiffExtraSampleType.AssociatedAlphaData, metadata.ExtraSampleType); + Assert.Equal(compression, metadata.Compression); + + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(pixel.ToVector4(), output[x, 0].ToVector4()); + } + + stream.Position = 0; + using Image defaultOutput = Image.Load(stream); + Image defaultPixels = Assert.IsType>(defaultOutput); + Assert.Equal(pixel.ToVector4(), defaultPixels[0, 0].ToVector4()); + } + + [Fact] + public void FloatTiff_DefaultReencodeRetainsFloatSamples() + { + using Image input = new(65, 1); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = new RgbaVector(2.5F + (x * .25F), -.5F, .25F, 1F); + } + + using MemoryStream first = new(); + input.Save(first, new TiffEncoder { SampleFormat = TiffSampleFormat.Float }); + + first.Position = 0; + using Image decoded = Image.Load(first); + Assert.IsType>(decoded); + + using MemoryStream second = new(); + decoded.Save(second, new TiffEncoder()); + + second.Position = 0; + using Image output = Image.Load(second); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffSampleFormat.Float, metadata.SampleFormat); + Assert.Equal(TiffBitsPerPixel.Bit128, metadata.BitsPerPixel); + + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, 0], output[x, 0]); + } + } + + [Fact] + public void FloatTiff_DefaultLoadPreservesNonfiniteSamplesAndSignedZero() + { + using Image input = new(65, 1); + RgbaVectorP pixel = new(float.NaN, float.PositiveInfinity, float.NegativeInfinity, -0F); + + for (int x = 0; x < input.Width; x++) + { + input[x, 0] = pixel; + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.Float, Compression = TiffCompression.Deflate }); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + Image output = Assert.IsType>(decoded); + + for (int x = 0; x < output.Width; x++) + { + RgbaVectorP actual = output[x, 0]; + Assert.True(float.IsNaN(actual.R)); + Assert.True(float.IsPositiveInfinity(actual.G)); + Assert.True(float.IsNegativeInfinity(actual.B)); + Assert.Equal(BitConverter.SingleToInt32Bits(-0F), BitConverter.SingleToInt32Bits(actual.A)); + } + } + [Theory] [InlineData(null, TiffBitsPerPixel.Bit24)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)] @@ -75,10 +389,6 @@ public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel) } [Theory] - [InlineData(TiffBitsPerPixel.Bit48)] - [InlineData(TiffBitsPerPixel.Bit42)] - [InlineData(TiffBitsPerPixel.Bit36)] - [InlineData(TiffBitsPerPixel.Bit30)] [InlineData(TiffBitsPerPixel.Bit12)] [InlineData(TiffBitsPerPixel.Bit10)] [InlineData(TiffBitsPerPixel.Bit6)] @@ -101,6 +411,242 @@ public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPix Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel); } + [Theory] + [InlineData(TiffBitsPerPixel.Bit30, TiffCompression.None)] + [InlineData(TiffBitsPerPixel.Bit36, TiffCompression.None)] + [InlineData(TiffBitsPerPixel.Bit42, TiffCompression.None)] + [InlineData(TiffBitsPerPixel.Bit30, TiffCompression.Jpeg)] + [InlineData(TiffBitsPerPixel.Bit36, TiffCompression.Jpeg)] + [InlineData(TiffBitsPerPixel.Bit42, TiffCompression.Jpeg)] + public void EncoderOptions_UnsupportedColorDepth_Uses48Bits(TiffBitsPerPixel bitsPerPixel, TiffCompression compression) + { + // Nonzero low bytes reveal a fallback through 8-bit samples. + Rgb48 expected = new(0x1201, 0x3402, 0x5603); + using Image input = new(1, 1); + input[0, 0] = expected; + using MemoryStream stream = new(); + + input.Save(stream, new TiffEncoder { BitsPerPixel = bitsPerPixel, Compression = compression }); + + stream.Position = 0; + using Image output = Image.Load(stream); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffBitsPerPixel.Bit48, metadata.BitsPerPixel); + Assert.Equal(new TiffBitsPerSample(16, 16, 16), metadata.BitsPerSample); + Assert.Equal(compression == TiffCompression.Jpeg ? TiffCompression.Deflate : compression, metadata.Compression); + Assert.Equal(expected, output[0, 0]); + } + + [Theory] + [InlineData(TiffCompression.None, TiffPredictor.None)] + [InlineData(TiffCompression.PackBits, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.Horizontal)] + [InlineData(TiffCompression.Lzw, TiffPredictor.None)] + [InlineData(TiffCompression.Lzw, TiffPredictor.Horizontal)] + [InlineData(TiffCompression.Jpeg, TiffPredictor.None)] + public void UnsignedRgb96_RoundTripsEverySampleBit(TiffCompression compression, TiffPredictor predictor) + { + // 2^24 + 1 is the first integer a float cannot represent exactly. + // The second value sets the unsigned high bit and keeps its low bit set. + const uint FirstInexactFloatSample = 0x01000001U; + const uint HighUnsignedSample = 0x80000001U; + + // The 65-pixel rows cross SIMD block boundaries and check that the predictor + // restarts on the second row. Decreasing from uint.MaxValue also checks the + // upper bound while adjacent samples differ in bits Vector4 would lose. + using Image input = new(65, 2); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + input[x, y] = new Rgb96(FirstInexactFloatSample + (uint)x, HighUnsignedSample + (uint)y, uint.MaxValue - (uint)x); + } + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.UnsignedInteger, BitsPerPixel = TiffBitsPerPixel.Bit96, Compression = compression, HorizontalPredictor = predictor }); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + Image output = Assert.IsType>(decoded); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffBitsPerPixel.Bit96, metadata.BitsPerPixel); + Assert.Equal(new TiffBitsPerSample(32, 32, 32), metadata.BitsPerSample); + Assert.Equal(TiffSampleFormat.UnsignedInteger, metadata.SampleFormat); + Assert.Equal(compression == TiffCompression.Jpeg ? TiffCompression.Deflate : compression, metadata.Compression); + Assert.Equal(predictor, metadata.Predictor); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + } + + [Theory] + [InlineData(TiffCompression.None, TiffPredictor.None)] + [InlineData(TiffCompression.PackBits, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.None)] + [InlineData(TiffCompression.Deflate, TiffPredictor.Horizontal)] + [InlineData(TiffCompression.Lzw, TiffPredictor.None)] + [InlineData(TiffCompression.Lzw, TiffPredictor.Horizontal)] + [InlineData(TiffCompression.Jpeg, TiffPredictor.None)] + public void UnsignedRgba128_RoundTripsEverySampleBit(TiffCompression compression, TiffPredictor predictor) + { + // These values exercise the first integer a float cannot represent, + // the unsigned high bit, and a non-opaque alpha with a significant low bit. + const uint FirstInexactFloatSample = 0x01000001U; + const uint HighUnsignedSample = 0x80000001U; + const uint NonOpaqueAlphaSample = 0x40000001U; + + // Cross SIMD block and row boundaries and decrease from uint.MaxValue. + // The first pixel in each row has nonzero color with zero alpha, which + // encoding must retain. + using Image input = new(65, 2); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + input[x, y] = new Rgba128(FirstInexactFloatSample + (uint)x, HighUnsignedSample + (uint)y, uint.MaxValue - (uint)x, x == 0 ? 0U : NonOpaqueAlphaSample + (uint)x); + } + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.UnsignedInteger, BitsPerPixel = TiffBitsPerPixel.Bit128, Compression = compression, HorizontalPredictor = predictor }); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + Image output = Assert.IsType>(decoded); + TiffFrameMetadata metadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + + Assert.Equal(TiffBitsPerPixel.Bit128, metadata.BitsPerPixel); + Assert.Equal(new TiffBitsPerSample(32, 32, 32, 32), metadata.BitsPerSample); + Assert.Equal(TiffSampleFormat.UnsignedInteger, metadata.SampleFormat); + Assert.Equal(TiffExtraSampleType.UnassociatedAlphaData, metadata.ExtraSampleType); + Assert.Equal(compression == TiffCompression.Jpeg ? TiffCompression.Deflate : compression, metadata.Compression); + Assert.Equal(predictor, metadata.Predictor); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + } + + [Theory] + [InlineData(TiledRgb96BitLittleEndianDeflateCompressedWithPredictor)] + [InlineData(TiledRgb96BitBigEndianDeflateCompressedWithPredictor)] + [InlineData(TiledRgb96BitLittleEndianLzwCompressedWithPredictor)] + [InlineData(TiledRgb96BitBigEndianLzwCompressedWithPredictor)] + public void UnsignedRgb96_RealReferenceImage_RoundTrips(string path) + { + using Image input = Image.Load(TestFile.GetInputFileFullPath(path)); + + // These are the sample words at two pixels in libtiff's uncompressed output + // of the source tiles. Hex keeps each stored byte visible. + Assert.Equal(new Rgb96(0x0D0D0D0DU, 0x0D0D0D0DU, 0x0F0F0F0FU), input[0, 0]); + Assert.Equal(new Rgb96(0xB1B1B1B1U, 0xA9A9A9A9U, 0x9E9E9E9EU), input[65, 97]); + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { BitsPerPixel = TiffBitsPerPixel.Bit96, Compression = TiffCompression.Deflate }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + } + + [Theory] + [InlineData(TiledRgba128BitLittleEndianDeflateCompressedWithPredictor)] + [InlineData(TiledRgba128BitBigEndianDeflateCompressedWithPredictor)] + [InlineData(TiledRgba128BitLittleEndianLzwCompressedWithPredictor)] + [InlineData(TiledRgba128BitBigEndianLzwCompressedWithPredictor)] + public void UnsignedRgba128_RealReferenceImage_RoundTrips(string path) + { + using Image input = Image.Load(TestFile.GetInputFileFullPath(path)); + + // These are the sample words at two pixels in libtiff's uncompressed output + // of the source tiles. Hex keeps each stored byte visible. + Assert.Equal(new Rgba128(0x0D0D0D0DU, 0x0D0D0D0DU, 0x0F0F0F0FU, uint.MaxValue), input[0, 0]); + Assert.Equal(new Rgba128(0xB1B1B1B1U, 0xA9A9A9A9U, 0x9E9E9E9EU, uint.MaxValue), input[65, 97]); + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { BitsPerPixel = TiffBitsPerPixel.Bit128, Compression = TiffCompression.Deflate }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + } + + [Theory] + [InlineData(FlowerRgb323232Planar)] + [InlineData(FlowerRgb323232PlanarLittleEndian)] + public void UnsignedRgb96_PlanarReferenceImage_RoundTrips(string path) + { + using Image input = Image.Load(TestFile.GetInputFileFullPath(path)); + + // These are the 32-bit sample words at two pixels in the uncompressed + // planar TIFF. Hex keeps each stored byte visible for byte-order checks. + Assert.Equal(new Rgb96(0x58CC576BU, 0x58A64DD6U, 0x47713036U), input[0, 0]); + Assert.Equal(new Rgb96(0x44B8137EU, 0x4650DF9BU, 0x3C3B4613U), input[72, 42]); + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { BitsPerPixel = TiffBitsPerPixel.Bit96, Compression = TiffCompression.Deflate }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + } + + [Theory] + [InlineData(Rgba32BitPlanarUnassociatedAlphaLittleEndian)] + [InlineData(Rgba32BitPlanarUnassociatedAlphaBigEndian)] + public void UnsignedRgba128_PlanarReferenceImage_RoundTrips(string path) + { + using Image input = Image.Load(TestFile.GetInputFileFullPath(path)); + + // These hex values are the exact 32-bit sample words at the named pixels + // in the uncompressed planar TIFF, with each byte visible. The first + // pixel has stored color at zero alpha; the second checks nonzero alpha. + Assert.Equal(new Rgba128(0xB2B2B2B2U, 0xB2B2B2B2U, 0xFEFEFEFEU, 0U), input[78, 7]); + Assert.Equal(new Rgba128(0xD2D2D2D2U, 0xA0A0A0A0U, 0xA0A0A0A0U, 0xF7F7F7F7U), input[140, 105]); + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { BitsPerPixel = TiffBitsPerPixel.Bit128, Compression = TiffCompression.Deflate }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int y = 0; y < input.Height; y++) + { + for (int x = 0; x < input.Width; x++) + { + Assert.Equal(input[x, y], output[x, y]); + } + } + } + [Theory] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Ccitt1D)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.CcittGroup3Fax)] diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffFloatRowTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffFloatRowTests.cs new file mode 100644 index 0000000000..4a8cf11721 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffFloatRowTests.cs @@ -0,0 +1,374 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.Common; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff; + +[Trait("Format", "Tiff")] +public class TiffFloatRowTests +{ + /// + /// Checks the three-component float writer with hardware intrinsics and its scalar fallback. + /// + [Fact] + public void FloatRgbWriter_PreservesSamplesAcrossSimdAndTail() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunFloatRgbWriter, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks four-component associated float output at zero and nonzero alpha. + /// + [Fact] + public void FloatRgbaWriter_PreservesAssociatedSamplesAcrossSimdAndTail() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunFloatRgbaWriter, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks single-component float output in both grayscale interpretations. + /// + /// Whether the stored grayscale samples are inverted. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void FloatSingleComponentWriter_PreservesSamplesAcrossSimdAndTail(bool whiteIsZero) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunFloatSingleComponentWriter, + whiteIsZero, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks BT.709 grayscale output from a four-component floating-point source. + /// + [Fact] + public void FloatColorToGrayscaleWriter_UsesBt709AcrossSimdAndTail() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunFloatColorToGrayscaleWriter, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Encodes three-component float samples and checks every decoded component bit. + /// + private static void RunFloatRgbWriter() + { + // The writer uses 64-pixel blocks. Width 71 gives a full block, then + // a four-pixel SIMD group and a three-pixel scalar tail. + const int Width = 71; + + // The explicit negative-zero and NaN-payload bits detect changes that + // numeric floating-point equality would not expose. + float[] samples = + [ + 2.5F, + -.5F, + 0.1F, + BitConverter.Int32BitsToSingle(unchecked((int)0x80000000)), + float.PositiveInfinity, + float.NegativeInfinity, + BitConverter.Int32BitsToSingle(unchecked((int)0x7FC01234)), + .25F + ]; + + using Image input = new(Width, 1); + for (int x = 0; x < Width; x++) + { + input[x, 0] = new RgbaVector( + samples[x % samples.Length], + samples[(x + 2) % samples.Length], + samples[(x + 4) % samples.Length], + .5F); + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.Float, BitsPerPixel = TiffBitsPerPixel.Bit96, Compression = TiffCompression.None }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int x = 0; x < Width; x++) + { + RgbaVector expected = input[x, 0]; + RgbaVector actual = output[x, 0]; + Assert.Equal(BitConverter.SingleToInt32Bits(expected.R), BitConverter.SingleToInt32Bits(actual.R)); + Assert.Equal(BitConverter.SingleToInt32Bits(expected.G), BitConverter.SingleToInt32Bits(actual.G)); + Assert.Equal(BitConverter.SingleToInt32Bits(expected.B), BitConverter.SingleToInt32Bits(actual.B)); + Assert.Equal(1F, actual.A); + } + } + + /// + /// Encodes associated samples without losing stored color at zero alpha. + /// + private static void RunFloatRgbaWriter() + { + // The second block includes one full SIMD group and a three-pixel tail. + const int Width = 71; + + using Image input = new(Width, 1); + for (int x = 0; x < Width; x++) + { + input[x, 0] = new RgbaVectorP(2.5F + (x * .125F), -.5F, .25F, x % 2 == 0 ? 0F : .5F); + } + + using MemoryStream stream = new(); + input.Save(stream, new TiffEncoder { SampleFormat = TiffSampleFormat.Float, BitsPerPixel = TiffBitsPerPixel.Bit128, Compression = TiffCompression.None }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int x = 0; x < Width; x++) + { + RgbaVectorP expected = input[x, 0]; + RgbaVectorP actual = output[x, 0]; + Assert.Equal(BitConverter.SingleToInt32Bits(expected.R), BitConverter.SingleToInt32Bits(actual.R)); + Assert.Equal(BitConverter.SingleToInt32Bits(expected.G), BitConverter.SingleToInt32Bits(actual.G)); + Assert.Equal(BitConverter.SingleToInt32Bits(expected.B), BitConverter.SingleToInt32Bits(actual.B)); + Assert.Equal(BitConverter.SingleToInt32Bits(expected.A), BitConverter.SingleToInt32Bits(actual.A)); + } + } + + /// + /// Encodes one-component float samples and checks the result after TIFF decoding. + /// + /// Whether to use the WhiteIsZero interpretation. + private static void RunFloatSingleComponentWriter(string serialized) + { + bool whiteIsZero = FeatureTestRunner.Deserialize(serialized); + + // Exercise the 64-pixel block boundary, a full SIMD group, and its tail. + const int Width = 71; + float[] samples = [2.5F, -.5F, .25F, 1F, 0F, 4F]; + + using Image input = new(Width, 1); + for (int x = 0; x < Width; x++) + { + input[x, 0] = new HalfSingle(samples[x % samples.Length]); + } + + using MemoryStream stream = new(); + input.Save( + stream, + new TiffEncoder + { + SampleFormat = TiffSampleFormat.Float, + BitsPerPixel = TiffBitsPerPixel.Bit32, + PhotometricInterpretation = whiteIsZero ? TiffPhotometricInterpretation.WhiteIsZero : TiffPhotometricInterpretation.BlackIsZero, + Compression = TiffCompression.None + }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int x = 0; x < Width; x++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(input[x, 0].ToSingle()), BitConverter.SingleToInt32Bits(output[x, 0].R)); + } + } + + /// + /// Encodes color as grayscale and checks the stored BT.709 intensity through decoding. + /// + private static void RunFloatColorToGrayscaleWriter() + { + // The seven pixels after the first block exercise both SIMD and scalar packing. + const int Width = 71; + using Image input = new(Width, 1); + + // One nonzero component per pixel makes the expected luminance independent + // of floating-point addition order while exposing component-order mistakes. + for (int x = 0; x < Width; x++) + { + input[x, 0] = (x % 3) switch + { + 0 => new RgbaVector(2F, 0F, 0F), + 1 => new RgbaVector(0F, -2F, 0F), + _ => new RgbaVector(0F, 0F, 4F) + }; + } + + using MemoryStream stream = new(); + input.Save( + stream, + new TiffEncoder + { + SampleFormat = TiffSampleFormat.Float, + BitsPerPixel = TiffBitsPerPixel.Bit32, + PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero, + Compression = TiffCompression.None + }); + + stream.Position = 0; + using Image output = Image.Load(stream); + for (int x = 0; x < Width; x++) + { + float expected = (x % 3) switch + { + 0 => 2F * ColorNumerics.Bt709.X, + 1 => -2F * ColorNumerics.Bt709.Y, + _ => 4F * ColorNumerics.Bt709.Z + }; + + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(output[x, 0].R)); + } + } + + [Theory] + [MemberData(nameof(SimdUtilsTests.ArbitraryArraySizes), MemberType = typeof(SimdUtilsTests))] + public void FloatDecoderRows_PreserveComponentBitsAcrossSimdAndTail(int count) + { + static void RunTest(string serialized) => AssertFloatDecoderRows(FeatureTestRunner.Deserialize(serialized)); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + count, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks the TIFF row decoders through their Decode contract at SIMD widths and tails. + /// + /// The number of pixels in the row. + private static void AssertFloatDecoderRows(int count) + { + // Payload bits and signed zero reveal conversions that numeric equality misses. + float[] values = + [ + BitConverter.Int32BitsToSingle(unchecked((int)0x7FC01234)), + float.PositiveInfinity, + float.NegativeInfinity, + BitConverter.Int32BitsToSingle(unchecked((int)0x80000000)), + -2.5F, + 0.1F, + 1F, + 65504F + ]; + + float[] triplets = new float[count * 3]; + float[] quads = new float[count * 4]; + float[] singles = new float[count]; + for (int i = 0; i < count; i++) + { + for (int component = 0; component < 4; component++) + { + float sample = values[((i * 4) + component) % values.Length]; + quads[(i * 4) + component] = sample; + if (component < 3) + { + triplets[(i * 3) + component] = sample; + } + } + + singles[i] = values[i % values.Length]; + } + + Configuration configuration = Configuration.Default; + using Buffer2D pixels = configuration.MemoryAllocator.Allocate2D(Math.Max(count, 1), 1); + + for (int byteOrder = 0; byteOrder < 2; byteOrder++) + { + bool reverseEndianness = byteOrder != 0; + bool isBigEndian = reverseEndianness == BitConverter.IsLittleEndian; + byte[] packedTriplets = MemoryMarshal.AsBytes(triplets.AsSpan()).ToArray(); + byte[] packedQuads = MemoryMarshal.AsBytes(quads.AsSpan()).ToArray(); + byte[] packedSingles = MemoryMarshal.AsBytes(singles.AsSpan()).ToArray(); + if (reverseEndianness) + { + ReverseSamples(packedTriplets); + ReverseSamples(packedQuads); + ReverseSamples(packedSingles); + } + + RgbFloat323232TiffColor rgb = new(configuration, isBigEndian); + rgb.Decode(packedTriplets, pixels, 0, 0, count, 1); + for (int i = 0; i < count; i++) + { + RgbaVector actual = pixels.DangerousGetRowSpan(0)[i]; + Assert.Equal(BitConverter.SingleToInt32Bits(triplets[i * 3]), BitConverter.SingleToInt32Bits(actual.R)); + Assert.Equal(BitConverter.SingleToInt32Bits(triplets[(i * 3) + 1]), BitConverter.SingleToInt32Bits(actual.G)); + Assert.Equal(BitConverter.SingleToInt32Bits(triplets[(i * 3) + 2]), BitConverter.SingleToInt32Bits(actual.B)); + Assert.Equal(1F, actual.A); + } + + RgbaFloat32323232TiffColor rgba = new(configuration, isBigEndian, TiffExtraSampleType.UnassociatedAlphaData); + rgba.Decode(packedQuads, pixels, 0, 0, count, 1); + for (int i = 0; i < count; i++) + { + RgbaVector actual = pixels.DangerousGetRowSpan(0)[i]; + Assert.Equal(BitConverter.SingleToInt32Bits(quads[i * 4]), BitConverter.SingleToInt32Bits(actual.R)); + Assert.Equal(BitConverter.SingleToInt32Bits(quads[(i * 4) + 1]), BitConverter.SingleToInt32Bits(actual.G)); + Assert.Equal(BitConverter.SingleToInt32Bits(quads[(i * 4) + 2]), BitConverter.SingleToInt32Bits(actual.B)); + Assert.Equal(BitConverter.SingleToInt32Bits(quads[(i * 4) + 3]), BitConverter.SingleToInt32Bits(actual.A)); + } + + BlackIsZero32FloatTiffColor grayscale = new(configuration, isBigEndian); + grayscale.Decode(packedSingles, pixels, 0, 0, count, 1); + for (int i = 0; i < count; i++) + { + RgbaVector actual = pixels.DangerousGetRowSpan(0)[i]; + int expected = BitConverter.SingleToInt32Bits(singles[i]); + Assert.Equal(expected, BitConverter.SingleToInt32Bits(actual.R)); + Assert.Equal(expected, BitConverter.SingleToInt32Bits(actual.G)); + Assert.Equal(expected, BitConverter.SingleToInt32Bits(actual.B)); + Assert.Equal(1F, actual.A); + } + } + } + + [Theory] + [MemberData(nameof(SimdUtilsTests.ArbitraryArraySizes), MemberType = typeof(SimdUtilsTests))] + public void WhiteIsZeroFloatRows_InvertAcrossSimdAndTail(int count) + { + static void RunTest(string serialized) => AssertWhiteIsZeroFloatRows(FeatureTestRunner.Deserialize(serialized)); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + count, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + } + + /// + /// Checks WhiteIsZero using finite samples, including values outside zero to one. + /// + /// The number of pixels in the row. + private static void AssertWhiteIsZeroFloatRows(int count) + { + float[] values = [0F, 0.25F, 0.5F, 1F, 2F]; + float[] samples = new float[count]; + for (int i = 0; i < count; i++) + { + samples[i] = values[i % values.Length]; + } + + Configuration configuration = Configuration.Default; + using Buffer2D pixels = configuration.MemoryAllocator.Allocate2D(Math.Max(count, 1), 1); + WhiteIsZero32FloatTiffColor decoder = new(configuration, isBigEndian: !BitConverter.IsLittleEndian); + decoder.Decode(MemoryMarshal.AsBytes(samples.AsSpan()), pixels, 0, 0, count, 1); + + for (int i = 0; i < count; i++) + { + RgbaVector actual = pixels.DangerousGetRowSpan(0)[i]; + Assert.Equal(1F - samples[i], actual.R); + Assert.Equal(actual.R, actual.G); + Assert.Equal(actual.R, actual.B); + Assert.Equal(1F, actual.A); + } + } + + /// + /// Reverses each stored float sample without changing sample order. + /// + /// The packed sample bytes. + private static void ReverseSamples(Span samples) + { + for (int i = 0; i < samples.Length; i += sizeof(float)) + { + samples.Slice(i, sizeof(float)).Reverse(); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 9f0f0e9910..d181cacb5a 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -34,15 +34,78 @@ public void TiffMetadata_CloneIsDeep() { ByteOrder = ByteOrder.BigEndian, FormatType = TiffFormatType.BigTIFF, + SampleFormat = TiffSampleFormat.Float }; TiffMetadata clone = (TiffMetadata)meta.DeepClone(); clone.ByteOrder = ByteOrder.LittleEndian; clone.FormatType = TiffFormatType.Default; + clone.SampleFormat = TiffSampleFormat.UnsignedInteger; Assert.Equal(ByteOrder.BigEndian, meta.ByteOrder); Assert.Equal(TiffFormatType.BigTIFF, meta.FormatType); + Assert.Equal(TiffSampleFormat.Float, meta.SampleFormat); + } + + [Fact] + public void TiffMetadata_PixelTypeInfo_DescribesSingle32BitGrayscaleSample() + { + TiffMetadata metadata = new() + { + BitsPerPixel = TiffBitsPerPixel.Bit32, + BitsPerSample = new TiffBitsPerSample(32, 0, 0), + PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero + }; + + PixelTypeInfo info = metadata.GetPixelTypeInfo(); + + Assert.Equal(32, info.BitsPerPixel); + Assert.Equal(PixelColorType.Luminance, info.ColorType); + Assert.Equal(PixelAlphaRepresentation.None, info.AlphaRepresentation); + Assert.Equal(1, info.ComponentInfo.Value.ComponentCount); + Assert.Equal(32, info.ComponentInfo.Value.GetComponentPrecision(0)); + Assert.Equal(0, info.ComponentInfo.Value.Padding); + } + + [Fact] + public void TiffMetadata_PixelTypeInfo_DescribesTwoComponentGrayscale() + { + TiffMetadata metadata = new() + { + BitsPerPixel = TiffBitsPerPixel.Bit16, + BitsPerSample = new TiffBitsPerSample(8, 8, 0), + PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero, + ExtraSampleType = TiffExtraSampleType.UnassociatedAlphaData + }; + + PixelTypeInfo info = metadata.GetPixelTypeInfo(); + + Assert.Equal(PixelColorType.Luminance | PixelColorType.Alpha, info.ColorType); + Assert.Equal(PixelAlphaRepresentation.Unassociated, info.AlphaRepresentation); + Assert.Equal(2, info.ComponentInfo.Value.ComponentCount); + Assert.Equal(8, info.ComponentInfo.Value.GetComponentPrecision(0)); + Assert.Equal(8, info.ComponentInfo.Value.GetComponentPrecision(1)); + Assert.Equal(0, info.ComponentInfo.Value.Padding); + } + + [Fact] + public void TiffMetadata_PixelTypeInfo_PreservesAssociatedAlpha() + { + TiffMetadata metadata = new() + { + BitsPerPixel = TiffBitsPerPixel.Bit128, + BitsPerSample = new TiffBitsPerSample(32, 32, 32, 32), + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + ExtraSampleType = TiffExtraSampleType.AssociatedAlphaData + }; + + PixelTypeInfo info = metadata.GetPixelTypeInfo(); + + Assert.Equal(PixelColorType.RGB | PixelColorType.Alpha, info.ColorType); + Assert.Equal(PixelAlphaRepresentation.Associated, info.AlphaRepresentation); + Assert.Equal(4, info.ComponentInfo.Value.ComponentCount); + Assert.Equal(32, info.ComponentInfo.Value.GetComponentPrecision(3)); } [Theory] diff --git a/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs b/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs index 903e997aad..3636c49ed6 100644 --- a/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs @@ -7,21 +7,19 @@ namespace SixLabors.ImageSharp.Tests.Helpers; public class ColorNumericsTests { + /// + /// Grayscale luminance retains the source value even when it is outside the unit interval. + /// + /// The grayscale component value. [Theory] - [InlineData(0.2f, 0.7f, 0.1f, 256, 140)] - [InlineData(0.5f, 0.5f, 0.5f, 256, 128)] - [InlineData(0.5f, 0.5f, 0.5f, 65536, 32768)] - [InlineData(0.2f, 0.7f, 0.1f, 65536, 36069)] - public void GetBT709Luminance_WithVector4(float x, float y, float z, int luminanceLevels, int expected) + [InlineData(-2F)] + [InlineData(0.5F)] + [InlineData(2F)] + public void GetBT709Luminance_PreservesUnboundedGrayscale(float value) { - // arrange - Vector4 vector = new(x, y, z, 0.0f); + Vector4 vector = new(value, value, value, 1F); - // act - int actual = ColorNumerics.GetBT709Luminance(vector, luminanceLevels); - - // assert - Assert.Equal(expected, actual); + Assert.Equal(value, ColorNumerics.GetBT709Luminance(vector)); } [Theory] diff --git a/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs b/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs index b3e5b434b7..684bf76118 100644 --- a/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs @@ -367,7 +367,7 @@ private static void AssertHalfPackingMatchesScalar() float lower = (float)BitConverter.UInt16BitsToHalf((ushort)bits); float upper = (float)BitConverter.UInt16BitsToHalf((ushort)(bits + 1)); - // Every midpoint exercises binary16 round-to-nearest-even; negating it covers the symmetric sign path. + // Every midpoint exercises half-precision round-to-nearest-even; negating it covers the symmetric sign path. float midpoint = (lower + upper) * .5F; components[index++] = midpoint; components[index++] = -midpoint; @@ -443,8 +443,8 @@ private static void AssertAlphaConversionsMatchScalar() unassociatedScaled[i] = new Vector4(((i * 37) % 4093) / 4092F, ((i * 73) % 4093) / 4092F, ((i * 109) % 4093) / 4092F, alpha); associatedScaled[i] = new Vector4(unassociatedScaled[i].X * alpha, unassociatedScaled[i].Y * alpha, unassociatedScaled[i].Z * alpha, alpha); - unassociatedNative[i] = (unassociatedScaled[i] * 131008F) - new Vector4(65504F); - associatedNative[i] = (associatedScaled[i] * 131008F) - new Vector4(65504F); + unassociatedNative[i] = unassociatedScaled[i]; + associatedNative[i] = associatedScaled[i]; expectedFromUnassociatedNative[i] = HalfVector4P.FromUnassociatedVector4(unassociatedNative[i]); expectedFromAssociatedNative[i] = HalfVector4P.FromAssociatedVector4(associatedNative[i]); expectedFromUnassociatedScaled[i] = HalfVector4P.FromUnassociatedScaledVector4(unassociatedScaled[i]); @@ -845,10 +845,7 @@ public void Rgba32ToHalfVector4PMatchesExactAssociationForEveryComponentAndAlpha const int pairCount = 65536; const int channelCount = 3; - const float finiteMinimum = -65504F; - const float finiteRange = 131008F; - const float inverseFiniteRange = (float)(1D / finiteRange); - const ulong zeroComponentBits = 0xFBFF; + const ulong zeroComponentBits = 0; Rgba32[] source = new Rgba32[pairCount * channelCount]; HalfVector4P[] expected = new HalfVector4P[source.Length]; HalfVector4P[] actualBulk = new HalfVector4P[source.Length]; @@ -858,14 +855,14 @@ public void Rgba32ToHalfVector4PMatchesExactAssociationForEveryComponentAndAlpha { // Association must use the alpha value recovered from the destination half, rather than the higher-precision source alpha. float normalizedAlpha = (float)(alpha / (double)byte.MaxValue); - ushort alphaBits = BitConverter.HalfToUInt16Bits((Half)((normalizedAlpha * finiteRange) + finiteMinimum)); - float destinationAlpha = ((float)BitConverter.UInt16BitsToHalf(alphaBits) * inverseFiniteRange) + .5F; + ushort alphaBits = BitConverter.HalfToUInt16Bits((Half)normalizedAlpha); + float destinationAlpha = (float)BitConverter.UInt16BitsToHalf(alphaBits); for (int unassociated = 0; unassociated <= byte.MaxValue; unassociated++) { float normalizedComponent = (float)(unassociated / (double)byte.MaxValue); float associated = normalizedComponent * destinationAlpha; - ushort associatedBits = BitConverter.HalfToUInt16Bits((Half)((associated * finiteRange) + finiteMinimum)); + ushort associatedBits = BitConverter.HalfToUInt16Bits((Half)associated); ulong alphaPacked = (ulong)alphaBits << 48; source[index] = new Rgba32((byte)unassociated, 0, 0, (byte)alpha); @@ -1205,9 +1202,6 @@ public void NormalizedByte4PQuantizesDestinationAlphaBeforeAssociation() [Fact] public void HalfVector4PQuantizesDestinationAlphaBeforeAssociation() { - const float finiteMinimum = -65504F; - const float finiteRange = 131008F; - const float inverseFiniteRange = (float)(1D / finiteRange); ReadOnlySpan components = [64, 127, 191]; Rgba64[] source = new Rgba64[(ushort.MaxValue + 1) * components.Length]; HalfVector4P[] actualBulk = new HalfVector4P[source.Length]; @@ -1227,13 +1221,13 @@ public void HalfVector4PQuantizesDestinationAlphaBeforeAssociation() for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) { float normalizedAlpha = alpha / (float)ushort.MaxValue; - ushort expectedAlpha = BitConverter.HalfToUInt16Bits((Half)((normalizedAlpha * finiteRange) + finiteMinimum)); - float storedAlpha = ((float)BitConverter.UInt16BitsToHalf(expectedAlpha) * inverseFiniteRange) + .5F; + ushort expectedAlpha = BitConverter.HalfToUInt16Bits((Half)normalizedAlpha); + float storedAlpha = (float)BitConverter.UInt16BitsToHalf(expectedAlpha); foreach (byte component in components) { float associatedRed = (component / (float)byte.MaxValue) * storedAlpha; - ushort expectedRed = BitConverter.HalfToUInt16Bits((Half)((associatedRed * finiteRange) + finiteMinimum)); + ushort expectedRed = BitConverter.HalfToUInt16Bits((Half)associatedRed); HalfVector4P actualScalar = HalfVector4P.FromRgba64(source[index]); Assert.Equal(expectedRed, (ushort)actualScalar.PackedValue); diff --git a/tests/ImageSharp.Tests/PixelFormats/Bgr565Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Bgr565Tests.cs index 3c4a104233..8c4f3e02da 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Bgr565Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Bgr565Tests.cs @@ -235,6 +235,7 @@ public void Bgr565_Clamping() { Assert.Equal(Vector3.Zero, new Bgr565(Vector3.One * -1234F).ToVector3()); Assert.Equal(Vector3.One, new Bgr565(Vector3.One * 1234F).ToVector3()); + Assert.Equal((ushort)0x07E0, Bgr565.FromScaledVector4(new Vector4(float.NaN, float.PositiveInfinity, float.NegativeInfinity, 1F)).PackedValue); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs b/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs deleted file mode 100644 index 5718da5b03..0000000000 --- a/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Tests.PixelFormats; - -[Trait("Category", "PixelFormats")] -public class FloatingPointPixelNormalizationTests -{ - /// - /// HalfSingle normalizes scaled input identically in scalar and bulk conversions. - /// - [Fact] - public void HalfSingle_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); - - /// - /// HalfVector2 normalizes scaled input identically in scalar and bulk conversions. - /// - [Fact] - public void HalfVector2_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); - - /// - /// HalfVector4 normalizes scaled input identically in scalar and bulk conversions. - /// - [Fact] - public void HalfVector4_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); - - /// - /// HalfVector4P normalizes scaled input identically in scalar and bulk conversions. - /// - [Fact] - public void HalfVector4P_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); - - /// - /// RgbaVector normalizes scaled input identically in scalar and bulk conversions. - /// - [Fact] - public void RgbaVector_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); - - /// - /// RgbaHalf normalizes scaled input identically in scalar and bulk conversions. - /// - [Fact] - public void RgbaHalf_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); - - /// - /// RgbaHalfP normalizes scaled input identically in scalar and bulk conversions. - /// - [Fact] - public void RgbaHalfP_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); - - /// - /// Raw half storage preserves IEEE special values while its scaled representation remains finite. - /// - [Fact] - public void HalfVector4_NativeSpecialValuesHaveNormalizedScaledOutput() => AssertNativeSpecialValuesHaveNormalizedScaledOutput(); - - /// - /// Associated half-vector conversion uses the stored alpha ratio before normalizing RGB. - /// - [Fact] - public void HalfVector4P_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized(); - - /// - /// Associated half-RGBA conversion uses the stored alpha ratio before normalizing RGB. - /// - [Fact] - public void RgbaHalfP_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized(); - - /// - /// Checks saturation and NaN handling without deriving expectations from the invalid-input path. - /// - /// The destination pixel format. - private static void AssertScaledInputIsNormalized() - where TPixel : unmanaged, IPixel - { - Vector4[] inputs = - [ - new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), - new(2F, -2F, .5F, 1F), - new(.25F, .5F, .75F, .5F), - new(.25F, .5F, .75F, float.NaN), - new(.25F, .5F, .75F, float.PositiveInfinity) - ]; - - Vector4[] normalized = - [ - new(1F, 0F, 0F, 1F), - new(1F, 0F, .5F, 1F), - new(.25F, .5F, .75F, .5F), - new(.25F, .5F, .75F, 0F), - new(.25F, .5F, .75F, 1F) - ]; - - // Seventeen pixels exercise wide registers and the narrower remainder paths. - Vector4[] source = new Vector4[17]; - TPixel[] expected = new TPixel[source.Length]; - TPixel[] actual = new TPixel[source.Length]; - - for (int i = 0; i < source.Length; i++) - { - int sample = i % inputs.Length; - source[i] = inputs[sample]; - expected[i] = TPixel.FromUnassociatedScaledVector4(normalized[sample]); - Assert.Equal(expected[i], TPixel.FromUnassociatedScaledVector4(source[i])); - } - - // Associated formats otherwise interpret the vectors using their native alpha representation. - PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); - Assert.Equal(expected, actual); - } - - /// - /// Checks associated input against finite control values with the same represented color. - /// - /// The associated destination pixel format. - private static void AssertAssociatedScaledInputIsNormalized() - where TPixel : unmanaged, IPixel - { - Vector4[] inputs = - [ - new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), - new(1F, .5F, 1.5F, 2F), - new(.125F, .25F, .375F, .5F), - new(.25F, .5F, .75F, float.NaN), - new(.25F, .5F, .75F, float.PositiveInfinity) - ]; - - Vector4[] normalized = - [ - new(1F, 0F, 0F, 1F), - new(.5F, .25F, .75F, 1F), - new(.125F, .25F, .375F, .5F), - Vector4.Zero, - new(0F, 0F, 0F, 1F) - ]; - - Vector4[] source = new Vector4[17]; - TPixel[] expected = new TPixel[source.Length]; - TPixel[] actual = new TPixel[source.Length]; - - for (int i = 0; i < source.Length; i++) - { - int sample = i % inputs.Length; - source[i] = inputs[sample]; - expected[i] = TPixel.FromAssociatedScaledVector4(normalized[sample]); - Assert.Equal(expected[i], TPixel.FromAssociatedScaledVector4(source[i])); - } - - PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); - Assert.Equal(expected, actual); - } - - /// - /// Checks native storage and every scaled output lane independently of integer conversion semantics. - /// - private static void AssertNativeSpecialValuesHaveNormalizedScaledOutput() - { - Vector4 native = new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 65504F); - HalfVector4 pixel = HalfVector4.FromVector4(native); - Assert.True(float.IsPositiveInfinity(pixel.ToVector4().X)); - Assert.True(float.IsNegativeInfinity(pixel.ToVector4().Y)); - Assert.True(float.IsNaN(pixel.ToVector4().Z)); - - Vector4 expected = new(1F, 0F, 0F, 1F); - Assert.Equal(expected, pixel.ToScaledVector4()); - Assert.Equal(1F, new HalfSingle(float.PositiveInfinity).ToScaledVector4().X); - Assert.Equal(0F, new HalfSingle(float.NaN).ToScaledVector4().X); - Assert.Equal(new Vector4(1F, 0F, 0F, 1F), new HalfVector2(new Vector2(float.PositiveInfinity, float.NaN)).ToScaledVector4()); - - HalfVector4[] source = new HalfVector4[17]; - Vector4[] nativeSource = new Vector4[source.Length]; - Array.Fill(nativeSource, native); - PixelOperations.Instance.FromVector4Destructive(Configuration.Default, nativeSource, source, PixelConversionModifiers.None); - Assert.All(source, value => Assert.Equal(pixel.PackedValue, value.PackedValue)); - - Vector4[] actual = new Vector4[source.Length]; - PixelOperations.Instance.ToVector4(Configuration.Default, source, actual, PixelConversionModifiers.Scale); - Assert.All(actual, value => Assert.Equal(expected, value)); - } -} diff --git a/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelPreservationTests.cs b/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelPreservationTests.cs new file mode 100644 index 0000000000..bad1ba1a41 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelPreservationTests.cs @@ -0,0 +1,654 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +[Trait("Category", "PixelFormats")] +public class FloatingPointPixelPreservationTests +{ + /// + /// The four half-precision pixel types keep the same DirectX component order and bit layout. + /// + [Fact] + public void FourComponentHalfPixelsKeepDirectXLayout() + { + Vector4 components = new(1F, 2F, -0F, -1F); + const ulong expected = 0xBC00800040003C00UL; + + Assert.Equal(expected, new HalfVector4(components).PackedValue); + Assert.Equal(expected, new HalfVector4P(components).PackedValue); + Assert.Equal(expected, new RgbaHalf(components).PackedValue); + Assert.Equal(expected, new RgbaHalfP(components).PackedValue); + } + + /// + /// Associated binary32 pixels keep the DirectX component order and signed zero. + /// + [Fact] + public void AssociatedFloatPixelsKeepDirectXLayout() + { + RgbaVectorP[] pixels = [new(1F, 2F, -0F, -1F)]; + ReadOnlySpan components = MemoryMarshal.Cast(pixels); + + Assert.Equal(4, components.Length); + Assert.Equal(BitConverter.SingleToInt32Bits(1F), BitConverter.SingleToInt32Bits(components[0])); + Assert.Equal(BitConverter.SingleToInt32Bits(2F), BitConverter.SingleToInt32Bits(components[1])); + Assert.Equal(BitConverter.SingleToInt32Bits(-0F), BitConverter.SingleToInt32Bits(components[2])); + Assert.Equal(BitConverter.SingleToInt32Bits(-1F), BitConverter.SingleToInt32Bits(components[3])); + } + + /// + /// Floating-point formats preserve finite component values outside the unit interval. + /// + [Fact] + public void ScaledFloatStoragePreservesOutOfRangeComponents() + { + Vector4 source = new(2F, -1F, .5F, 1F); + + Assert.Equal(new Vector4(2F, 0F, 0F, 1F), HalfSingle.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(new Vector4(2F, -1F, 0F, 1F), HalfVector2.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, HalfVector4.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, RgbaHalf.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, RgbaVector.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, HalfVector4P.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, RgbaHalfP.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, RgbaVectorP.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + } + + /// + /// Four-component floating-point formats do not bound alpha to the unit interval. + /// + [Fact] + public void ScaledFloatStoragePreservesOutOfRangeAlpha() + { + Vector4 source = new(2F, -1F, .5F, 2F); + + Assert.Equal(source, HalfVector4.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, RgbaHalf.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, RgbaVector.FromUnassociatedScaledVector4(source).ToUnassociatedScaledVector4()); + Assert.Equal(source, HalfVector4P.FromAssociatedScaledVector4(source).ToAssociatedScaledVector4()); + Assert.Equal(source, RgbaHalfP.FromAssociatedScaledVector4(source).ToAssociatedScaledVector4()); + Assert.Equal(source, RgbaVectorP.FromAssociatedScaledVector4(source).ToAssociatedScaledVector4()); + } + + /// + /// Bounded destinations saturate finite floating-point values outside the unit interval. + /// + [Fact] + public void FloatToBoundedBulkConversionSaturatesOutOfRangeComponents() + { + Rgba32[] expectedSingle32 = [new(255, 0, 0, 255), new(0, 0, 0, 255), new(0, 0, 0, 255), new(128, 0, 0, 255), new(255, 0, 0, 255)]; + Rgba64[] expectedSingle64 = [new(65535, 0, 0, 65535), new(0, 0, 0, 65535), new(0, 0, 0, 65535), new(32768, 0, 0, 65535), new(65535, 0, 0, 65535)]; + Rgba32[] expectedTwo32 = [new(255, 0, 0, 255), new(0, 255, 0, 255), new(0, 191, 0, 255), new(128, 0, 0, 255), new(255, 0, 0, 255)]; + Rgba64[] expectedTwo64 = [new(65535, 0, 0, 65535), new(0, 65535, 0, 65535), new(0, 49151, 0, 65535), new(32768, 0, 0, 65535), new(65535, 0, 0, 65535)]; + Rgba32[] expectedFour32 = [new(255, 0, 128, 255), new(0, 255, 64, 255), new(0, 191, 255, 255), new(128, 0, 255, 128), new(255, 0, 191, 64)]; + Rgba64[] expectedFour64 = [new(65535, 0, 32768, 65535), new(0, 65535, 16384, 65535), new(0, 49151, 65535, 65535), new(32768, 0, 65535, 32768), new(65535, 0, 49151, 16384)]; + + AssertBoundedConversion(expectedSingle32, expectedSingle64); + AssertBoundedConversion(expectedTwo32, expectedTwo64); + AssertBoundedConversion(expectedFour32, expectedFour64); + AssertBoundedConversion(expectedFour32, expectedFour64); + AssertBoundedConversion(expectedFour32, expectedFour64); + AssertBoundedConversion(expectedFour32, expectedFour64); + AssertBoundedConversion(expectedFour32, expectedFour64); + AssertBoundedConversion(expectedFour32, expectedFour64); + } + + /// + /// Bounded pixel formats saturate nonfinite components in both scalar and bulk conversions. + /// + [Fact] + public void BoundedPixelFormatsSaturateNonfiniteComponents() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertBoundedPixelFormatsSaturateNonfiniteComponents, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks each bounded color or vector destination through its scalar and bulk entry points. + /// + private static void AssertBoundedPixelFormatsSaturateNonfiniteComponents() + { + Vector4 input = new(float.NaN, float.PositiveInfinity, float.NegativeInfinity, 1F); + RgbaVector[] source = new RgbaVector[65]; + Array.Fill(source, RgbaVector.FromUnassociatedScaledVector4(input)); + + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + AssertBoundedSaturation(input, source); + + // An all-NaN color input isolates the lower endpoint of luminance and alpha-only storage. + Vector4 monochromeInput = new(float.NaN, float.NaN, float.NaN, float.PositiveInfinity); + Array.Fill(source, RgbaVector.FromUnassociatedScaledVector4(monochromeInput)); + + AssertBoundedSaturation(monochromeInput, source, Vector4.UnitW); + AssertBoundedSaturation(monochromeInput, source, Vector4.UnitW); + AssertBoundedSaturation(monochromeInput, source, Vector4.UnitW); + AssertBoundedSaturation(monochromeInput, source, Vector4.UnitW); + AssertBoundedSaturation(monochromeInput, source, Vector4.UnitW); + } + + /// + /// Checks a destination's scalar storage and its generic bulk conversion against fixed saturation endpoints. + /// + /// The bounded destination pixel format. + /// The input components. + /// The floating-point source pixels. + private static void AssertBoundedSaturation(Vector4 input, ReadOnlySpan source) + where TPixel : unmanaged, IPixel + => AssertBoundedSaturation(input, source, new Vector4(0F, 1F, 0F, 1F)); + + /// + /// Checks a destination's scalar storage and generic bulk conversion against specified saturation endpoints. + /// + /// The bounded destination pixel format. + /// The input components. + /// The floating-point source pixels. + /// The expected scaled components. + private static void AssertBoundedSaturation(Vector4 input, ReadOnlySpan source, Vector4 expected) + where TPixel : unmanaged, IPixel + { + Assert.Equal(expected, TPixel.FromUnassociatedScaledVector4(input).ToUnassociatedScaledVector4()); + + TPixel[] destination = new TPixel[source.Length]; + PixelOperations.Instance.From(Configuration.Default, source, destination); + + foreach (TPixel pixel in destination) + { + Assert.Equal(expected, pixel.ToUnassociatedScaledVector4()); + } + } + + /// + /// Generic conversion normalizes bounded sources but keeps floating-point values until bounded storage. + /// + [Fact] + public void SinglePixelConversionAppliesScaleAtEachPixelFormat() + { + Byte4[] byteSource = [new Byte4(128F, 0F, 255F, 255F)]; + RgbaVector[] floatFromByte = new RgbaVector[1]; + HalfVector4[] halfSource = [new HalfVector4(0F, 0F, 0F, 1F)]; + RgbaVector[] floatFromHalf = new RgbaVector[1]; + RgbaVector[] hdrSource = [RgbaVector.FromUnassociatedScaledVector4(new Vector4(2.5F, .4F, 0F, 1F))]; + RgbaHalf[] halfFromFloat = new RgbaHalf[1]; + Rgba32[] boundedFromFloat = new Rgba32[1]; + + PixelOperations.Instance.From(Configuration.Default, byteSource, floatFromByte); + PixelOperations.Instance.From(Configuration.Default, halfSource, floatFromHalf); + PixelOperations.Instance.From(Configuration.Default, hdrSource, halfFromFloat); + PixelOperations.Instance.From(Configuration.Default, hdrSource, boundedFromFloat); + + Assert.Equal(128F / 255F, floatFromByte[0].ToScaledVector4().X); + Assert.Equal(0F, floatFromHalf[0].ToScaledVector4().X); + Assert.Equal(2.5F, halfFromFloat[0].ToScaledVector4().X); + Assert.Equal(new Rgba32(255, 102, 0, 255), boundedFromFloat[0]); + } + + /// + /// Bulk conversion changes alpha representation once and preserves out-of-range color values. + /// + [Fact] + public void BulkConversionChangesAlphaRepresentationOnce() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertBulkConversionChangesAlphaRepresentationOnce, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks the association result and its inverse using distinct values in adjacent pixels. + /// + private static void AssertBulkConversionChangesAlphaRepresentationOnce() + { + Vector4[] inputs = + [ + new(2F, -.5F, .25F, .5F), + new(0F, 3F, -1F, .25F), + new(-4F, 0F, .5F, 1F), + new(1F, 2F, 3F, 2F), + new(5F, -2F, 1F, .125F) + ]; + + Vector4[] expectedAssociated = + [ + new(1F, -.25F, .125F, .5F), + new(0F, .75F, -.25F, .25F), + new(-4F, 0F, .5F, 1F), + new(2F, 4F, 6F, 2F), + new(.625F, -.25F, .125F, .125F) + ]; + + RgbaVector[] straight = new RgbaVector[17]; + RgbaHalfP[] associated = new RgbaHalfP[straight.Length]; + RgbaVector[] restored = new RgbaVector[straight.Length]; + + for (int i = 0; i < straight.Length; i++) + { + straight[i] = RgbaVector.FromUnassociatedScaledVector4(inputs[i % inputs.Length]); + } + + PixelOperations.Instance.From(Configuration.Default, straight, associated); + PixelOperations.Instance.From(Configuration.Default, associated, restored); + + for (int i = 0; i < straight.Length; i++) + { + Assert.Equal(expectedAssociated[i % expectedAssociated.Length], associated[i].ToAssociatedScaledVector4()); + Assert.Equal(inputs[i % inputs.Length], restored[i].ToUnassociatedScaledVector4()); + } + } + + /// + /// Matching associated representations keep stored color even when alpha is zero. + /// Straight input is multiplied by zero when stored as associated color. + /// + [Fact] + public void BulkConversionAtZeroAlphaFollowsSourceRepresentation() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertBulkConversionAtZeroAlphaFollowsSourceRepresentation, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks both alpha-zero rules with different stored colors in adjacent pixels. + /// + private static void AssertBulkConversionAtZeroAlphaFollowsSourceRepresentation() + { + Vector4[] inputs = + [ + new(2F, -1F, .5F, 0F), + new(-3F, 4F, .25F, 0F), + new(.125F, .375F, .75F, 0F), + new(0F, 1F, 2F, 0F), + new(1F, 0F, -.5F, 0F) + ]; + + RgbaHalfP[] associatedSource = new RgbaHalfP[17]; + HalfVector4P[] associatedDestination = new HalfVector4P[associatedSource.Length]; + RgbaVector[] straightSource = new RgbaVector[associatedSource.Length]; + RgbaHalfP[] multipliedDestination = new RgbaHalfP[associatedSource.Length]; + + for (int i = 0; i < associatedSource.Length; i++) + { + Vector4 value = inputs[i % inputs.Length]; + associatedSource[i] = RgbaHalfP.FromAssociatedScaledVector4(value); + straightSource[i] = RgbaVector.FromUnassociatedScaledVector4(value); + } + + PixelOperations.Instance.From(Configuration.Default, associatedSource, associatedDestination); + PixelOperations.Instance.From(Configuration.Default, straightSource, multipliedDestination); + + for (int i = 0; i < associatedSource.Length; i++) + { + Assert.Equal(inputs[i % inputs.Length], associatedDestination[i].ToAssociatedScaledVector4()); + Assert.Equal(Vector4.Zero, multipliedDestination[i].ToAssociatedScaledVector4()); + } + } + + /// + /// Bounded storage applies its existing nonfinite rules to floating-point input. + /// + [Fact] + public void FloatToBoundedSpanConversionHandlesNonfiniteComponents() + { + Vector4[] inputs = + [ + new(float.NaN, float.PositiveInfinity, float.NegativeInfinity, float.PositiveInfinity), + new(float.PositiveInfinity, float.NaN, float.NegativeInfinity, 1F), + new(float.NegativeInfinity, float.PositiveInfinity, float.NaN, 0F), + new(2F, -1F, .5F, 1F), + new(.25F, .75F, 0F, .5F) + ]; + + Rgba32[] expected32 = [new(0, 255, 0, 255), new(255, 0, 0, 255), new(0, 255, 0, 0), new(255, 0, 128, 255), new(64, 191, 0, 128)]; + Rgba64[] expected64 = [new(0, 65535, 0, 65535), new(65535, 0, 0, 65535), new(0, 65535, 0, 0), new(65535, 0, 32768, 65535), new(16384, 49151, 0, 32768)]; + RgbaVector[] source = new RgbaVector[17]; + Rgba32[] destination32 = new Rgba32[source.Length]; + Rgba64[] destination64 = new Rgba64[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = RgbaVector.FromUnassociatedScaledVector4(inputs[i % inputs.Length]); + } + + PixelOperations.Instance.From(Configuration.Default, source, destination32); + PixelOperations.Instance.From(Configuration.Default, source, destination64); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(expected32[i % expected32.Length], destination32[i]); + Assert.Equal(expected64[i % expected64.Length], destination64[i]); + } + } + + /// + /// Float-to-float bulk conversion keeps IEEE special values and the sign of zero. + /// + [Fact] + public void FloatToHalfBulkConversionPreservesSpecialValues() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertFloatToHalfBulkConversionPreservesSpecialValues, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks special-value conversion at each supported instruction width. + /// + private static void AssertFloatToHalfBulkConversionPreservesSpecialValues() + { + Vector4[] inputs = + [ + new(-0F, float.PositiveInfinity, float.NegativeInfinity, float.NaN), + new(2.5F, -1F, .5F, 1F), + new(0F, 65504F, -65504F, 0F), + new(.25F, .75F, 1.5F, 2F), + new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, -0F) + ]; + + RgbaVector[] source = new RgbaVector[17]; + HalfVector4[] destination = new HalfVector4[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = RgbaVector.FromUnassociatedScaledVector4(inputs[i % inputs.Length]); + } + + PixelOperations.Instance.From(Configuration.Default, source, destination); + + for (int i = 0; i < destination.Length; i++) + { + ulong packed = destination[i].PackedValue; + + // NaN payload bits are not fixed; mask only that component and check its classification. + switch (i % inputs.Length) + { + case 0: + Assert.Equal(0x0000FC007C008000UL, packed & 0x0000FFFFFFFFFFFFUL); + Assert.True(float.IsNaN(destination[i].ToVector4().W)); + break; + case 1: + Assert.Equal(0x3C003800BC004100UL, packed); + break; + case 2: + Assert.Equal(0x0000FBFF7BFF0000UL, packed); + break; + case 3: + Assert.Equal(0x40003E003A003400UL, packed); + break; + default: + Assert.Equal(0x80000000FC007C00UL, packed & 0xFFFF0000FFFFFFFFUL); + Assert.True(float.IsNaN(destination[i].ToVector4().Z)); + break; + } + } + } + + /// + /// Scalar and SIMD half-precision packing use nearest-even rounding, overflow to infinity, and signed zero. + /// + [Fact] + public void HalfVector4BulkPackingMatchesBinary16BoundaryBits() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertHalfVector4BulkPackingMatchesBinary16BoundaryBits, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Checks half-precision boundary bits at each supported instruction width. + /// + private static void AssertHalfVector4BulkPackingMatchesBinary16BoundaryBits() + { + Vector4[] inputs = + [ + new(1.00048828125F, 65504F, 65520F, -0F), + new(-1.00048828125F, -65504F, -65520F, 0F), + new(.5F, -.5F, 2F, -2F), + new(0F, 1F, -1F, 4F), + new(.25F, .75F, 1.5F, -4F) + ]; + + ulong[] expectedBits = + [ + 0x80007C007BFF3C00, + 0x0000FC00FBFFBC00, + 0xC0004000B8003800, + 0x4400BC003C000000, + 0xC4003E003A003400 + ]; + + Vector4[] source = new Vector4[17]; + HalfVector4[] actual = new HalfVector4[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = inputs[i % inputs.Length]; + } + + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + for (int i = 0; i < actual.Length; i++) + { + Assert.Equal(expectedBits[i % expectedBits.Length], actual[i].PackedValue); + } + } + + /// + /// HalfSingle preserves scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfSingle_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// HalfVector2 preserves scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector2_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// HalfVector4 preserves scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector4_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// HalfVector4P preserves scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector4P_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// RgbaVector preserves scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaVector_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// RgbaHalf preserves scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaHalf_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// RgbaHalfP preserves scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaHalfP_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// Associated binary32 pixels preserve unassociated scaled input in scalar and bulk conversions. + /// + [Fact] + public void RgbaVectorP_ScaledInputIsPreserved() => AssertScaledInputIsPreserved(); + + /// + /// Raw half storage and scaled vectors preserve IEEE special values. + /// + [Fact] + public void HalfVector4_ScaledOutputPreservesSpecialValues() => AssertScaledOutputPreservesSpecialValues(); + + /// + /// Associated half-vector conversion preserves the stored component values. + /// + [Fact] + public void HalfVector4P_AssociatedScaledInputIsPreserved() => AssertAssociatedScaledInputIsPreserved(); + + /// + /// Associated half-RGBA conversion preserves the stored component values. + /// + [Fact] + public void RgbaHalfP_AssociatedScaledInputIsPreserved() => AssertAssociatedScaledInputIsPreserved(); + + /// + /// Associated binary32 pixels preserve stored values in scalar and bulk conversions. + /// + [Fact] + public void RgbaVectorP_AssociatedScaledInputIsPreserved() => AssertAssociatedScaledInputIsPreserved(); + + /// + /// Checks that the scaled modifier does not change finite floating-point input. + /// + /// The destination pixel format. + private static void AssertScaledInputIsPreserved() + where TPixel : unmanaged, IPixel + { + Vector4[] inputs = + [ + new(2.5F, -2F, .5F, 1F), + new(.25F, .5F, .75F, .5F), + new(-3F, 4F, .25F, 0F), + new(65504F, -65504F, 0F, 1F) + ]; + + // Seventeen pixels exercise wide registers and the narrower remainder paths. + Vector4[] source = new Vector4[17]; + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actual = new TPixel[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int sample = i % inputs.Length; + source[i] = inputs[sample]; + expected[i] = TPixel.FromUnassociatedVector4(source[i]); + Assert.Equal(expected[i], TPixel.FromUnassociatedScaledVector4(source[i])); + } + + // Associated formats otherwise interpret the vectors using their native alpha representation. + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + Assert.Equal(expected, actual); + } + + /// + /// Checks the bounded result through the generic bulk conversion entry points. + /// + /// The floating-point source pixel format. + /// The expected eight-bit pixels for each input value. + /// The expected sixteen-bit pixels for each input value. + private static void AssertBoundedConversion(Rgba32[] expected32, Rgba64[] expected64) + where TPixel : unmanaged, IPixel + { + Vector4[] inputs = + [ + new(2F, -1F, .5F, 1F), + new(-3F, 3F, .25F, 2F), + new(0F, .75F, 1.5F, 1F), + new(.5F, 0F, 1F, .5F), + new(1F, 0F, .75F, .25F) + ]; + + TPixel[] source = new TPixel[17]; + Rgba32[] destination32 = new Rgba32[source.Length]; + Rgba64[] destination64 = new Rgba64[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + // Five samples shift between four-, eight-, and sixteen-pixel blocks. + source[i] = TPixel.FromUnassociatedScaledVector4(inputs[i % inputs.Length]); + } + + PixelOperations.Instance.From(Configuration.Default, source, destination32); + PixelOperations.Instance.From(Configuration.Default, source, destination64); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(expected32[i % expected32.Length], destination32[i]); + Assert.Equal(expected64[i % expected64.Length], destination64[i]); + } + } + + /// + /// Checks that the scaled modifier does not change associated floating-point input. + /// + /// The associated destination pixel format. + private static void AssertAssociatedScaledInputIsPreserved() + where TPixel : unmanaged, IPixel + { + Vector4[] inputs = + [ + new(2.5F, -2F, .5F, 1F), + new(1F, .5F, 1.5F, 2F), + new(.125F, .25F, .375F, .5F), + new(-3F, 4F, .25F, 0F) + ]; + + Vector4[] source = new Vector4[17]; + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actual = new TPixel[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int sample = i % inputs.Length; + source[i] = inputs[sample]; + expected[i] = TPixel.FromAssociatedVector4(source[i]); + Assert.Equal(expected[i], TPixel.FromAssociatedScaledVector4(source[i])); + } + + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + Assert.Equal(expected, actual); + } + + /// + /// Checks native storage and scaled output independently of integer conversion semantics. + /// + private static void AssertScaledOutputPreservesSpecialValues() + { + Vector4 native = new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 65504F); + HalfVector4 pixel = HalfVector4.FromVector4(native); + Assert.True(float.IsPositiveInfinity(pixel.ToVector4().X)); + Assert.True(float.IsNegativeInfinity(pixel.ToVector4().Y)); + Assert.True(float.IsNaN(pixel.ToVector4().Z)); + + Vector4 expected = pixel.ToVector4(); + Assert.True(float.IsPositiveInfinity(expected.X)); + Assert.True(float.IsNegativeInfinity(expected.Y)); + Assert.True(float.IsNaN(expected.Z)); + Assert.Equal(65504F, expected.W); + Assert.True(float.IsPositiveInfinity(new HalfSingle(float.PositiveInfinity).ToScaledVector4().X)); + Assert.True(float.IsNaN(new HalfSingle(float.NaN).ToScaledVector4().X)); + Vector4 halfVector2 = new HalfVector2(new Vector2(float.PositiveInfinity, float.NaN)).ToScaledVector4(); + Assert.True(float.IsPositiveInfinity(halfVector2.X)); + Assert.True(float.IsNaN(halfVector2.Y)); + + } +} diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs index 3e06f04714..634fc6876b 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs @@ -42,29 +42,29 @@ public void HalfSingle_ToVector4() [Fact] public void HalfSingle_ToScaledVector4() { - Assert.Equal(new Vector4(0F, 0F, 0F, 1F), new HalfSingle((float)Half.MinValue).ToScaledVector4()); - Assert.Equal(new Vector4(.5F, 0F, 0F, 1F), new HalfSingle(0F).ToScaledVector4()); - Assert.Equal(new Vector4(1F, 0F, 0F, 1F), new HalfSingle((float)Half.MaxValue).ToScaledVector4()); + Assert.Equal(new Vector4((float)Half.MinValue, 0F, 0F, 1F), new HalfSingle((float)Half.MinValue).ToScaledVector4()); + Assert.Equal(new Vector4(0F, 0F, 0F, 1F), new HalfSingle(0F).ToScaledVector4()); + Assert.Equal(new Vector4((float)Half.MaxValue, 0F, 0F, 1F), new HalfSingle((float)Half.MaxValue).ToScaledVector4()); } [Fact] public void HalfSingle_FromScaledVector4() { - Assert.Equal((ushort)0xFBFF, HalfSingle.FromScaledVector4(new Vector4(0F, 0F, 0F, 1F)).PackedValue); - Assert.Equal((ushort)0, HalfSingle.FromScaledVector4(new Vector4(.5F, 0F, 0F, 1F)).PackedValue); - Assert.Equal((ushort)0x7BFF, HalfSingle.FromScaledVector4(new Vector4(1F, 0F, 0F, 1F)).PackedValue); + Assert.Equal((ushort)0xFBFF, HalfSingle.FromScaledVector4(new Vector4((float)Half.MinValue, 0F, 0F, 1F)).PackedValue); + Assert.Equal((ushort)0, HalfSingle.FromScaledVector4(new Vector4(0F, 0F, 0F, 1F)).PackedValue); + Assert.Equal((ushort)0x7BFF, HalfSingle.FromScaledVector4(new Vector4((float)Half.MaxValue, 0F, 0F, 1F)).PackedValue); } [Fact] - public void HalfSingle_BulkScaledConversionsCoverFiniteRange() => + public void HalfSingle_BulkScaledConversionsPreserveFiniteRange() => FeatureTestRunner.RunWithHwIntrinsicsFeature( - AssertHalfSingleBulkScaledConversionsCoverFiniteRange, + AssertHalfSingleBulkScaledConversionsPreserveFiniteRange, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - private static void AssertHalfSingleBulkScaledConversionsCoverFiniteRange() + private static void AssertHalfSingleBulkScaledConversionsPreserveFiniteRange() { ushort[] packedValues = [0xFBFF, 0, 0x7BFF]; - Vector4[] scaledValues = [new(0F, 0F, 0F, 1F), new(.5F, 0F, 0F, 1F), new(1F, 0F, 0F, 1F)]; + Vector4[] scaledValues = [new((float)Half.MinValue, 0F, 0F, 1F), new(0F, 0F, 0F, 1F), new((float)Half.MaxValue, 0F, 0F, 1F)]; HalfSingle[] source = new HalfSingle[17]; Vector4[] expectedVectors = new Vector4[source.Length]; diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs index 1fde34a826..f92c54a325 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs @@ -40,8 +40,8 @@ public void HalfVector2_ToScaledVector4() Vector4 actual = halfVector.ToScaledVector4(); // assert - Assert.Equal(0F, actual.X); - Assert.Equal(1F, actual.Y); + Assert.Equal((float)Half.MinValue, actual.X); + Assert.Equal((float)Half.MaxValue, actual.Y); Assert.Equal(0, actual.Z); Assert.Equal(1, actual.W); } @@ -50,7 +50,7 @@ public void HalfVector2_ToScaledVector4() public void HalfVector2_FromScaledVector4() { // arrange - Vector4 scaled = new(0F, 1F, 0F, 1F); + Vector4 scaled = new((float)Half.MinValue, (float)Half.MaxValue, 0F, 1F); const uint expected = 0x7BFF_FBFF; // act @@ -62,18 +62,18 @@ public void HalfVector2_FromScaledVector4() } [Fact] - public void HalfVector2_BulkScaledConversionsCoverFiniteRange() => + public void HalfVector2_BulkScaledConversionsPreserveFiniteRange() => FeatureTestRunner.RunWithHwIntrinsicsFeature( - AssertHalfVector2BulkScaledConversionsCoverFiniteRange, + AssertHalfVector2BulkScaledConversionsPreserveFiniteRange, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - private static void AssertHalfVector2BulkScaledConversionsCoverFiniteRange() + private static void AssertHalfVector2BulkScaledConversionsPreserveFiniteRange() { HalfVector2 pixel = new() { PackedValue = 0x7BFF_FBFF }; HalfVector2[] source = new HalfVector2[17]; Vector4[] expectedVectors = new Vector4[source.Length]; Array.Fill(source, pixel); - Array.Fill(expectedVectors, new Vector4(0F, 1F, 0F, 1F)); + Array.Fill(expectedVectors, new Vector4((float)Half.MinValue, (float)Half.MaxValue, 0F, 1F)); Vector4[] actualVectors = new Vector4[source.Length]; PixelOperations.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs index 75130c144c..0488378aa9 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs @@ -44,7 +44,7 @@ public void HalfVector4_ToVector4() public void HalfVector4_ToScaledVector4() { // arrange - Vector4 expected = new(0F, .5F, 1F, 0F); + Vector4 expected = new((float)Half.MinValue, 0F, (float)Half.MaxValue, (float)Half.MinValue); HalfVector4 pixel = new((float)Half.MinValue, 0F, (float)Half.MaxValue, (float)Half.MinValue); // act @@ -58,8 +58,8 @@ public void HalfVector4_ToScaledVector4() public void HalfVector4_FromScaledVector4() { // arrange - Vector4 scaled = new(0F, .25F, .5F, 1F); - ulong expected = new HalfVector4((float)Half.MinValue, -32752F, 0F, (float)Half.MaxValue).PackedValue; + Vector4 scaled = new((float)Half.MinValue, -32752F, 0F, (float)Half.MaxValue); + ulong expected = new HalfVector4(scaled).PackedValue; // act HalfVector4 pixel = HalfVector4.FromScaledVector4(scaled); @@ -70,19 +70,19 @@ public void HalfVector4_FromScaledVector4() } [Fact] - public void HalfVector4_BulkScaledConversionsCoverFiniteRange() => + public void HalfVector4_BulkScaledConversionsPreserveFiniteRange() => FeatureTestRunner.RunWithHwIntrinsicsFeature( - AssertHalfVector4BulkScaledConversionsCoverFiniteRange, + AssertHalfVector4BulkScaledConversionsPreserveFiniteRange, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - private static void AssertHalfVector4BulkScaledConversionsCoverFiniteRange() + private static void AssertHalfVector4BulkScaledConversionsPreserveFiniteRange() { const ulong packedValue = 0xFBFF_7BFF_0000_FBFF; HalfVector4 pixel = new() { PackedValue = packedValue }; HalfVector4[] source = new HalfVector4[17]; Vector4[] expectedVectors = new Vector4[source.Length]; Array.Fill(source, pixel); - Array.Fill(expectedVectors, new Vector4(0F, .5F, 1F, 0F)); + Array.Fill(expectedVectors, new Vector4((float)Half.MinValue, 0F, (float)Half.MaxValue, (float)Half.MinValue)); Vector4[] actualVectors = new Vector4[source.Length]; PixelOperations.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs index e6be539c9a..870d9d4ff2 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs @@ -102,6 +102,7 @@ public void NormalizedByte2_FromScaledVector4() // assert Assert.Equal(expected, actual); + Assert.Equal(0x7F81U, NormalizedByte2.FromScaledVector4(new Vector4(float.NaN, float.PositiveInfinity, 0F, 1F)).PackedValue); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs index 9cb3aee508..87caec9536 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs @@ -106,6 +106,7 @@ public void NormalizedShort2_FromScaledVector4() // assert Assert.Equal(expected, actual); + Assert.Equal(0x7FFF8001U, NormalizedShort2.FromScaledVector4(new Vector4(float.NaN, float.PositiveInfinity, 0F, 1F)).PackedValue); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs index c317586e48..37b59e177d 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs @@ -93,6 +93,14 @@ private static void AssertNormalizedShort4MinimumStorageCodeDecodesAsNegativeOne NormalizedShort4[] actualPixels = new NormalizedShort4[source.Length]; PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); Assert.All(actualPixels, actual => Assert.Equal(0x8001800180018001UL, actual.PackedValue)); + + Vector4 special = new(float.NaN, float.PositiveInfinity, float.NegativeInfinity, 2F); + const ulong expectedSpecial = 0x7FFF80017FFF8001UL; + Assert.Equal(expectedSpecial, NormalizedShort4.FromScaledVector4(special).PackedValue); + + Array.Fill(destructiveSource, special); + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(expectedSpecial, actual.PackedValue)); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs index 2cc26e76ca..17cede398c 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs @@ -389,20 +389,20 @@ public void NormalizedShort4NativeConversionsUseScaledAlpha() => AssertSignedNormalizedNativeConversions(static vector => (vector * 2F) - Vector4.One); [Fact] - public void HalfVector4NativeConversionsUseScaledAlpha() - => AssertNativeConversions(static vector => (vector * 131008F) - new Vector4(65504F)); + public void HalfVector4NativeConversionsUseStoredAlpha() + => AssertNativeConversions(static vector => vector); [Fact] - public void HalfVector4PNativeConversionsUseScaledAlpha() - => AssertNativeConversions(static vector => (vector * 131008F) - new Vector4(65504F)); + public void HalfVector4PNativeConversionsUseStoredAlpha() + => AssertNativeConversions(static vector => vector); [Fact] - public void HalfSingleFromAssociatedNativeVectorUsesScaledAlpha() - => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 131008F) - 65504F, 0F, 0F, vector.W)); + public void HalfSingleFromAssociatedNativeVectorUsesStoredAlpha() + => AssertAlphaLessNativeFrom(static vector => vector); [Fact] - public void HalfVector2FromAssociatedNativeVectorUsesScaledAlpha() - => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 131008F) - 65504F, (vector.Y * 131008F) - 65504F, 0F, vector.W)); + public void HalfVector2FromAssociatedNativeVectorUsesStoredAlpha() + => AssertAlphaLessNativeFrom(static vector => vector); [Fact] public void NormalizedByte2FromAssociatedNativeVectorUsesScaledAlpha() diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs b/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs index dbff541407..99b95d68ae 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs @@ -91,14 +91,19 @@ internal static void To( return; } - // Scalar source and destination boundaries are the reference for the bulk operation: each format owns any representation conversion and destination quantization it requires. + bool bothAssociated = TSourcePixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated + && TDestinationPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated; + + // Keep associated values associated when both formats store them, including stored color at zero alpha. + // Every other pair crosses the scalar unassociated boundary before destination storage. ref TDestinationPixel destRef = ref MemoryMarshal.GetReference(destinationPixels); for (int i = 0; i < count; i++) { ref TSourcePixel sp = ref Unsafe.Add(ref sourceRef, i); ref TDestinationPixel dp = ref Unsafe.Add(ref destRef, i); - Vector4 vector = sp.ToUnassociatedScaledVector4(); - dp = TDestinationPixel.FromUnassociatedScaledVector4(vector); + dp = bothAssociated + ? TDestinationPixel.FromAssociatedScaledVector4(sp.ToAssociatedScaledVector4()) + : TDestinationPixel.FromUnassociatedScaledVector4(sp.ToUnassociatedScaledVector4()); } } } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs index 0fa34af39c..3966f0ff10 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs @@ -20,6 +20,36 @@ public partial class PixelOperationsTests [WithBlankImages(1, 1, PixelTypes.All)] public void GetGlobalInstance(TestImageProvider _) where T : unmanaged, IPixel => Assert.NotNull(PixelOperations.Instance); + + /// + /// Exercises the shared pixel operations for 32-bit unsigned three-component storage. + /// + public sealed class Rgb96_OperationsTests : PixelOperationsTests + { + /// + /// Initializes a new instance of the class. + /// + /// The test output. + public Rgb96_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + } + + /// + /// Exercises the shared pixel operations for 32-bit unsigned four-component storage. + /// + public sealed class Rgba128_OperationsTests : PixelOperationsTests + { + /// + /// Initializes a new instance of the class. + /// + /// The test output. + public Rgba128_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + } } public abstract class PixelOperationsTests : MeasureFixture @@ -348,7 +378,9 @@ public void ToVector4(int count) new TestPixel(), new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), @@ -368,6 +400,28 @@ public void Generic_To(TestPixel _) TPixel[] source = CreatePixelTestData(count); TDestPixel[] expected = new TDestPixel[count]; + // These samples retain HDR and IEEE values in floating-point sources while bounded sources saturate at construction. + Vector4[] boundaryValues = + [ + new(0F, .4F, 1F, 1F), + new(2.5F, -1F, .5F, 1F), + new(float.NaN, float.NegativeInfinity, float.PositiveInfinity, 1F), + new(-0F, 0F, 1F, 0F), + new(1F, 1F, 1F, float.PositiveInfinity), + new(0F, 0F, 0F, float.NaN) + ]; + + for (int i = 0; i < boundaryValues.Length; i++) + { + source[i] = TPixel.FromUnassociatedScaledVector4(boundaryValues[i]); + } + + if (this.HasAssociatedAlpha) + { + // Associated floating-point formats can store color with zero alpha; matching destinations must retain it. + source[boundaryValues.Length] = TPixel.FromAssociatedScaledVector4(new Vector4(2F, -1F, .5F, 0F)); + } + PixelConverterTests.ReferenceImplementations.To(source, expected); TestOperation(source, expected, (s, d) => this.Operations.To(this.Configuration, s, d.GetSpan()), false); diff --git a/tests/ImageSharp.Tests/PixelFormats/Rg32Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Rg32Tests.cs index b2790469a1..1278cc25fe 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Rg32Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Rg32Tests.cs @@ -80,6 +80,7 @@ public void Rg32_Clamping() { Assert.Equal(Vector2.Zero, new Rg32(Vector2.One * -1234.0f).ToVector2()); Assert.Equal(Vector2.One, new Rg32(Vector2.One * 1234.0f).ToVector2()); + Assert.Equal(0xFFFF0000U, Rg32.FromScaledVector4(new Vector4(float.NaN, float.PositiveInfinity, 0F, 1F)).PackedValue); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs b/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs index 9b22a9f8c8..bc1dc5922c 100644 --- a/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs @@ -10,13 +10,13 @@ namespace SixLabors.ImageSharp.Tests.PixelFormats; /// -/// Tests the unit-range binary16 RGBA pixel formats. +/// Tests the half-precision RGBA pixel formats. /// [Trait("Category", "PixelFormats")] public class RgbaHalfTests { /// - /// Verifies that the unassociated format has the native layout required by RGBA binary16 surfaces. + /// Verifies that the unassociated format has the native layout required by RGBA half-precision surfaces. /// [Fact] public void RgbaHalfHasRgbaBinary16Layout() @@ -34,7 +34,7 @@ public void RgbaHalfHasRgbaBinary16Layout() } /// - /// Verifies that zero-filled binary16 storage represents transparent black without affine remapping. + /// Verifies that zero-filled half-precision storage represents transparent black without affine remapping. /// [Fact] public void RgbaHalfDefaultIsTransparentBlack() @@ -46,18 +46,18 @@ public void RgbaHalfDefaultIsTransparentBlack() } /// - /// Verifies that scaled input is clamped to the pixel format's unit color range. + /// Verifies that scaled input retains finite values outside the unit interval. /// [Fact] - public void RgbaHalfFromScaledVector4ClampsToUnitRange() + public void RgbaHalfFromScaledVector4PreservesHdrValues() { RgbaHalf pixel = RgbaHalf.FromScaledVector4(new Vector4(-1F, .5F, 2F, 1F)); - Assert.Equal(new Vector4(0F, .5F, 1F, 1F), pixel.ToScaledVector4()); + Assert.Equal(new Vector4(-1F, .5F, 2F, 1F), pixel.ToScaledVector4()); } /// - /// Verifies that the associated format stores associated binary16 components in the same RGBA order. + /// Verifies that the associated format stores associated half-precision components in the same RGBA order. /// [Fact] public void RgbaHalfPHasAssociatedRgbaBinary16Layout() @@ -74,7 +74,7 @@ public void RgbaHalfPHasAssociatedRgbaBinary16Layout() } /// - /// Verifies that zero-filled associated binary16 storage represents transparent black. + /// Verifies that zero-filled associated half-precision storage represents transparent black. /// [Fact] public void RgbaHalfPDefaultIsTransparentBlack() @@ -87,7 +87,7 @@ public void RgbaHalfPDefaultIsTransparentBlack() } /// - /// Verifies that association uses the alpha value that survives binary16 quantization. + /// Verifies that association uses the alpha value that survives half-precision quantization. /// [Fact] public void RgbaHalfPQuantizesAlphaBeforeAssociation() @@ -135,7 +135,7 @@ public void RgbaHalfPBulkConversionsMatchScalarAcrossHardwareWidths() /// /// Compares bulk conversions with the corresponding scalar pixel operations for representative vector widths and remainders. /// - /// The binary16 pixel format to test. + /// The half-precision pixel format to test. private static void AssertBulkConversionsMatchScalar() where TPixel : unmanaged, IPixel { @@ -252,7 +252,7 @@ private static TPixel FromScalarVector(Vector4 vector, bool associated, }; /// - /// Verifies the component layout and alpha metadata exposed by a binary16 pixel format. + /// Verifies the component layout and alpha metadata exposed by a half-precision pixel format. /// /// The pixel format to inspect. /// The expected alpha representation. diff --git a/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs index 86fc828304..7788e6ef1e 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs @@ -47,6 +47,7 @@ public void Short2_Clamping() { Assert.Equal(Vector2.One * 0x7FFF, new Short2(Vector2.One * 1234567.0f).ToVector2()); Assert.Equal(Vector2.One * -0x8000, new Short2(Vector2.One * -1234567.0f).ToVector2()); + Assert.Equal(0x7FFF8000U, Short2.FromScaledVector4(new Vector4(float.NaN, float.PositiveInfinity, 0F, 1F)).PackedValue); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs index c8502462df..8edc1b2192 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs @@ -77,6 +77,14 @@ private static void AssertShort4BulkScaledConversionsCoverFullSignedRange() Short4[] actualPixels = new Short4[length]; PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); Assert.Equal(source, actualPixels); + + Vector4 special = new(float.NaN, float.PositiveInfinity, float.NegativeInfinity, 2F); + const ulong expectedSpecial = 0x7FFF80007FFF8000UL; + Assert.Equal(expectedSpecial, Short4.FromScaledVector4(special).PackedValue); + + Array.Fill(destructiveSource, special); + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(expectedSpecial, actual.PackedValue)); } [Fact] diff --git a/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs index d5f7f69d99..c61025d088 100644 --- a/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs @@ -43,17 +43,17 @@ public void Brightness_scaled_vector() Assert.Equal(new Rgb24(20, 20, 20), rgbImage[0, 0]); - // HalfSingle normalizes the complete finite binary16 interval, making -65504 logical zero and -32752 logical .25. + // Floating-point Scale preserves the sample, so brightness doubles it before storing it as Half. Image halfSingleImage = new(Configuration.Default, 100, 100, new HalfSingle((float)Half.MinValue)); halfSingleImage.Mutate(x => x.ApplyProcessor(new BrightnessProcessor(2))); - Assert.Equal(new HalfSingle((float)Half.MinValue), halfSingleImage[0, 0]); + Assert.Equal(new HalfSingle(float.NegativeInfinity), halfSingleImage[0, 0]); halfSingleImage = new Image(Configuration.Default, 100, 100, new HalfSingle(-32752F)); halfSingleImage.Mutate(x => x.ApplyProcessor(new BrightnessProcessor(2))); - Assert.Equal(new HalfSingle(0), halfSingleImage[0, 0]); + Assert.Equal(new HalfSingle((float)Half.MinValue), halfSingleImage[0, 0]); } } diff --git a/tests/ImageSharp.Tests/Processing/Normalization/AutoLevelProcessorTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/AutoLevelProcessorTests.cs new file mode 100644 index 0000000000..c6100d904a --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Normalization/AutoLevelProcessorTests.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Normalization; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.Processing.Normalization; + +[Trait("Category", "Processors")] +public class AutoLevelProcessorTests +{ + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); + + /// + /// The one-call operation expands a simple grayscale range while preserving alpha. + /// + [Fact] + public void AutoLevel_AdjustsImageWithOneCall() + { + using Image image = new(3, 1); + image[0, 0] = new RgbaVector(0F, 0F, 0F, 0.625F); + image[1, 0] = new RgbaVector(0.25F, 0.25F, 0.25F, 0.625F); + image[2, 0] = new RgbaVector(0.5F, 0.5F, 0.5F, 0.625F); + + image.Mutate(ctx => ctx.AutoLevel()); + + Assert.Equal(new RgbaVector(0F, 0F, 0F, 0.625F), image[0, 0]); + Assert.Equal(new RgbaVector(0.5F, 0.5F, 0.5F, 0.625F), image[1, 0]); + Assert.Equal(new RgbaVector(1F, 1F, 1F, 0.625F), image[2, 0]); + } + + /// + /// Separate-channel CDF lookups use the same endpoint bins for out-of-range and nonfinite samples. + /// + [Fact] + public void SeparateChannels_FloatingSamplesMatchBoundedIndexReference() + { + float[] samples = [float.NaN, float.NegativeInfinity, -1F, 0F, 0.25F, 0.75F, 2F, float.PositiveInfinity]; + float[] bounded = [0F, 0F, 0F, 0F, 0.25F, 0.75F, 1F, 1F]; + using Image actual = new(17, 17); + using Image reference = new(17, 17); + + for (int y = 0; y < actual.Height; y++) + { + for (int x = 0; x < actual.Width; x++) + { + int sample = (x + y) % samples.Length; + actual[x, y] = new RgbaVector(samples[sample], samples[sample], samples[sample], 0.625F); + reference[x, y] = new RgbaVector(bounded[sample], bounded[sample], bounded[sample], 0.625F); + } + } + + AutoLevelProcessor processor = new(5, false, 350, false); + + actual.Mutate(ctx => ctx.ApplyProcessor(processor)); + reference.Mutate(ctx => ctx.ApplyProcessor(processor)); + + for (int y = 0; y < actual.Height; y++) + { + for (int x = 0; x < actual.Width; x++) + { + Assert.Equal(reference[x, y], actual[x, y]); + } + } + } + + /// + /// Synchronized AutoLevel uses the endpoint CDF entry while retaining the original HDR color for scaling. + /// + [Fact] + public void SynchronizedChannels_UsesEndpointIndexWithoutClampingHdrColor() + { + float[] samples = [float.NaN, float.NegativeInfinity, 0.25F, 0.5F, 0.75F, 2F, float.PositiveInfinity]; + using Image image = new(samples.Length, 1); + + for (int x = 0; x < samples.Length; x++) + { + image[x, 0] = new RgbaVector(samples[x], samples[x], samples[x], 0.625F); + } + + image.Mutate(ctx => ctx.ApplyProcessor(new AutoLevelProcessor(5, false, 350, true))); + + Assert.Equal(new RgbaVector(2.5F, 2.5F, 2.5F, 0.625F), image[5, 0]); + Assert.True(float.IsPositiveInfinity(image[6, 0].ToVector4().X)); + } + + /// + /// Synchronized AutoLevel maps black without division and scales HDR color from the endpoint CDF entry. + /// + [Fact] + public void SynchronizedChannels_MapsBlackAndHdrColor() + { + using Image image = new(3, 1); + image[0, 0] = new RgbaVector(0F, 0F, 0F, 0.625F); + image[1, 0] = new RgbaVector(0.5F, 0.5F, 0.5F, 0.625F); + image[2, 0] = new RgbaVector(2F, 2F, 2F, 0.625F); + + image.Mutate(ctx => ctx.ApplyProcessor(new AutoLevelProcessor(5, false, 350, true))); + + Assert.Equal(new RgbaVector(0F, 0F, 0F, 0.625F), image[0, 0]); + Assert.Equal(new RgbaVector(0.625F, 0.625F, 0.625F, 0.625F), image[1, 0]); + Assert.Equal(new RgbaVector(2.5F, 2.5F, 2.5F, 0.625F), image[2, 0]); + } + + /// + /// Separate-channel AutoLevel uses the endpoint CDF entry for HDR component values. + /// + [Fact] + public void SeparateChannels_MapsHdrComponentsToEndpointBin() + { + using Image image = new(3, 1); + image[0, 0] = new RgbaVector(0F, 0F, 0F, 0.625F); + image[1, 0] = new RgbaVector(0.5F, 0.5F, 0.5F, 0.625F); + image[2, 0] = new RgbaVector(2F, 2F, 2F, 0.625F); + + image.Mutate(ctx => ctx.ApplyProcessor(new AutoLevelProcessor(5, false, 350, false))); + + Assert.Equal(new RgbaVector(0F, 0F, 0F, 0.625F), image[0, 0]); + Assert.Equal(new RgbaVector(0.5F, 0.5F, 0.5F, 0.625F), image[1, 0]); + Assert.Equal(new RgbaVector(1F, 1F, 1F, 0.625F), image[2, 0]); + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] + public void SeparateChannels_CompareToReferenceOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.ApplyProcessor(new AutoLevelProcessor(256, false, 350, false))); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png"); + } + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] + public void SynchronizedChannels_CompareToReferenceOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.AutoLevel()); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png"); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index c39648d014..af475a1903 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -14,6 +14,97 @@ public class HistogramEqualizationTests { private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); + /// + /// Histogram indices saturate before they address a bounded CDF. + /// + /// The floating-point sample. + /// The expected index in a five-bin histogram. + [Theory] + [InlineData(float.NaN, 0)] + [InlineData(float.NegativeInfinity, 0)] + [InlineData(-1F, 0)] + [InlineData(0.25F, 1)] + [InlineData(0.75F, 3)] + [InlineData(2F, 4)] + [InlineData(float.PositiveInfinity, 4)] + public void FloatingLuminance_UsesBoundedHistogramIndex(float sample, int expectedIndex) + { + RgbaVector pixel = new(sample, sample, sample); + int index = HistogramEqualizationProcessor.GetLuminance(pixel, 5); + + Assert.Equal(expectedIndex, index); + } + + /// + /// Global equalization places out-of-range and nonfinite samples in the endpoint bins. + /// + [Fact] + public void GlobalFloatingSamples_UseEndpointBins() + { + float[] samples = [float.NaN, float.NegativeInfinity, -1F, 0.25F, 0.75F, 2F, float.PositiveInfinity]; + float[] expected = [0F, 0F, 0F, 0.25F, 0.5F, 1F, 1F]; + using Image image = new(samples.Length, 1); + + for (int x = 0; x < samples.Length; x++) + { + image[x, 0] = new RgbaVector(samples[x], samples[x], samples[x], 0.625F); + } + + image.Mutate(ctx => ctx.HistogramEqualization(new HistogramEqualizationOptions + { + Method = HistogramEqualizationMethod.Global, + LuminanceLevels = 5 + })); + + for (int x = 0; x < samples.Length; x++) + { + Assert.Equal(new RgbaVector(expected[x], expected[x], expected[x], 0.625F), image[x, 0]); + } + } + + /// + /// Adaptive CDF lookups produce the same pixels when their input indices match. + /// + /// The equalization method. + [Theory] + [InlineData(HistogramEqualizationMethod.AdaptiveTileInterpolation)] + [InlineData(HistogramEqualizationMethod.AdaptiveSlidingWindow)] + public void FloatingSamples_MatchBoundedIndexReference(HistogramEqualizationMethod method) + { + float[] samples = [float.NaN, float.NegativeInfinity, -1F, 0F, 0.25F, 0.75F, 2F, float.PositiveInfinity]; + float[] bounded = [0F, 0F, 0F, 0F, 0.25F, 0.75F, 1F, 1F]; + using Image actual = new(17, 17); + using Image reference = new(17, 17); + + for (int y = 0; y < actual.Height; y++) + { + for (int x = 0; x < actual.Width; x++) + { + int sample = (x + y) % samples.Length; + actual[x, y] = new RgbaVector(samples[sample], samples[sample], samples[sample], 0.625F); + reference[x, y] = new RgbaVector(bounded[sample], bounded[sample], bounded[sample], 0.625F); + } + } + + HistogramEqualizationOptions options = new() + { + Method = method, + LuminanceLevels = 5, + NumberOfTiles = 2 + }; + + actual.Mutate(ctx => ctx.HistogramEqualization(options)); + reference.Mutate(ctx => ctx.HistogramEqualization(options)); + + for (int y = 0; y < actual.Height; y++) + { + for (int x = 0; x < actual.Width; x++) + { + Assert.Equal(reference[x, y], actual[x, y]); + } + } + } + [Theory] [InlineData(256)] [InlineData(65536)] @@ -134,44 +225,6 @@ public void Adaptive_TileInterpolation_10Tiles_WithClipping(TestImagePro } } - [Theory] - [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] - public void AutoLevel_SeparateChannels_CompareToReferenceOutput(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - HistogramEqualizationOptions options = new() - { - Method = HistogramEqualizationMethod.AutoLevel, - LuminanceLevels = 256, - SyncChannels = false - }; - image.Mutate(x => x.HistogramEqualization(options)); - image.DebugSave(provider); - image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png"); - } - } - - [Theory] - [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] - public void AutoLevel_SynchronizedChannels_CompareToReferenceOutput(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - HistogramEqualizationOptions options = new() - { - Method = HistogramEqualizationMethod.AutoLevel, - LuminanceLevels = 256, - SyncChannels = true - }; - image.Mutate(x => x.HistogramEqualization(options)); - image.DebugSave(provider); - image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png"); - } - } - /// /// This is regression test for a bug with the calculation of the y-start positions, /// where it could happen that one too much start position was calculated in some cases. diff --git a/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs index 2b609a9b2a..256bcb457b 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs @@ -49,7 +49,9 @@ private static FileStream LoadAsStream(TestImageProvider provide ?? throw new InvalidOperationException("CompareToMagick() works only with file providers!"); TestFile testFile = TestFile.Create(path); - return new FileStream(testFile.FullPath, FileMode.Open); + + // The input is shared with other tests running in parallel. + return File.OpenRead(testFile.FullPath); } private static Image ConvertImageFromMagick(MagickImage magickImage) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryThresholdTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryThresholdTest.cs index c9d3f69143..742bfdd8c8 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryThresholdTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryThresholdTest.cs @@ -1,9 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Globalization; +using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Binarization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization; @@ -11,6 +12,114 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization; [Trait("Category", "Processors")] public class BinaryThresholdTest { + [Theory] + [InlineData(2F, 5F, 6F)] + [InlineData(20F, 50F, 60F)] + public void LuminanceThreshold_UsesObservedHdrRange(float minimum, float middle, float maximum) + { + using Image image = new(65, 1); + for (int x = 0; x < image.Width; x++) + { + image[x, 0] = new RgbaVector(minimum, minimum, minimum); + } + + image[32, 0] = new RgbaVector(middle, middle, middle); + image[64, 0] = new RgbaVector(maximum, maximum, maximum); + + image.Mutate(x => x.BinaryThreshold(.5F)); + + Assert.Equal(RgbaVector.FromRgba32(Color.Black.ToPixel()), image[0, 0]); + Assert.Equal(RgbaVector.FromRgba32(Color.White.ToPixel()), image[32, 0]); + Assert.Equal(RgbaVector.FromRgba32(Color.White.ToPixel()), image[64, 0]); + } + + [Fact] + public void LuminanceThreshold_NonfiniteMetricsSelectLower() + { + using Image image = new(65, 1); + for (int x = 0; x < image.Width; x++) + { + image[x, 0] = new RgbaVector(2F, 2F, 2F); + } + + image[30, 0] = new RgbaVector(4F, 4F, 4F); + image[31, 0] = new RgbaVector(float.NaN, 2F, 2F); + image[32, 0] = new RgbaVector(float.PositiveInfinity, 2F, 2F); + image[33, 0] = new RgbaVector(float.NegativeInfinity, 2F, 2F); + image[34, 0] = new RgbaVector(4F, 4F, 4F, float.NaN); + + image.Mutate(x => x.BinaryThreshold(.5F)); + + Assert.Equal(0F, image[0, 0].R); + Assert.Equal(1F, image[30, 0].R); + Assert.Equal(0F, image[31, 0].R); + Assert.Equal(0F, image[32, 0].R); + Assert.Equal(0F, image[33, 0].R); + Assert.Equal(0F, image[34, 0].R); + } + + [Fact] + public void LuminanceThreshold_UsesOnlySelectedRegionForRange() + { + using Image image = new(65, 1); + for (int x = 0; x < image.Width; x++) + { + image[x, 0] = new RgbaVector(2F, 2F, 2F); + } + + image[32, 0] = new RgbaVector(4F, 4F, 4F); + image[64, 0] = new RgbaVector(100F, 100F, 100F); + + image.Mutate(x => x.BinaryThreshold(.5F, new Rectangle(0, 0, 64, 1))); + + Assert.Equal(0F, image[0, 0].R); + Assert.Equal(1F, image[32, 0].R); + Assert.Equal(100F, image[64, 0].R); + } + + [Theory] + [InlineData(BinaryThresholdMode.Saturation)] + [InlineData(BinaryThresholdMode.MaxChroma)] + public void ColorMetricThreshold_UsesObservedRange(BinaryThresholdMode mode) + { + using Image image = new(65, 1); + for (int x = 0; x < image.Width; x++) + { + image[x, 0] = new RgbaVector(.5F, .5F, .5F); + } + + image[32, 0] = new RgbaVector(.8F, 0F, 0F); + image[64, 0] = new RgbaVector(1F, 0F, 0F); + + image.Mutate(x => x.BinaryThreshold(.5F, mode)); + + Assert.Equal(0F, image[0, 0].R); + Assert.Equal(1F, image[32, 0].R); + Assert.Equal(1F, image[64, 0].R); + } + + [Fact] + public void LuminanceThreshold_ConstantFiniteRangeSelectsUpper() + { + using Image image = new(65, 1, new RgbaVector(2F, 2F, 2F)); + + image.Mutate(x => x.BinaryThreshold(.5F)); + + Assert.Equal(1F, image[0, 0].R); + Assert.Equal(1F, image[64, 0].R); + } + + [Fact] + public void LuminanceThreshold_WithoutFiniteMetricsSelectsLower() + { + using Image image = new(65, 1, new RgbaVector(float.NaN, float.NaN, float.NaN)); + + image.Mutate(x => x.BinaryThreshold(.5F)); + + Assert.Equal(0F, image[0, 0].R); + Assert.Equal(0F, image[64, 0].R); + } + public static readonly TheoryData BinaryThresholdValues = new() { @@ -60,11 +169,12 @@ public void ImageShouldApplyBinaryThresholdInBox(TestImageProvider(TestImageProvider provider, float value) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) + using (Image source = provider.GetImage()) + using (Image image = source.Clone()) { image.Mutate(x => x.BinaryThreshold(value, BinaryThresholdMode.Saturation)); image.DebugSave(provider, value); - image.CompareToReferenceOutput(ImageComparer.Exact, provider, value.ToString("0.00", NumberFormatInfo.InvariantInfo)); + AssertObservedRangeThreshold(source, image, source.Bounds, value, BinaryThresholdMode.Saturation); } } @@ -80,7 +190,7 @@ public void ImageShouldApplyBinarySaturationThresholdInBox(TestImageProv image.Mutate(x => x.BinaryThreshold(value, BinaryThresholdMode.Saturation, bounds)); image.DebugSave(provider, value); - image.CompareToReferenceOutput(ImageComparer.Exact, provider, value.ToString("0.00", NumberFormatInfo.InvariantInfo)); + AssertObservedRangeThreshold(source, image, bounds, value, BinaryThresholdMode.Saturation); } } @@ -89,20 +199,12 @@ public void ImageShouldApplyBinarySaturationThresholdInBox(TestImageProv public void ImageShouldApplyBinaryMaxChromaThresholdFilter(TestImageProvider provider, float value) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) + using (Image source = provider.GetImage()) + using (Image image = source.Clone()) { image.Mutate(x => x.BinaryThreshold(value, BinaryThresholdMode.MaxChroma)); image.DebugSave(provider, value); - - if (!TestEnvironment.Is64BitProcess && TestEnvironment.IsFramework) - { - ImageComparer comparer = ImageComparer.TolerantPercentage(0.0004F); - image.CompareToReferenceOutput(comparer, provider, value.ToString("0.00", NumberFormatInfo.InvariantInfo)); - } - else - { - image.CompareToReferenceOutput(ImageComparer.Exact, provider, value.ToString("0.00", NumberFormatInfo.InvariantInfo)); - } + AssertObservedRangeThreshold(source, image, source.Bounds, value, BinaryThresholdMode.MaxChroma); } } @@ -118,16 +220,86 @@ public void ImageShouldApplyBinaryMaxChromaThresholdInBox(TestImageProvi image.Mutate(x => x.BinaryThreshold(value, BinaryThresholdMode.MaxChroma, bounds)); image.DebugSave(provider, value); + AssertObservedRangeThreshold(source, image, bounds, value, BinaryThresholdMode.MaxChroma); + } + } - if (!TestEnvironment.Is64BitProcess && TestEnvironment.IsFramework) + /// + /// Checks every output pixel against the observed range of the original image. + /// + private static void AssertObservedRangeThreshold( + Image source, + Image actual, + Rectangle bounds, + float fraction, + BinaryThresholdMode mode) + where TPixel : unmanaged, IPixel + { + Vector4[] vectors = new Vector4[source.Width]; + PixelOperations operations = PixelOperations.Instance; + PixelConversionModifiers modifiers = PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply; + float minimum = float.PositiveInfinity; + float maximum = float.NegativeInfinity; + + // Use the same pixel conversion contract for the source values, then calculate + // the range and expected mask independently from the unmodified image. + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + operations.ToVector4(source.Configuration, source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y), vectors, modifiers); + + for (int x = bounds.Left; x < bounds.Right; x++) { - ImageComparer comparer = ImageComparer.TolerantPercentage(0.0004F); - image.CompareToReferenceOutput(comparer, provider, value.ToString("0.00", NumberFormatInfo.InvariantInfo)); + float metric = GetReferenceMetric(vectors[x], mode); + minimum = MathF.Min(minimum, metric); + maximum = MathF.Max(maximum, metric); } - else + } + + float threshold = (float)(minimum + (fraction * ((double)maximum - minimum))); + TPixel upper = Color.White.ToPixel(); + TPixel lower = Color.Black.ToPixel(); + + for (int y = 0; y < source.Height; y++) + { + operations.ToVector4(source.Configuration, source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y), vectors, modifiers); + + for (int x = 0; x < source.Width; x++) { - image.CompareToReferenceOutput(ImageComparer.Exact, provider, value.ToString("0.00", NumberFormatInfo.InvariantInfo)); + TPixel expected = bounds.Contains(x, y) + ? GetReferenceMetric(vectors[x], mode) >= threshold ? upper : lower + : source[x, y]; + + TPixel actualPixel = actual[x, y]; + if (!expected.Equals(actualPixel)) + { + Assert.Fail($"({x}, {y}) source={source[x, y]} metric={GetReferenceMetric(vectors[x], mode)} threshold={threshold} expected={expected} actual={actualPixel}"); + } } } } + + /// + /// Calculates the selected color metric from one straight source pixel. + /// + private static float GetReferenceMetric(Vector4 vector, BinaryThresholdMode mode) + { + float max = MathF.Max(vector.X, MathF.Max(vector.Y, vector.Z)); + float min = MathF.Min(vector.X, MathF.Min(vector.Y, vector.Z)); + + if (mode == BinaryThresholdMode.Saturation) + { + float chroma = max - min; + if (MathF.Abs(chroma) < Constants.Epsilon) + { + return 0F; + } + + float lightness = (max + min) * .5F; + return lightness <= .5F ? chroma / (max + min) : chroma / (2F - max - min); + } + + float cb = (-0.168736F * vector.X) - (0.331264F * vector.Y) + (0.5F * vector.Z); + float cr = (0.5F * vector.X) - (0.418688F * vector.Y) - (0.081312F * vector.Z); + return MathF.Max(MathF.Abs(cb), MathF.Abs(cr)); + } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 3c2e5e0d50..29cdc45d59 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -105,6 +105,19 @@ public void DiffusionFilter_ShouldNotDependOnSinglePixelType(TestImagePr provider.RunValidatingProcessorTest(x => x.Dither(DefaultErrorDiffuser), comparer: comparer); } + [Fact] + public void DiffusionFilter_PreservesFloatingResidualBeyondOne() + { + using Image image = new(4, 4, new RgbaVector(.5F, 0F, 0F)); + RgbaVector source = new(4F, 0F, 0F); + image[1, 1] = source; + + ErrorDither.FloydSteinberg.Dither(image.Frames.RootFrame, image.Bounds, source, new RgbaVector(0F, 0F, 0F), 1, 1, 1F); + + // The right neighbor receives 7/16 of the error: 0.5 + (4 * 7/16) = 2.25. + Assert.Equal(2.25F, image[2, 1].R); + } + [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)] public void DiffusionFilter_WorksWithAllErrorDiffusers( diff --git a/tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs index b7e8597859..3c7e2ad24f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs @@ -46,10 +46,53 @@ public void InBox(TestImageProvider provider, int levels, int br $"{levels}-{brushSize}", ImageComparer.TolerantPercentage(0.01F)); + /// + /// A uniform image isolates bin selection; the output must retain the source component even at nonfinite endpoints. + /// + /// The unbounded component. + [Theory] + [InlineData(2F)] + [InlineData(100F)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(float.NaN)] + public void Issue2518_PixelComponentOutsideOfRange_UsesBoundedIntensityBin(float component) + { + using Image image = new(10, 10, new RgbaVector(1, 1, component)); + + image.Mutate(ctx => ctx.OilPaint()); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + float actual = image[x, y].ToVector4().Z; + if (float.IsNaN(component)) + { + Assert.True(float.IsNaN(actual)); + } + else + { + Assert.Equal(component, actual); + } + } + } + } + + /// + /// Values above the intensity range share its last bin while their color samples remain unbounded. + /// [Fact] - public void Issue2518_PixelComponentOutsideOfRange_ThrowsImageProcessingException() + public void OilPaint_HdrSamplesShareEndpointBinWithoutClampingColor() { - using Image image = new(10, 10, new RgbaVector(1, 1, 100)); - Assert.Throws(() => image.Mutate(ctx => ctx.OilPaint())); + using Image image = new(2, 2); + image[0, 0] = new RgbaVector(2F, 2F, 2F); + image[1, 0] = new RgbaVector(4F, 4F, 4F); + image[0, 1] = new RgbaVector(2F, 2F, 2F); + image[1, 1] = new RgbaVector(4F, 4F, 4F); + + image.Mutate(ctx => ctx.OilPaint(2, 2)); + + Assert.Equal(new RgbaVector(3F, 3F, 3F), image[1, 1]); } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 517a406cb6..e5b123e8bf 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1419,6 +1419,7 @@ public static class Exr public const string Uncompressed = "Exr/Calliphora_uncompressed.exr"; public const string UncompressedRgba = "Exr/Calliphora_uncompressed_rgba.exr"; public const string UncompressedFloatRgb = "Exr/Calliphora_float_uncompressed.exr"; + public const string OpenExrHdrHalf = "Exr/openexr_comp_none.exr"; public const string UncompressedUintRgb = "Exr/Calliphora_uint32_uncompressed.exr"; public const string UintRgba = "Exr/rgba_uint_uncompressed.exr"; public const string Zip = "Exr/Calliphora_zip.exr"; diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs index 7c45dd047c..bbff1453e2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs @@ -29,6 +29,12 @@ namespace SixLabors.ImageSharp.Tests; /// public bool Equals(float x, float y) { + // Equal infinities and matching NaNs have no finite difference to compare with epsilon. + if (x == y || (float.IsNaN(x) && float.IsNaN(y))) + { + return true; + } + float d = x - y; return d >= -this.epsilon && d <= this.epsilon; diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/AutoLevelProcessorTests/SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png similarity index 100% rename from tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png rename to tests/Images/External/ReferenceOutput/AutoLevelProcessorTests/SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/AutoLevelProcessorTests/SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png similarity index 100% rename from tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png rename to tests/Images/External/ReferenceOutput/AutoLevelProcessorTests/SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png index 5da96d59d2..af007028bd 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e7ece9d70c4fe0771abd43e4dbb33fb95f474ca56633dcb821022ee44e746d4 -size 728 +oid sha256:6b002566a8a5ebf2abe309dccd9d0fb445a7c6e3936b3c071ad90cb3afa9a0ce +size 848 diff --git a/tests/Images/Input/Exr/LICENSE.openexr.txt b/tests/Images/Input/Exr/LICENSE.openexr.txt new file mode 100644 index 0000000000..b2a016ed19 --- /dev/null +++ b/tests/Images/Input/Exr/LICENSE.openexr.txt @@ -0,0 +1,11 @@ +Copyright (c) Contributors to the OpenEXR Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/Images/Input/Exr/openexr_comp_none.exr b/tests/Images/Input/Exr/openexr_comp_none.exr new file mode 100644 index 0000000000..f0b9796215 --- /dev/null +++ b/tests/Images/Input/Exr/openexr_comp_none.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ba46760fb9d9dde5200853dabdc6f901d4f0a9c48b700fe395b2ed2a9582abd +size 3180931