diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Parameters.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Parameters.cs
new file mode 100644
index 000000000..42982f751
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Parameters.cs
@@ -0,0 +1,262 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Formats.Heif.Color;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
+
+///
+/// Provides fixed-point scalar and SIMD coefficient storage for eight-bit 4:2:0 conversion.
+///
+internal static partial class HevcYuv420ToRgb8Converter
+{
+ ///
+ /// Stores every scalar and SIMD coefficient representation resolved once for an image.
+ ///
+ private readonly struct ConversionParameters
+ {
+ ///
+ /// The scalar fixed-point coefficients.
+ ///
+ public readonly FixedPointParameters Scalar;
+
+ ///
+ /// The four-lane SIMD coefficients.
+ ///
+ public readonly Vector128Parameters FourLane;
+
+ ///
+ /// The eight-lane SIMD coefficients.
+ ///
+ public readonly Vector256Parameters EightLane;
+
+ ///
+ /// The sixteen-lane SIMD coefficients.
+ ///
+ public readonly Vector512Parameters SixteenLane;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The shared floating-point conversion parameters.
+ public ConversionParameters(in HeifColorConversionParameters parameters)
+ {
+ FixedPointParameters scalar = new(in parameters);
+ this.Scalar = scalar;
+ this.FourLane = new(in scalar);
+ this.EightLane = new(in scalar);
+ this.SixteenLane = new(in scalar);
+ }
+ }
+
+ ///
+ /// Stores the scalar fixed-point coefficients resolved for one image.
+ ///
+ private readonly struct FixedPointParameters
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The shared floating-point conversion parameters.
+ public FixedPointParameters(in HeifColorConversionParameters parameters)
+ {
+ float scale = 1 << CoefficientShift;
+
+ // Rounding each image-invariant coefficient once gives the integer kernel eight fractional bits.
+ // The signed green coefficients retain the exact addition and rounding order used by every SIMD lane.
+ this.RedCr = (int)MathF.Round(parameters.RedChromaScale * scale, MidpointRounding.AwayFromZero);
+ this.GreenCb = -(int)MathF.Round(parameters.GreenBlueChromaScale * scale, MidpointRounding.AwayFromZero);
+ this.GreenCr = -(int)MathF.Round(parameters.GreenRedChromaScale * scale, MidpointRounding.AwayFromZero);
+ this.BlueCb = (int)MathF.Round(parameters.BlueChromaScale * scale, MidpointRounding.AwayFromZero);
+ }
+
+ ///
+ /// Gets the red contribution from centered Cr.
+ ///
+ public int RedCr { get; }
+
+ ///
+ /// Gets the green contribution from centered Cb.
+ ///
+ public int GreenCb { get; }
+
+ ///
+ /// Gets the green contribution from centered Cr.
+ ///
+ public int GreenCr { get; }
+
+ ///
+ /// Gets the blue contribution from centered Cb.
+ ///
+ public int BlueCb { get; }
+ }
+
+ ///
+ /// Broadcasts the fixed-point coefficients for four-lane conversion.
+ ///
+ private readonly struct Vector128Parameters
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The scalar fixed-point coefficients.
+ public Vector128Parameters(in FixedPointParameters parameters)
+ {
+ this.ChromaMidpoint = Vector128.Create(HevcYuv420ToRgb8Converter.ChromaMidpoint);
+ this.RoundingBias = Vector128.Create(HevcYuv420ToRgb8Converter.RoundingBias);
+ this.Maximum = Vector128.Create((int)byte.MaxValue);
+ this.RedCr = Vector128.Create(parameters.RedCr);
+ this.GreenCb = Vector128.Create(parameters.GreenCb);
+ this.GreenCr = Vector128.Create(parameters.GreenCr);
+ this.BlueCb = Vector128.Create(parameters.BlueCb);
+ }
+
+ ///
+ /// Gets the neutral chroma code-value lanes.
+ ///
+ public Vector128 ChromaMidpoint { get; }
+
+ ///
+ /// Gets the fixed-point rounding-bias lanes.
+ ///
+ public Vector128 RoundingBias { get; }
+
+ ///
+ /// Gets the maximum eight-bit sample lanes.
+ ///
+ public Vector128 Maximum { get; }
+
+ ///
+ /// Gets the red Cr coefficient lanes.
+ ///
+ public Vector128 RedCr { get; }
+
+ ///
+ /// Gets the green Cb coefficient lanes.
+ ///
+ public Vector128 GreenCb { get; }
+
+ ///
+ /// Gets the green Cr coefficient lanes.
+ ///
+ public Vector128 GreenCr { get; }
+
+ ///
+ /// Gets the blue Cb coefficient lanes.
+ ///
+ public Vector128 BlueCb { get; }
+ }
+
+ ///
+ /// Broadcasts the fixed-point coefficients for eight-lane conversion.
+ ///
+ private readonly struct Vector256Parameters
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The scalar fixed-point coefficients.
+ public Vector256Parameters(in FixedPointParameters parameters)
+ {
+ this.ChromaMidpoint = Vector256.Create(HevcYuv420ToRgb8Converter.ChromaMidpoint);
+ this.RoundingBias = Vector256.Create(HevcYuv420ToRgb8Converter.RoundingBias);
+ this.Maximum = Vector256.Create((int)byte.MaxValue);
+ this.RedCr = Vector256.Create(parameters.RedCr);
+ this.GreenCb = Vector256.Create(parameters.GreenCb);
+ this.GreenCr = Vector256.Create(parameters.GreenCr);
+ this.BlueCb = Vector256.Create(parameters.BlueCb);
+ }
+
+ ///
+ /// Gets the neutral chroma code-value lanes.
+ ///
+ public Vector256 ChromaMidpoint { get; }
+
+ ///
+ /// Gets the fixed-point rounding-bias lanes.
+ ///
+ public Vector256 RoundingBias { get; }
+
+ ///
+ /// Gets the maximum eight-bit sample lanes.
+ ///
+ public Vector256 Maximum { get; }
+
+ ///
+ /// Gets the red Cr coefficient lanes.
+ ///
+ public Vector256 RedCr { get; }
+
+ ///
+ /// Gets the green Cb coefficient lanes.
+ ///
+ public Vector256 GreenCb { get; }
+
+ ///
+ /// Gets the green Cr coefficient lanes.
+ ///
+ public Vector256 GreenCr { get; }
+
+ ///
+ /// Gets the blue Cb coefficient lanes.
+ ///
+ public Vector256 BlueCb { get; }
+ }
+
+ ///
+ /// Broadcasts the fixed-point coefficients for sixteen-lane conversion.
+ ///
+ private readonly struct Vector512Parameters
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The scalar fixed-point coefficients.
+ public Vector512Parameters(in FixedPointParameters parameters)
+ {
+ this.ChromaMidpoint = Vector512.Create(HevcYuv420ToRgb8Converter.ChromaMidpoint);
+ this.RoundingBias = Vector512.Create(HevcYuv420ToRgb8Converter.RoundingBias);
+ this.Maximum = Vector512.Create((int)byte.MaxValue);
+ this.RedCr = Vector512.Create(parameters.RedCr);
+ this.GreenCb = Vector512.Create(parameters.GreenCb);
+ this.GreenCr = Vector512.Create(parameters.GreenCr);
+ this.BlueCb = Vector512.Create(parameters.BlueCb);
+ }
+
+ ///
+ /// Gets the neutral chroma code-value lanes.
+ ///
+ public Vector512 ChromaMidpoint { get; }
+
+ ///
+ /// Gets the fixed-point rounding-bias lanes.
+ ///
+ public Vector512 RoundingBias { get; }
+
+ ///
+ /// Gets the maximum eight-bit sample lanes.
+ ///
+ public Vector512 Maximum { get; }
+
+ ///
+ /// Gets the red Cr coefficient lanes.
+ ///
+ public Vector512 RedCr { get; }
+
+ ///
+ /// Gets the green Cb coefficient lanes.
+ ///
+ public Vector512 GreenCb { get; }
+
+ ///
+ /// Gets the green Cr coefficient lanes.
+ ///
+ public Vector512 GreenCr { get; }
+
+ ///
+ /// Gets the blue Cb coefficient lanes.
+ ///
+ public Vector512 BlueCb { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Simd.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Simd.cs
new file mode 100644
index 000000000..65223ea44
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.Simd.cs
@@ -0,0 +1,293 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Common.Helpers;
+using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
+
+///
+/// Provides the fixed-point scalar and SIMD row kernels for eight-bit 4:2:0 conversion.
+///
+internal static partial class HevcYuv420ToRgb8Converter
+{
+ ///
+ /// Converts one luma row and its nearest native chroma row to planar eight-bit RGB.
+ ///
+ /// The full-resolution luma samples.
+ /// The half-width blue-difference samples.
+ /// The half-width red-difference samples.
+ /// The destination red samples.
+ /// The destination green samples.
+ /// The destination blue samples.
+ /// The fixed-point matrix coefficients.
+ private static void ConvertRow(
+ ReadOnlySpan luma,
+ ReadOnlySpan chromaBlue,
+ ReadOnlySpan chromaRed,
+ Span red,
+ Span green,
+ Span blue,
+ in ConversionParameters parameters)
+ {
+ ref ushort lumaBase = ref MemoryMarshal.GetReference(luma);
+ ref ushort chromaBlueBase = ref MemoryMarshal.GetReference(chromaBlue);
+ ref ushort chromaRedBase = ref MemoryMarshal.GetReference(chromaRed);
+ ref byte redBase = ref MemoryMarshal.GetReference(red);
+ ref byte greenBase = ref MemoryMarshal.GetReference(green);
+ ref byte blueBase = ref MemoryMarshal.GetReference(blue);
+ int x = 0;
+
+ // The shared offset lets the widest supported register consume the row first. Narrower widths then
+ // handle the complete remainder, leaving at most three pixels for the scalar fallback.
+ if (Vector512.IsHardwareAccelerated)
+ {
+ int oneVectorFromEnd = luma.Length - Vector512.Count;
+
+ for (; x <= oneVectorFromEnd; x += Vector512.Count)
+ {
+ Vector512 y = LoadVector512(ref Unsafe.Add(ref lumaBase, x));
+ Vector512 cb = LoadRepeatedVector512(ref Unsafe.Add(ref chromaBlueBase, x >> 1));
+ Vector512 cr = LoadRepeatedVector512(ref Unsafe.Add(ref chromaRedBase, x >> 1));
+
+ Convert(y, cb, cr, in parameters.SixteenLane, out Vector512 r, out Vector512 g, out Vector512 b);
+ HeifByteSampleStorer.Store(r, ref Unsafe.Add(ref redBase, x));
+ HeifByteSampleStorer.Store(g, ref Unsafe.Add(ref greenBase, x));
+ HeifByteSampleStorer.Store(b, ref Unsafe.Add(ref blueBase, x));
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ int oneVectorFromEnd = luma.Length - Vector256.Count;
+
+ for (; x <= oneVectorFromEnd; x += Vector256.Count)
+ {
+ Vector256 y = LoadVector256(ref Unsafe.Add(ref lumaBase, x));
+ Vector256 cb = LoadRepeatedVector256(ref Unsafe.Add(ref chromaBlueBase, x >> 1));
+ Vector256 cr = LoadRepeatedVector256(ref Unsafe.Add(ref chromaRedBase, x >> 1));
+
+ Convert(y, cb, cr, in parameters.EightLane, out Vector256 r, out Vector256 g, out Vector256 b);
+ HeifByteSampleStorer.Store(r, ref Unsafe.Add(ref redBase, x));
+ HeifByteSampleStorer.Store(g, ref Unsafe.Add(ref greenBase, x));
+ HeifByteSampleStorer.Store(b, ref Unsafe.Add(ref blueBase, x));
+ }
+ }
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ int oneVectorFromEnd = luma.Length - Vector128.Count;
+
+ for (; x <= oneVectorFromEnd; x += Vector128.Count)
+ {
+ Vector128 y = LoadVector128(ref Unsafe.Add(ref lumaBase, x));
+ Vector128 cb = LoadRepeatedVector128(ref Unsafe.Add(ref chromaBlueBase, x >> 1));
+ Vector128 cr = LoadRepeatedVector128(ref Unsafe.Add(ref chromaRedBase, x >> 1));
+
+ Convert(y, cb, cr, in parameters.FourLane, out Vector128 r, out Vector128 g, out Vector128 b);
+ HeifByteSampleStorer.Store(r, ref Unsafe.Add(ref redBase, x));
+ HeifByteSampleStorer.Store(g, ref Unsafe.Add(ref greenBase, x));
+ HeifByteSampleStorer.Store(b, ref Unsafe.Add(ref blueBase, x));
+ }
+ }
+
+ for (; x < luma.Length; x++)
+ {
+ Convert(
+ Unsafe.Add(ref lumaBase, x),
+ Unsafe.Add(ref chromaBlueBase, x >> 1),
+ Unsafe.Add(ref chromaRedBase, x >> 1),
+ in parameters.Scalar,
+ out Unsafe.Add(ref redBase, x),
+ out Unsafe.Add(ref greenBase, x),
+ out Unsafe.Add(ref blueBase, x));
+ }
+ }
+
+ ///
+ /// Loads sixteen luma samples as signed 32-bit SIMD lanes.
+ ///
+ /// The first native luma sample.
+ /// The widened luma lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector512 LoadVector512(ref ushort source)
+ {
+ (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref source));
+ return Vector512.Create(lower, upper).AsInt32();
+ }
+
+ ///
+ /// Loads eight luma samples as signed 32-bit SIMD lanes.
+ ///
+ /// The first native luma sample.
+ /// The widened luma lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 LoadVector256(ref ushort source)
+ {
+ Vector128 samples = Vector128.LoadUnsafe(ref source);
+ return Vector256.Create(Vector128.WidenLower(samples), Vector128.WidenUpper(samples)).AsInt32();
+ }
+
+ ///
+ /// Loads four luma samples as signed 32-bit SIMD lanes.
+ ///
+ /// The first native luma sample.
+ /// The widened luma lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 LoadVector128(ref ushort source)
+ {
+ ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source));
+ return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsUInt16()).AsInt32();
+ }
+
+ ///
+ /// Loads eight chroma samples and repeats each sample into two of sixteen 32-bit SIMD lanes.
+ ///
+ /// The first native chroma sample.
+ /// The horizontally replicated chroma lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector512 LoadRepeatedVector512(ref ushort source)
+ {
+ Vector128 samples = Vector128.LoadUnsafe(ref source);
+ Vector128 lower = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16();
+ Vector128 upper = Vector128_.UnpackHigh(samples.AsInt16(), samples.AsInt16()).AsUInt16();
+ (Vector256 widenedLower, Vector256 widenedUpper) = Vector256.Widen(Vector256.Create(lower, upper));
+ return Vector512.Create(widenedLower, widenedUpper).AsInt32();
+ }
+
+ ///
+ /// Loads four chroma samples and repeats each sample into two of eight 32-bit SIMD lanes.
+ ///
+ /// The first native chroma sample.
+ /// The horizontally replicated chroma lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 LoadRepeatedVector256(ref ushort source)
+ {
+ ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source));
+ Vector128 samples = Vector128.CreateScalarUnsafe(packed).AsUInt16();
+ Vector128 repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16();
+ return Vector256.Create(Vector128.WidenLower(repeated), Vector128.WidenUpper(repeated)).AsInt32();
+ }
+
+ ///
+ /// Loads two chroma samples and repeats each sample into two of four 32-bit SIMD lanes.
+ ///
+ /// The first native chroma sample.
+ /// The horizontally replicated chroma lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 LoadRepeatedVector128(ref ushort source)
+ {
+ uint packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source));
+ Vector128 samples = Vector128.CreateScalarUnsafe(packed).AsUInt16();
+ Vector128 repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16();
+ return Vector128.WidenLower(repeated).AsInt32();
+ }
+
+ ///
+ /// Converts one coefficient-based H.273 YCbCr sample to eight-bit RGB.
+ ///
+ /// The luma sample.
+ /// The blue-difference sample.
+ /// The red-difference sample.
+ /// The fixed-point matrix coefficients.
+ /// The converted red sample.
+ /// The converted green sample.
+ /// The converted blue sample.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Convert(ushort y, ushort cb, ushort cr, in FixedPointParameters parameters, out byte r, out byte g, out byte b)
+ {
+ int centeredBlue = cb - ChromaMidpoint;
+ int centeredRed = cr - ChromaMidpoint;
+ int red = y + (((parameters.RedCr * centeredRed) + RoundingBias) >> CoefficientShift);
+ int green = y + (((parameters.GreenCb * centeredBlue) + (parameters.GreenCr * centeredRed) + RoundingBias) >> CoefficientShift);
+ int blue = y + (((parameters.BlueCb * centeredBlue) + RoundingBias) >> CoefficientShift);
+
+ r = (byte)Numerics.Clamp(red, 0, byte.MaxValue);
+ g = (byte)Numerics.Clamp(green, 0, byte.MaxValue);
+ b = (byte)Numerics.Clamp(blue, 0, byte.MaxValue);
+ }
+
+ ///
+ /// Converts four coefficient-based H.273 YCbCr samples to eight-bit RGB lanes.
+ ///
+ /// The luma lanes.
+ /// The blue-difference lanes.
+ /// The red-difference lanes.
+ /// The fixed-point matrix coefficient lanes.
+ /// The converted red lanes.
+ /// The converted green lanes.
+ /// The converted blue lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Convert(
+ Vector128 y,
+ Vector128 cb,
+ Vector128 cr,
+ in Vector128Parameters parameters,
+ out Vector128 r,
+ out Vector128 g,
+ out Vector128 b)
+ {
+ cb -= parameters.ChromaMidpoint;
+ cr -= parameters.ChromaMidpoint;
+ r = Vector128.Clamp(y + (((parameters.RedCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector128.Zero, parameters.Maximum);
+ g = Vector128.Clamp(y + (((parameters.GreenCb * cb) + (parameters.GreenCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector128.Zero, parameters.Maximum);
+ b = Vector128.Clamp(y + (((parameters.BlueCb * cb) + parameters.RoundingBias) >> CoefficientShift), Vector128.Zero, parameters.Maximum);
+ }
+
+ ///
+ /// Converts eight coefficient-based H.273 YCbCr samples to eight-bit RGB lanes.
+ ///
+ /// The luma lanes.
+ /// The blue-difference lanes.
+ /// The red-difference lanes.
+ /// The fixed-point matrix coefficient lanes.
+ /// The converted red lanes.
+ /// The converted green lanes.
+ /// The converted blue lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Convert(
+ Vector256 y,
+ Vector256 cb,
+ Vector256 cr,
+ in Vector256Parameters parameters,
+ out Vector256 r,
+ out Vector256 g,
+ out Vector256 b)
+ {
+ cb -= parameters.ChromaMidpoint;
+ cr -= parameters.ChromaMidpoint;
+ r = Vector256.Clamp(y + (((parameters.RedCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector256.Zero, parameters.Maximum);
+ g = Vector256.Clamp(y + (((parameters.GreenCb * cb) + (parameters.GreenCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector256.Zero, parameters.Maximum);
+ b = Vector256.Clamp(y + (((parameters.BlueCb * cb) + parameters.RoundingBias) >> CoefficientShift), Vector256.Zero, parameters.Maximum);
+ }
+
+ ///
+ /// Converts sixteen coefficient-based H.273 YCbCr samples to eight-bit RGB lanes.
+ ///
+ /// The luma lanes.
+ /// The blue-difference lanes.
+ /// The red-difference lanes.
+ /// The fixed-point matrix coefficient lanes.
+ /// The converted red lanes.
+ /// The converted green lanes.
+ /// The converted blue lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Convert(
+ Vector512 y,
+ Vector512 cb,
+ Vector512 cr,
+ in Vector512Parameters parameters,
+ out Vector512 r,
+ out Vector512 g,
+ out Vector512 b)
+ {
+ cb -= parameters.ChromaMidpoint;
+ cr -= parameters.ChromaMidpoint;
+ r = Vector512.Clamp(y + (((parameters.RedCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector512.Zero, parameters.Maximum);
+ g = Vector512.Clamp(y + (((parameters.GreenCb * cb) + (parameters.GreenCr * cr) + parameters.RoundingBias) >> CoefficientShift), Vector512.Zero, parameters.Maximum);
+ b = Vector512.Clamp(y + (((parameters.BlueCb * cb) + parameters.RoundingBias) >> CoefficientShift), Vector512.Zero, parameters.Maximum);
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.cs
new file mode 100644
index 000000000..5fb3e0dd7
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Converter.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Formats.Heif.Color;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
+
+///
+/// Converts full-range eight-bit HEVC 4:2:0 planes with an unspecified matrix to packed RGB pixels.
+///
+internal static partial class HevcYuv420ToRgb8Converter
+{
+ ///
+ /// The fixed-point precision used for H.273 matrix coefficients.
+ ///
+ private const int CoefficientShift = 8;
+
+ ///
+ /// The half-unit bias used before fixed-point coefficient results are shifted to integer samples.
+ ///
+ private const int RoundingBias = 1 << (CoefficientShift - 1);
+
+ ///
+ /// The neutral code value for full-range eight-bit chroma.
+ ///
+ private const int ChromaMidpoint = 128;
+
+ ///
+ /// Determines whether the specialized integer conversion supports the supplied picture and color description.
+ ///
+ /// The reconstructed HEVC picture.
+ /// The effective H.273 color description.
+ /// The resolved H.273 conversion operation.
+ /// when the picture can use this converter; otherwise, .
+ public static bool IsSupported(HevcPictureBuffer picture, CicpProfile colorProfile, HeifColorConversionMode mode)
+ => picture.ChromaFormat == 1
+ && !picture.SeparateColorPlane
+ && picture.BitDepthLuma == 8
+ && picture.BitDepthChroma == 8
+ && colorProfile.FullRange
+ && colorProfile.MatrixCoefficients == CicpMatrixCoefficients.Unspecified
+ && mode == HeifColorConversionMode.Coefficients;
+
+ ///
+ /// Converts a supported HEVC picture to packed pixels using integer SIMD with a scalar tail.
+ ///
+ /// The destination pixel type.
+ /// The configuration used for allocation and pixel conversion.
+ /// The reconstructed HEVC picture.
+ /// The destination image frame.
+ /// The resolved H.273 conversion parameters.
+ /// The horizontal luma-sample offset of the output window.
+ /// The vertical luma-sample offset of the output window.
+ public static void Convert(
+ Configuration configuration,
+ HevcPictureBuffer picture,
+ ImageFrame image,
+ in HeifColorConversionParameters parameters,
+ int sourceX,
+ int sourceY)
+ where TPixel : unmanaged, IPixel
+ {
+ ConversionParameters conversionParameters = new(in parameters);
+ using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(image.Width * 3);
+ Span components = componentOwner.GetSpan();
+ Span red = components[..image.Width];
+ Span green = components.Slice(image.Width, image.Width);
+ Span blue = components.Slice(image.Width * 2, image.Width);
+
+ for (int y = 0; y < image.Height; y++)
+ {
+ int lumaY = sourceY + y;
+
+ // HEVC expresses 4:2:0 conformance-window offsets in complete chroma sample units, so both source
+ // offsets are even here. The unspecified-matrix presentation replicates each native chroma sample
+ // across its 2x2 luma cell before applying the default BT.601 coefficients.
+ ReadOnlySpan luma = picture.GetRowSpan(HevcPlane.Y, lumaY).Slice(sourceX, image.Width);
+ ReadOnlySpan chromaBlue = picture.GetRowSpan(HevcPlane.Cb, lumaY >> 1).Slice(sourceX >> 1);
+ ReadOnlySpan chromaRed = picture.GetRowSpan(HevcPlane.Cr, lumaY >> 1).Slice(sourceX >> 1);
+
+ ConvertRow(luma, chromaBlue, chromaRed, red, green, blue, in conversionParameters);
+
+ Span destination = image.PixelBuffer.DangerousGetRowSpan(y);
+ PixelOperations.Instance.PackFromRgbPlanes(red, green, blue, destination);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcYuvConverter.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs
similarity index 96%
rename from src/ImageSharp/Formats/Heif/Hevc/HevcYuvConverter.cs
rename to src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs
index 012ff858e..dfc2d0a6e 100644
--- a/src/ImageSharp/Formats/Heif/Hevc/HevcYuvConverter.cs
+++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs
@@ -11,12 +11,12 @@ using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase;
-namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
+namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
///
/// Converts between reconstructed HEVC component planes and packed ImageSharp pixels.
///
-internal static class HevcYuvConverter
+internal static partial class HevcYuvConverter
{
///
/// The largest value represented by an eight-bit packed RGB component.
@@ -60,6 +60,15 @@ internal static class HevcYuvConverter
where TPixel : unmanaged, IPixel
{
HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode);
+
+ // H.273 resolves an unspecified matrix to BT.601 coefficients. The common full-range eight-bit 4:2:0
+ // presentation can therefore remain in the integer sample domain and avoid float staging and rounding.
+ if (HevcYuv420ToRgb8Converter.IsSupported(picture, colorProfile, mode))
+ {
+ HevcYuv420ToRgb8Converter.Convert(configuration, picture, image, in parameters, sourceX, sourceY);
+ return;
+ }
+
HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, picture.ChromaFormat == 0);
YuvToRgbRowConverter converter = new(configuration, picture, image, colorConverter, chromaSampleLocation, sourceX, sourceY);
using IMemoryOwner scratchOwner = configuration.MemoryAllocator.Allocate(converter.BufferLength);
@@ -67,11 +76,9 @@ internal static class HevcYuvConverter
if (converter.UsesBytePacking)
{
- using IMemoryOwner proxyOwner = configuration.MemoryAllocator.Allocate(image.Width + 3);
- Span proxy = proxyOwner.GetSpan()[..(image.Width + 3)];
for (int y = 0; y < image.Height; y++)
{
- converter.Convert(y, scratch, proxy);
+ converter.Convert(y, scratch);
}
return;
@@ -79,7 +86,7 @@ internal static class HevcYuvConverter
for (int y = 0; y < image.Height; y++)
{
- converter.Convert(y, scratch, Span.Empty);
+ converter.Convert(y, scratch);
}
}
@@ -317,8 +324,7 @@ internal static class HevcYuvConverter
///
/// The zero-based luma row.
/// The reusable pooled row buffer.
- /// The padded byte-packing destination when the image row has insufficient padding.
- public void Convert(int y, Span scratch, Span proxy)
+ public void Convert(int y, Span scratch)
{
int width = this.image.Width;
Span red = scratch[..width];
@@ -391,15 +397,7 @@ internal static class HevcYuvConverter
SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes);
SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes);
- if (this.image.PixelBuffer.DangerousTryGetPaddedRowSpan(y, 3, out Span paddedDestination))
- {
- PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, paddedDestination);
- }
- else
- {
- PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, proxy);
- proxy[..width].CopyTo(destination);
- }
+ PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, destination);
return;
}
diff --git a/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs
index 4ba80c86c..e6899994d 100644
--- a/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs
+++ b/src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Hevc;
+using SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs
index 11f6ee658..010d9498b 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/HevcColorConversionBenchmarks.cs
@@ -3,6 +3,7 @@
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
+using SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
index e66fc7cff..a8e1528bf 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
@@ -84,10 +84,7 @@ public class HeifDecoderTests
HeifMetadata metadata = image.Metadata.GetHeifMetadata();
image.DebugSave(provider);
- // The extracted native YUV tiles have byte-exact HM coverage. Pinned libheif 1.23.1 selects its cheaper fused
- // nearest-neighbor RGB path by default, so the full-image oracle permits the bounded difference from our
- // bilinear reconstruction while still covering grids, alpha composition, color conversion, and presentation.
- image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.6F), provider);
+ image.CompareToReferenceOutput(ImageComparer.Exact, provider);
Assert.Equal(new Size(width, height), image.Size);
Assert.Equal(HeifCompressionMethod.Hevc, metadata.CompressionMethod);
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs
index 1b66a49b1..b6ea5b42c 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcYuvConverterTests.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Hevc;
+using SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;