diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 1cf805497..a27d40024 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -34,9 +34,11 @@ Checkboxes may be marked complete only when the implementation and the verificat
- [x] Preserve the exact ICC payload from an independently encoded AVIF primary item.
- [x] Prove that a genuine non-sRGB AVIF profile changes decoded pixels and matches the independently converted source image within the documented AV1 tolerance.
- [x] Verify canonical-sRGB compaction and metadata skipping independently from ICC preservation and color conversion.
- - [x] Replace the AV1 RGB/YUV arithmetic layer with JPEG's closed-generic static operator pattern in both directions: frame-scoped operator selection, in-place planar decode, RGB inputs with `out` component planes for encode, `Vector512`/`Vector256`/`Vector128` traversal, and one scalar tail.
+ - [x] Replace the AV1 RGB/YUV arithmetic layer with JPEG's closed-generic static operator pattern in both directions: frame-scoped operator selection, in-place planar decode, RGB inputs with `out` component planes for encode, `Vector512`/`Vector256`/`Vector128` traversal, one scalar tail, and JPEG's optimized byte-plane pack/unpack contracts for eight-bit pixels.
- [x] Keep codec row scheduling sequential and reuse frame-scoped allocator-backed buffers. Reuse JPEG's optimized `UnpackIntoRgbPlanes` contract for 8-bit encoding and pooled `Rgb48` staging for 10/12-bit encoding so high-precision input is not truncated through an eight-bit or `Vector4` intermediate.
- [x] Implement SIMD range expansion, chroma reconstruction, matrix/transfer conversion, clamping, and RGB packing for coefficient YCbCr, identity GBR, YCgCo, constant luminance, SMPTE ST 2085, and PQ/HLG ICtCp, with focused SIMD-lane/scalar-tail parity coverage.
+ - [x] Implement the H.273 V4 IPT-C2 and reversible YCgCo-Re/YCgCo-Ro transforms in both directions as closed SIMD operators, including their reduced RGB precision, signed integer lifting, full- and limited-range behavior, and 4:4:4 constraint.
+ - [x] Verify IPT-C2 against independently calculated matrix values and verify YCgCo-Re/YCgCo-Ro against exact 8/10/12-bit code values, limited-range black/white values, and SIMD-lane/scalar-tail parity.
- [x] Implement SIMD 4:2:0 and 4:2:2 encoder downsampling with odd-width and odd-height tails, and verify the stored 8/12-bit chroma samples against independently encoded full-resolution planes.
- [ ] Complete the SIMD YUV/CICP paths for every supported AV1 bit depth, chroma format, range, matrix, transfer function, color primary, and chroma position, with scalar fallback only when hardware vectorization is unavailable or the operation is inherently non-vectorizable.
- [x] Apply ICC conversion only after the SIMD YUV/CICP stage, alpha composition, grid assembly, and presentation transforms have produced the presented RGB image; retain ImageSharp's shared ICC converter and optimize reusable bulk kernels rather than creating a HEIF-specific color-management implementation.
@@ -199,7 +201,7 @@ This snapshot pins or classifies the available references and failures; it does
| Managed implementation | Normative behavior | Reviewed implementation reference | Use |
| --- | --- | --- | --- |
-| `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, scalar row conversion, chroma reconstruction, and chroma downsampling | ITU-T H.273 (V4) equations 14-16 and 27-84, including limited/full-range scaling, chromaticity-derived equations 39-47, YCgCo equations 51-57, constant-luminance equations 66-75, and the PQ/HLG ICtCp matrices; AV1 section 6.4.2 chroma sample positions | Official ITU-T H.273 (V4) (07/2024); libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for full/limited-range conversion at 8, 10, and 12 bits. Decode and encode cover identity, YCgCo, coefficient-based NCL, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, and ICtCp across monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with AV1 chroma sample positioning. Limited-range YCgCo retains the 219-code scale inherited from its R/G/B inputs instead of applying YCbCr's unrelated 224-code chroma range. Chromaticity derivation uses every defined H.273 primary and matches libavif's BT.709 fallback for unspecified or reserved primaries. The ICtCp inverse is derived from the exact H.273 integer matrices rather than an unrelated display conversion. Later SIMD paths must preserve this scalar behavior. |
+| `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, scalar row conversion, chroma reconstruction, and chroma downsampling | ITU-T H.273 (V4) equations 14-16 and 27-84, including limited/full-range scaling, chromaticity-derived equations 39-47, YCgCo equations 51-57, constant-luminance equations 66-75, IPT-C2, YCgCo-Re/YCgCo-Ro, and the PQ/HLG ICtCp matrices; AV1 section 6.4.2 chroma sample positions | Official ITU-T H.273 (V4) (07/2024); libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for full/limited-range conversion at 8, 10, and 12 bits. Decode and encode cover every H.273 V4 matrix code point through 17: identity, YCgCo, coefficient-based NCL, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, ICtCp, IPT-C2, YCgCo-Re, and YCgCo-Ro. The common paths cover monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with AV1 chroma sample positioning; identity and the reversible lifting transforms enforce their full-chroma constraint. Limited-range YCgCo retains the 219-code scale inherited from its R/G/B inputs instead of applying YCbCr's unrelated 224-code chroma range. YCgCo-Re and YCgCo-Ro apply range adjustment at their reduced-precision RGB code values before signed integer lifting. Chromaticity derivation uses every defined H.273 primary and matches libavif's BT.709 fallback for unspecified or reserved primaries. The ICtCp and IPT-C2 inverses are derived from their exact H.273 matrices rather than unrelated display conversions. The closed operators provide `Vector512`, `Vector256`, and `Vector128` implementations with one scalar tail. |
| `Av1TransferFunctions` | ITU-T H.273 (V4) Table 3 transfer characteristics 1-18 | Official ITU-T H.273 (V4) (07/2024); libavif `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826` | Apply every AV1-signallable transfer function required by constant-luminance and ICtCp color conversion. Retain the H.273 normalized PQ and HLG definitions; do not import libavif's display-oriented 203-nit scaling or HLG OOTF into codec sample interpretation. Use libavif's midpoint convention only for the non-bijective zero code of the two logarithmic curves. |
| `ObuReader.ReadSequenceHeader`, `ReadUncompressedFrameHeader`, decoder-model parsing, and operating-parameter consumption | AV1 sections 5.5.2 through 5.5.4 sequence timing and decoder-model syntax, section 5.9.2 uncompressed frame-header syntax, and section 5.9.31 temporal-point syntax | libaom `av1/decoder/decodeframe.c` functions `av1_read_decoder_model_info`, `av1_read_op_parameters_info`, `read_temporal_point_info`, and `read_uncompressed_header`, plus `common/av1_config.c`, at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Read the normative 32-bit decoding-tick field; consume operating-point buffer delays and the layer-applicable frame removal times needed to keep a non-reduced still-image sequence aligned; and read presentation time only under the normative decoder-model condition. Retain no scheduling or playback behavior from those values and introduce no ISO BMFF timing, track, sample-table, or sequence surface. |
| `Av1FrameBuffer` high-bit-depth sample layout and `Av1YuvConverter` 10/12-bit packed-pixel conversion | AV1 section 6.4.1 bit depth and H.273 sample-range scaling | libaom `aom_scale/yv12config.h`, `av1/common/idct.c`, and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f`; libavif `src/avif.c` and `src/reformat.c` at `092276ce89098ead06db80975173191e5fee1826` | Establish two-byte native sample storage with sample-unit strides for 10/12-bit reconstruction and use ImageSharp's existing `Rgb48` pixel-operation paths in both directions so packed-pixel staging does not reduce high-bit-depth samples to eight bits. |
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs
index e2581248e..79474a252 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs
@@ -22,6 +22,10 @@ internal readonly struct Av1ColorConversionParameters
/// The encoded luma range.
/// The encoded chroma midpoint.
/// The encoded chroma range.
+ /// The largest encoded sample value.
+ /// The reversible transform's RGB code-value bias.
+ /// The reversible transform's RGB code-value range.
+ /// The largest reversible transform RGB code value.
public Av1ColorConversionParameters(
float kr,
float kg,
@@ -31,7 +35,11 @@ internal readonly struct Av1ColorConversionParameters
float lumaBias,
float lumaScale,
float chromaBias,
- float chromaScale)
+ float chromaScale,
+ float encodedSampleMaximum,
+ float rgbBias,
+ float rgbScale,
+ float rgbSampleMaximum)
{
this.Kr = kr;
this.Kg = kg;
@@ -46,6 +54,10 @@ internal readonly struct Av1ColorConversionParameters
this.LumaScale = lumaScale;
this.ChromaBias = chromaBias;
this.ChromaScale = chromaScale;
+ this.EncodedSampleMaximum = encodedSampleMaximum;
+ this.RgbBias = rgbBias;
+ this.RgbScale = rgbScale;
+ this.RgbSampleMaximum = rgbSampleMaximum;
}
///
@@ -112,6 +124,26 @@ internal readonly struct Av1ColorConversionParameters
/// Gets the encoded chroma range.
///
public float ChromaScale { get; }
+
+ ///
+ /// Gets the largest encoded sample value.
+ ///
+ public float EncodedSampleMaximum { get; }
+
+ ///
+ /// Gets the reversible transform's RGB code-value bias.
+ ///
+ public float RgbBias { get; }
+
+ ///
+ /// Gets the reversible transform's RGB code-value range.
+ ///
+ public float RgbScale { get; }
+
+ ///
+ /// Gets the largest reversible transform RGB code value.
+ ///
+ public float RgbSampleMaximum { get; }
}
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IptC2Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IptC2Operator.cs
new file mode 100644
index 000000000..422c1bcf9
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IptC2Operator.cs
@@ -0,0 +1,502 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1;
+
+internal abstract partial class Av1ColorConverterBase
+{
+ ///
+ /// Implements IPT-C2 conversion for scalar and SIMD lanes.
+ ///
+ internal readonly struct Av1IptC2ColorOperator : IAv1ColorOperator
+ {
+ ///
+ /// The linear red contribution to L.
+ ///
+ public const float RedToL = 1747F / 4096F;
+
+ ///
+ /// The linear green contribution to L.
+ ///
+ public const float GreenToL = 2169F / 4096F;
+
+ ///
+ /// The linear blue contribution to L.
+ ///
+ public const float BlueToL = 180F / 4096F;
+
+ ///
+ /// The linear red contribution to M.
+ ///
+ public const float RedToM = 673F / 4096F;
+
+ ///
+ /// The linear green contribution to M.
+ ///
+ public const float GreenToM = 3029F / 4096F;
+
+ ///
+ /// The linear blue contribution to M.
+ ///
+ public const float BlueToM = 394F / 4096F;
+
+ ///
+ /// The linear red contribution to S.
+ ///
+ public const float RedToS = 50F / 4096F;
+
+ ///
+ /// The linear green contribution to S.
+ ///
+ public const float GreenToS = 207F / 4096F;
+
+ ///
+ /// The linear blue contribution to S.
+ ///
+ public const float BlueToS = 3839F / 4096F;
+
+ ///
+ /// The nonlinear L contribution to intensity.
+ ///
+ public const float LToIntensity = 1638F / 4096F;
+
+ ///
+ /// The nonlinear M contribution to intensity.
+ ///
+ public const float MToIntensity = 1638F / 4096F;
+
+ ///
+ /// The nonlinear S contribution to intensity.
+ ///
+ public const float SToIntensity = 820F / 4096F;
+
+ ///
+ /// The nonlinear L contribution to the protan axis.
+ ///
+ public const float LToProtan = 18248F / 4096F;
+
+ ///
+ /// The nonlinear M contribution to the protan axis.
+ ///
+ public const float MToProtan = -19870F / 4096F;
+
+ ///
+ /// The nonlinear S contribution to the protan axis.
+ ///
+ public const float SToProtan = 1622F / 4096F;
+
+ ///
+ /// The nonlinear L contribution to the tritan axis.
+ ///
+ public const float LToTritan = 3300F / 4096F;
+
+ ///
+ /// The nonlinear M contribution to the tritan axis.
+ ///
+ public const float MToTritan = 1463F / 4096F;
+
+ ///
+ /// The nonlinear S contribution to the tritan axis.
+ ///
+ public const float SToTritan = -4763F / 4096F;
+
+ ///
+ /// The protan contribution to nonlinear L.
+ ///
+ public const float ProtanToL = 0.0975578875686935F;
+
+ ///
+ /// The tritan contribution to nonlinear L.
+ ///
+ public const float TritanToL = 0.20538292958984272F;
+
+ ///
+ /// The protan contribution to nonlinear M.
+ ///
+ public const float ProtanToM = -0.11388362209560723F;
+
+ ///
+ /// The tritan contribution to nonlinear M.
+ ///
+ public const float TritanToM = 0.13337828363655785F;
+
+ ///
+ /// The protan contribution to nonlinear S.
+ ///
+ public const float ProtanToS = 0.032611650189127685F;
+
+ ///
+ /// The tritan contribution to nonlinear S.
+ ///
+ public const float TritanToS = -0.6766961795912734F;
+
+ ///
+ /// The linear L contribution to red.
+ ///
+ public const float LToRed = 3.2374662424353895F;
+
+ ///
+ /// The linear M contribution to red.
+ ///
+ public const float MToRed = -2.324205800020636F;
+
+ ///
+ /// The linear S contribution to red.
+ ///
+ public const float SToRed = 0.08673955758524626F;
+
+ ///
+ /// The linear L contribution to green.
+ ///
+ public const float LToGreen = -0.7188754693535147F;
+
+ ///
+ /// The linear M contribution to green.
+ ///
+ public const float MToGreen = 1.877899954242238F;
+
+ ///
+ /// The linear S contribution to green.
+ ///
+ public const float SToGreen = -0.1590244848887234F;
+
+ ///
+ /// The linear L contribution to blue.
+ ///
+ public const float LToBlue = -0.003403513926958051F;
+
+ ///
+ /// The linear M contribution to blue.
+ ///
+ public const float MToBlue = -0.07098593397424108F;
+
+ ///
+ /// The linear S contribution to blue.
+ ///
+ public const float SToBlue = 1.074389447901199F;
+
+ ///
+ public static bool ChromaUsesLumaRange => false;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(ref float intensity, ref float protan, ref float tritan, in Av1ColorConversionParameters parameters)
+ {
+ // IPT-C2 stores opponent axes around intensity in nonlinear LMS. Undo both matrices around the
+ // signaled transfer function so the final RGB values remain in the source signal domain.
+ float nonlinearL = intensity + (ProtanToL * protan) + (TritanToL * tritan);
+ float nonlinearM = intensity + (ProtanToM * protan) + (TritanToM * tritan);
+ float nonlinearS = intensity + (ProtanToS * protan) + (TritanToS * tritan);
+ float linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
+ float linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
+ float linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
+ float linearRed = (LToRed * linearL) + (MToRed * linearM) + (SToRed * linearS);
+ float linearGreen = (LToGreen * linearL) + (MToGreen * linearM) + (SToGreen * linearS);
+ float linearBlue = (LToBlue * linearL) + (MToBlue * linearM) + (SToBlue * linearS);
+
+ intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
+ protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
+ tritan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(
+ ref Vector128 intensity,
+ ref Vector128 protan,
+ ref Vector128 tritan,
+ in Av1ColorConversionParameters parameters)
+ {
+ Vector128 nonlinearL = Vector128.MultiplyAddEstimate(
+ Vector128.Create(TritanToL),
+ tritan,
+ Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToL), protan, intensity));
+
+ Vector128 nonlinearM = Vector128.MultiplyAddEstimate(
+ Vector128.Create(TritanToM),
+ tritan,
+ Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToM), protan, intensity));
+
+ Vector128 nonlinearS = Vector128.MultiplyAddEstimate(
+ Vector128.Create(TritanToS),
+ tritan,
+ Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToS), protan, intensity));
+
+ Vector128 linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
+ Vector128 linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
+ Vector128 linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
+ Vector128 linearRed = Vector128.MultiplyAddEstimate(
+ Vector128.Create(SToRed), linearS, Vector128.MultiplyAddEstimate(Vector128.Create(MToRed), linearM, Vector128.Create(LToRed) * linearL));
+
+ Vector128 linearGreen = Vector128.MultiplyAddEstimate(
+ Vector128.Create(SToGreen), linearS, Vector128.MultiplyAddEstimate(Vector128.Create(MToGreen), linearM, Vector128.Create(LToGreen) * linearL));
+
+ Vector128 linearBlue = Vector128.MultiplyAddEstimate(
+ Vector128.Create(SToBlue), linearS, Vector128.MultiplyAddEstimate(Vector128.Create(MToBlue), linearM, Vector128.Create(LToBlue) * linearL));
+
+ intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
+ protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
+ tritan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(
+ ref Vector256 intensity,
+ ref Vector256 protan,
+ ref Vector256 tritan,
+ in Av1ColorConversionParameters parameters)
+ {
+ Vector256 nonlinearL = Vector256.MultiplyAddEstimate(
+ Vector256.Create(TritanToL),
+ tritan,
+ Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToL), protan, intensity));
+
+ Vector256 nonlinearM = Vector256.MultiplyAddEstimate(
+ Vector256.Create(TritanToM),
+ tritan,
+ Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToM), protan, intensity));
+
+ Vector256 nonlinearS = Vector256.MultiplyAddEstimate(
+ Vector256.Create(TritanToS),
+ tritan,
+ Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToS), protan, intensity));
+
+ Vector256 linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
+ Vector256 linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
+ Vector256 linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
+ Vector256 linearRed = Vector256.MultiplyAddEstimate(
+ Vector256.Create(SToRed), linearS, Vector256.MultiplyAddEstimate(Vector256.Create(MToRed), linearM, Vector256.Create(LToRed) * linearL));
+
+ Vector256 linearGreen = Vector256.MultiplyAddEstimate(
+ Vector256.Create(SToGreen), linearS, Vector256.MultiplyAddEstimate(Vector256.Create(MToGreen), linearM, Vector256.Create(LToGreen) * linearL));
+
+ Vector256 linearBlue = Vector256.MultiplyAddEstimate(
+ Vector256.Create(SToBlue), linearS, Vector256.MultiplyAddEstimate(Vector256.Create(MToBlue), linearM, Vector256.Create(LToBlue) * linearL));
+
+ intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
+ protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
+ tritan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(
+ ref Vector512 intensity,
+ ref Vector512 protan,
+ ref Vector512 tritan,
+ in Av1ColorConversionParameters parameters)
+ {
+ Vector512 nonlinearL = Vector512.MultiplyAddEstimate(
+ Vector512.Create(TritanToL),
+ tritan,
+ Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToL), protan, intensity));
+
+ Vector512 nonlinearM = Vector512.MultiplyAddEstimate(
+ Vector512.Create(TritanToM),
+ tritan,
+ Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToM), protan, intensity));
+
+ Vector512 nonlinearS = Vector512.MultiplyAddEstimate(
+ Vector512.Create(TritanToS),
+ tritan,
+ Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToS), protan, intensity));
+
+ Vector512 linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
+ Vector512 linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
+ Vector512 linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
+ Vector512 linearRed = Vector512.MultiplyAddEstimate(
+ Vector512.Create(SToRed), linearS, Vector512.MultiplyAddEstimate(Vector512.Create(MToRed), linearM, Vector512.Create(LToRed) * linearL));
+
+ Vector512 linearGreen = Vector512.MultiplyAddEstimate(
+ Vector512.Create(SToGreen), linearS, Vector512.MultiplyAddEstimate(Vector512.Create(MToGreen), linearM, Vector512.Create(LToGreen) * linearL));
+
+ Vector512 linearBlue = Vector512.MultiplyAddEstimate(
+ Vector512.Create(SToBlue), linearS, Vector512.MultiplyAddEstimate(Vector512.Create(MToBlue), linearM, Vector512.Create(LToBlue) * linearL));
+
+ intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
+ protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
+ tritan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ float red,
+ float green,
+ float blue,
+ in Av1ColorConversionParameters parameters,
+ out float intensity,
+ out float protan,
+ out float tritan)
+ {
+ // The encoded RGB signal is linearized before the LMS matrix, then the signaled transfer function
+ // is reapplied to each LMS component before the fixed IPT-C2 opponent matrix.
+ float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
+ float linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
+ float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
+ float nonlinearL = Av1TransferFunctions.ToGamma(
+ parameters.TransferCharacteristics,
+ (RedToL * linearRed) + (GreenToL * linearGreen) + (BlueToL * linearBlue));
+
+ float nonlinearM = Av1TransferFunctions.ToGamma(
+ parameters.TransferCharacteristics,
+ (RedToM * linearRed) + (GreenToM * linearGreen) + (BlueToM * linearBlue));
+
+ float nonlinearS = Av1TransferFunctions.ToGamma(
+ parameters.TransferCharacteristics,
+ (RedToS * linearRed) + (GreenToS * linearGreen) + (BlueToS * linearBlue));
+
+ intensity = (LToIntensity * nonlinearL) + (MToIntensity * nonlinearM) + (SToIntensity * nonlinearS);
+ protan = (LToProtan * nonlinearL) + (MToProtan * nonlinearM) + (SToProtan * nonlinearS);
+ tritan = (LToTritan * nonlinearL) + (MToTritan * nonlinearM) + (SToTritan * nonlinearS);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ Vector128 red,
+ Vector128 green,
+ Vector128 blue,
+ in Av1ColorConversionParameters parameters,
+ out Vector128 intensity,
+ out Vector128 protan,
+ out Vector128 tritan)
+ {
+ Vector128 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
+ Vector128 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
+ Vector128 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
+ Vector128 linearL = Vector128.MultiplyAddEstimate(
+ Vector128.Create(BlueToL),
+ linearBlue,
+ Vector128.MultiplyAddEstimate(Vector128.Create(GreenToL), linearGreen, Vector128.Create(RedToL) * linearRed));
+
+ Vector128 linearM = Vector128.MultiplyAddEstimate(
+ Vector128.Create(BlueToM),
+ linearBlue,
+ Vector128.MultiplyAddEstimate(Vector128.Create(GreenToM), linearGreen, Vector128.Create(RedToM) * linearRed));
+
+ Vector128 linearS = Vector128.MultiplyAddEstimate(
+ Vector128.Create(BlueToS),
+ linearBlue,
+ Vector128.MultiplyAddEstimate(Vector128.Create(GreenToS), linearGreen, Vector128.Create(RedToS) * linearRed));
+
+ Vector128 nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL);
+ Vector128 nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM);
+ Vector128 nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS);
+ intensity = Vector128.MultiplyAddEstimate(
+ Vector128.Create(SToIntensity),
+ nonlinearS,
+ Vector128.MultiplyAddEstimate(Vector128.Create(MToIntensity), nonlinearM, Vector128.Create(LToIntensity) * nonlinearL));
+
+ protan = Vector128.MultiplyAddEstimate(
+ Vector128.Create(SToProtan),
+ nonlinearS,
+ Vector128.MultiplyAddEstimate(Vector128.Create(MToProtan), nonlinearM, Vector128.Create(LToProtan) * nonlinearL));
+
+ tritan = Vector128.MultiplyAddEstimate(
+ Vector128.Create(SToTritan),
+ nonlinearS,
+ Vector128.MultiplyAddEstimate(Vector128.Create(MToTritan), nonlinearM, Vector128.Create(LToTritan) * nonlinearL));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ Vector256 red,
+ Vector256 green,
+ Vector256 blue,
+ in Av1ColorConversionParameters parameters,
+ out Vector256 intensity,
+ out Vector256 protan,
+ out Vector256 tritan)
+ {
+ Vector256 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
+ Vector256 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
+ Vector256 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
+ Vector256 linearL = Vector256.MultiplyAddEstimate(
+ Vector256.Create(BlueToL),
+ linearBlue,
+ Vector256.MultiplyAddEstimate(Vector256.Create(GreenToL), linearGreen, Vector256.Create(RedToL) * linearRed));
+
+ Vector256 linearM = Vector256.MultiplyAddEstimate(
+ Vector256.Create(BlueToM),
+ linearBlue,
+ Vector256.MultiplyAddEstimate(Vector256.Create(GreenToM), linearGreen, Vector256.Create(RedToM) * linearRed));
+
+ Vector256 linearS = Vector256.MultiplyAddEstimate(
+ Vector256.Create(BlueToS),
+ linearBlue,
+ Vector256.MultiplyAddEstimate(Vector256.Create(GreenToS), linearGreen, Vector256.Create(RedToS) * linearRed));
+
+ Vector256 nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL);
+ Vector256 nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM);
+ Vector256 nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS);
+ intensity = Vector256.MultiplyAddEstimate(
+ Vector256.Create(SToIntensity),
+ nonlinearS,
+ Vector256.MultiplyAddEstimate(Vector256.Create(MToIntensity), nonlinearM, Vector256.Create(LToIntensity) * nonlinearL));
+
+ protan = Vector256.MultiplyAddEstimate(
+ Vector256.Create(SToProtan),
+ nonlinearS,
+ Vector256.MultiplyAddEstimate(Vector256.Create(MToProtan), nonlinearM, Vector256.Create(LToProtan) * nonlinearL));
+
+ tritan = Vector256.MultiplyAddEstimate(
+ Vector256.Create(SToTritan),
+ nonlinearS,
+ Vector256.MultiplyAddEstimate(Vector256.Create(MToTritan), nonlinearM, Vector256.Create(LToTritan) * nonlinearL));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ Vector512 red,
+ Vector512 green,
+ Vector512 blue,
+ in Av1ColorConversionParameters parameters,
+ out Vector512 intensity,
+ out Vector512 protan,
+ out Vector512 tritan)
+ {
+ Vector512 linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
+ Vector512 linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
+ Vector512 linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
+ Vector512 linearL = Vector512.MultiplyAddEstimate(
+ Vector512.Create(BlueToL),
+ linearBlue,
+ Vector512.MultiplyAddEstimate(Vector512.Create(GreenToL), linearGreen, Vector512.Create(RedToL) * linearRed));
+
+ Vector512 linearM = Vector512.MultiplyAddEstimate(
+ Vector512.Create(BlueToM),
+ linearBlue,
+ Vector512.MultiplyAddEstimate(Vector512.Create(GreenToM), linearGreen, Vector512.Create(RedToM) * linearRed));
+
+ Vector512 linearS = Vector512.MultiplyAddEstimate(
+ Vector512.Create(BlueToS),
+ linearBlue,
+ Vector512.MultiplyAddEstimate(Vector512.Create(GreenToS), linearGreen, Vector512.Create(RedToS) * linearRed));
+
+ Vector512 nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL);
+ Vector512 nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM);
+ Vector512 nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS);
+ intensity = Vector512.MultiplyAddEstimate(
+ Vector512.Create(SToIntensity),
+ nonlinearS,
+ Vector512.MultiplyAddEstimate(Vector512.Create(MToIntensity), nonlinearM, Vector512.Create(LToIntensity) * nonlinearL));
+
+ protan = Vector512.MultiplyAddEstimate(
+ Vector512.Create(SToProtan),
+ nonlinearS,
+ Vector512.MultiplyAddEstimate(Vector512.Create(MToProtan), nonlinearM, Vector512.Create(LToProtan) * nonlinearL));
+
+ tritan = Vector512.MultiplyAddEstimate(
+ Vector512.Create(SToTritan),
+ nonlinearS,
+ Vector512.MultiplyAddEstimate(Vector512.Create(MToTritan), nonlinearM, Vector512.Create(LToTritan) * nonlinearL));
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoReversibleOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoReversibleOperator.cs
new file mode 100644
index 000000000..c68dfb7de
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoReversibleOperator.cs
@@ -0,0 +1,222 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1;
+
+internal abstract partial class Av1ColorConverterBase
+{
+ ///
+ /// Implements the YCgCo-Re and YCgCo-Ro integer lifting transforms for scalar and SIMD lanes.
+ ///
+ internal readonly struct Av1YCgCoReversibleColorOperator : IAv1ColorOperator
+ {
+ ///
+ public static bool ChromaUsesLumaRange => false;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(ref float y, ref float cg, ref float co, in Av1ColorConversionParameters parameters)
+ {
+ int yCode = (int)MathF.Floor((y * parameters.EncodedSampleMaximum) + 0.5F);
+ int cgCode = (int)MathF.Floor((cg * parameters.EncodedSampleMaximum) + 0.5F);
+ int coCode = (int)MathF.Floor((co * parameters.EncodedSampleMaximum) + 0.5F);
+
+ // Signed arithmetic shifts are part of the reversible lifting definition. In particular, they preserve
+ // the specified floor division for negative odd Cg and Co values instead of truncating toward zero.
+ int temporary = yCode - (cgCode >> 1);
+ int greenCode = Numerics.Clamp(temporary + cgCode, 0, (int)parameters.RgbSampleMaximum);
+ int blueCode = Numerics.Clamp(temporary - (coCode >> 1), 0, (int)parameters.RgbSampleMaximum);
+ int redCode = Numerics.Clamp(blueCode + coCode, 0, (int)parameters.RgbSampleMaximum);
+ float inverseRgbScale = 1F / parameters.RgbScale;
+ y = (redCode - parameters.RgbBias) * inverseRgbScale;
+ cg = (greenCode - parameters.RgbBias) * inverseRgbScale;
+ co = (blueCode - parameters.RgbBias) * inverseRgbScale;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(
+ ref Vector128 y,
+ ref Vector128 cg,
+ ref Vector128 co,
+ in Av1ColorConversionParameters parameters)
+ {
+ Vector128 encodedMaximum = Vector128.Create(parameters.EncodedSampleMaximum);
+ Vector128 half = Vector128.Create(0.5F);
+ Vector128 yCode = Vector128.ConvertToInt32(Vector128.Floor((y * encodedMaximum) + half));
+ Vector128 cgCode = Vector128.ConvertToInt32(Vector128.Floor((cg * encodedMaximum) + half));
+ Vector128 coCode = Vector128.ConvertToInt32(Vector128.Floor((co * encodedMaximum) + half));
+
+ // Integer lanes preserve the normative arithmetic shifts; converting the lifting stages back to
+ // floating point would change negative odd Cg and Co values and break reversibility.
+ Vector128 temporary = yCode - Vector128.ShiftRightArithmetic(cgCode, 1);
+ Vector128 zero = Vector128.Zero;
+ Vector128 rgbMaximum = Vector128.Create((int)parameters.RgbSampleMaximum);
+ Vector128 greenCode = Vector128.Min(Vector128.Max(temporary + cgCode, zero), rgbMaximum);
+ Vector128 blueCode = Vector128.Min(Vector128.Max(temporary - Vector128.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum);
+ Vector128 redCode = Vector128.Min(Vector128.Max(blueCode + coCode, zero), rgbMaximum);
+ Vector128 rgbBias = Vector128.Create(parameters.RgbBias);
+ Vector128 inverseRgbScale = Vector128.Create(1F / parameters.RgbScale);
+ y = (Vector128.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale;
+ cg = (Vector128.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale;
+ co = (Vector128.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(
+ ref Vector256 y,
+ ref Vector256 cg,
+ ref Vector256 co,
+ in Av1ColorConversionParameters parameters)
+ {
+ Vector256 encodedMaximum = Vector256.Create(parameters.EncodedSampleMaximum);
+ Vector256 half = Vector256.Create(0.5F);
+ Vector256 yCode = Vector256.ConvertToInt32(Vector256.Floor((y * encodedMaximum) + half));
+ Vector256 cgCode = Vector256.ConvertToInt32(Vector256.Floor((cg * encodedMaximum) + half));
+ Vector256 coCode = Vector256.ConvertToInt32(Vector256.Floor((co * encodedMaximum) + half));
+ Vector256 temporary = yCode - Vector256.ShiftRightArithmetic(cgCode, 1);
+ Vector256 zero = Vector256.Zero;
+ Vector256 rgbMaximum = Vector256.Create((int)parameters.RgbSampleMaximum);
+ Vector256 greenCode = Vector256.Min(Vector256.Max(temporary + cgCode, zero), rgbMaximum);
+ Vector256 blueCode = Vector256.Min(Vector256.Max(temporary - Vector256.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum);
+ Vector256 redCode = Vector256.Min(Vector256.Max(blueCode + coCode, zero), rgbMaximum);
+ Vector256 rgbBias = Vector256.Create(parameters.RgbBias);
+ Vector256 inverseRgbScale = Vector256.Create(1F / parameters.RgbScale);
+ y = (Vector256.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale;
+ cg = (Vector256.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale;
+ co = (Vector256.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertToRgb(
+ ref Vector512 y,
+ ref Vector512 cg,
+ ref Vector512 co,
+ in Av1ColorConversionParameters parameters)
+ {
+ Vector512 encodedMaximum = Vector512.Create(parameters.EncodedSampleMaximum);
+ Vector512 half = Vector512.Create(0.5F);
+ Vector512 yCode = Vector512.ConvertToInt32(Vector512.Floor((y * encodedMaximum) + half));
+ Vector512 cgCode = Vector512.ConvertToInt32(Vector512.Floor((cg * encodedMaximum) + half));
+ Vector512 coCode = Vector512.ConvertToInt32(Vector512.Floor((co * encodedMaximum) + half));
+ Vector512 temporary = yCode - Vector512.ShiftRightArithmetic(cgCode, 1);
+ Vector512 zero = Vector512.Zero;
+ Vector512 rgbMaximum = Vector512.Create((int)parameters.RgbSampleMaximum);
+ Vector512 greenCode = Vector512.Min(Vector512.Max(temporary + cgCode, zero), rgbMaximum);
+ Vector512 blueCode = Vector512.Min(Vector512.Max(temporary - Vector512.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum);
+ Vector512 redCode = Vector512.Min(Vector512.Max(blueCode + coCode, zero), rgbMaximum);
+ Vector512 rgbBias = Vector512.Create(parameters.RgbBias);
+ Vector512 inverseRgbScale = Vector512.Create(1F / parameters.RgbScale);
+ y = (Vector512.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale;
+ cg = (Vector512.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale;
+ co = (Vector512.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ float red,
+ float green,
+ float blue,
+ in Av1ColorConversionParameters parameters,
+ out float y,
+ out float cg,
+ out float co)
+ {
+ int redCode = (int)MathF.Floor((red * parameters.RgbScale) + parameters.RgbBias + 0.5F);
+ int greenCode = (int)MathF.Floor((green * parameters.RgbScale) + parameters.RgbBias + 0.5F);
+ int blueCode = (int)MathF.Floor((blue * parameters.RgbScale) + parameters.RgbBias + 0.5F);
+ int coCode = redCode - blueCode;
+ int temporary = blueCode + (coCode >> 1);
+ int cgCode = greenCode - temporary;
+ int yCode = temporary + (cgCode >> 1);
+ float inverseEncodedMaximum = 1F / parameters.EncodedSampleMaximum;
+ y = yCode * inverseEncodedMaximum;
+ cg = cgCode * inverseEncodedMaximum;
+ co = coCode * inverseEncodedMaximum;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ Vector128 red,
+ Vector128 green,
+ Vector128 blue,
+ in Av1ColorConversionParameters parameters,
+ out Vector128 y,
+ out Vector128 cg,
+ out Vector128 co)
+ {
+ Vector128 rgbScale = Vector128.Create(parameters.RgbScale);
+ Vector128 quantizationOffset = Vector128.Create(parameters.RgbBias + 0.5F);
+ Vector128 redCode = Vector128.ConvertToInt32(Vector128.Floor((red * rgbScale) + quantizationOffset));
+ Vector128 greenCode = Vector128.ConvertToInt32(Vector128.Floor((green * rgbScale) + quantizationOffset));
+ Vector128 blueCode = Vector128.ConvertToInt32(Vector128.Floor((blue * rgbScale) + quantizationOffset));
+ Vector128 coCode = redCode - blueCode;
+ Vector128 temporary = blueCode + Vector128.ShiftRightArithmetic(coCode, 1);
+ Vector128 cgCode = greenCode - temporary;
+ Vector128 yCode = temporary + Vector128.ShiftRightArithmetic(cgCode, 1);
+ Vector128 inverseEncodedMaximum = Vector128.Create(1F / parameters.EncodedSampleMaximum);
+ y = Vector128.ConvertToSingle(yCode) * inverseEncodedMaximum;
+ cg = Vector128.ConvertToSingle(cgCode) * inverseEncodedMaximum;
+ co = Vector128.ConvertToSingle(coCode) * inverseEncodedMaximum;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ Vector256 red,
+ Vector256 green,
+ Vector256 blue,
+ in Av1ColorConversionParameters parameters,
+ out Vector256 y,
+ out Vector256 cg,
+ out Vector256 co)
+ {
+ Vector256 rgbScale = Vector256.Create(parameters.RgbScale);
+ Vector256 quantizationOffset = Vector256.Create(parameters.RgbBias + 0.5F);
+ Vector256 redCode = Vector256.ConvertToInt32(Vector256.Floor((red * rgbScale) + quantizationOffset));
+ Vector256 greenCode = Vector256.ConvertToInt32(Vector256.Floor((green * rgbScale) + quantizationOffset));
+ Vector256 blueCode = Vector256.ConvertToInt32(Vector256.Floor((blue * rgbScale) + quantizationOffset));
+ Vector256 coCode = redCode - blueCode;
+ Vector256 temporary = blueCode + Vector256.ShiftRightArithmetic(coCode, 1);
+ Vector256 cgCode = greenCode - temporary;
+ Vector256 yCode = temporary + Vector256.ShiftRightArithmetic(cgCode, 1);
+ Vector256 inverseEncodedMaximum = Vector256.Create(1F / parameters.EncodedSampleMaximum);
+ y = Vector256.ConvertToSingle(yCode) * inverseEncodedMaximum;
+ cg = Vector256.ConvertToSingle(cgCode) * inverseEncodedMaximum;
+ co = Vector256.ConvertToSingle(coCode) * inverseEncodedMaximum;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ConvertFromRgb(
+ Vector512 red,
+ Vector512 green,
+ Vector512 blue,
+ in Av1ColorConversionParameters parameters,
+ out Vector512 y,
+ out Vector512 cg,
+ out Vector512 co)
+ {
+ Vector512 rgbScale = Vector512.Create(parameters.RgbScale);
+ Vector512 quantizationOffset = Vector512.Create(parameters.RgbBias + 0.5F);
+ Vector512 redCode = Vector512.ConvertToInt32(Vector512.Floor((red * rgbScale) + quantizationOffset));
+ Vector512 greenCode = Vector512.ConvertToInt32(Vector512.Floor((green * rgbScale) + quantizationOffset));
+ Vector512 blueCode = Vector512.ConvertToInt32(Vector512.Floor((blue * rgbScale) + quantizationOffset));
+ Vector512 coCode = redCode - blueCode;
+ Vector512 temporary = blueCode + Vector512.ShiftRightArithmetic(coCode, 1);
+ Vector512 cgCode = greenCode - temporary;
+ Vector512 yCode = temporary + Vector512.ShiftRightArithmetic(cgCode, 1);
+ Vector512 inverseEncodedMaximum = Vector512.Create(1F / parameters.EncodedSampleMaximum);
+ y = Vector512.ConvertToSingle(yCode) * inverseEncodedMaximum;
+ cg = Vector512.ConvertToSingle(cgCode) * inverseEncodedMaximum;
+ co = Vector512.ConvertToSingle(coCode) * inverseEncodedMaximum;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs
index 970f830cf..aeeccd508 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs
@@ -37,6 +37,16 @@ internal enum Av1ColorConversionMode
/// The BT.2100 ICtCp color transform.
///
ICtCp,
+
+ ///
+ /// The IPT-C2 color transform.
+ ///
+ IptC2,
+
+ ///
+ /// The YCgCo-Re or YCgCo-Ro reversible color transform.
+ ///
+ YCgCoReversible,
}
///
@@ -121,6 +131,8 @@ internal abstract partial class Av1ColorConverterBase
Av1ColorConversionMode.Smpte2085 => new Av1ColorConverter(in parameters, isMonochrome),
Av1ColorConversionMode.ConstantLuminance => new Av1ColorConverter(in parameters, isMonochrome),
Av1ColorConversionMode.ICtCp => new Av1ColorConverter(in parameters, isMonochrome),
+ Av1ColorConversionMode.IptC2 => new Av1ColorConverter(in parameters, isMonochrome),
+ Av1ColorConversionMode.YCgCoReversible => new Av1ColorConverter(in parameters, isMonochrome),
_ => new Av1ColorConverter(in parameters, isMonochrome),
};
}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs
index 61d029c79..515f05d08 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs
@@ -3,6 +3,7 @@
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -114,7 +115,7 @@ internal static partial class Av1YuvConverter
get
{
// Three float component rows are converted in place. Color input adds two reusable chroma scratch
- // rows, and the final one or two float-sized slots per pixel back an Rgba32 or Rgba64 staging row.
+ // rows, and the final one or two float-sized slots per pixel back RGB byte planes or an Rgba64 row.
int rowCount = this.isMonochrome ? 3 : 5;
int packedRowCount = typeof(TSample) == typeof(byte) ? 1 : 2;
return this.image.Width * (rowCount + packedRowCount);
@@ -126,7 +127,8 @@ internal static partial class Av1YuvConverter
///
/// The row index.
/// The reusable conversion buffer.
- public void Convert(int y, Span span)
+ /// The padded destination used when an eight-bit image row cannot expose sufficient padding.
+ public void Convert(int y, Span span, Span proxy)
{
int width = this.image.Width;
Span red = span[..width];
@@ -189,9 +191,25 @@ internal static partial class Av1YuvConverter
Span destination = this.image.PixelBuffer.DangerousGetRowSpan(y);
if (typeof(TSample) == typeof(byte))
{
- Span packed = MemoryMarshal.Cast(packedStorage)[..width];
- PackRgba32(red, green, blue, packed);
- PixelOperations.Instance.FromRgba32(this.configuration, packed, destination);
+ // Match JPEG's byte-plane packing contract so optimized pixel types use the existing RGB packer.
+ // The final float row provides enough byte storage for three tightly packed component planes.
+ Span byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)];
+ Span redBytes = byteStorage[..width];
+ Span greenBytes = byteStorage.Slice(width, width);
+ Span blueBytes = byteStorage.Slice(width * 2, width);
+ SimdUtils.NormalizedFloatToByteSaturate(red, redBytes);
+ 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);
+ }
}
else
{
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs
index df28df6ed..64115a22b 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs
@@ -601,71 +601,6 @@ internal static partial class Av1YuvConverter
}
}
- ///
- /// Packs normalized RGB component rows into opaque eight-bit RGBA pixels.
- ///
- /// The normalized red components.
- /// The normalized green components.
- /// The normalized blue components.
- /// The destination pixels.
- private static void PackRgba32(ReadOnlySpan red, ReadOnlySpan green, ReadOnlySpan blue, Span destination)
- {
- ref float redBase = ref MemoryMarshal.GetReference(red);
- ref float greenBase = ref MemoryMarshal.GetReference(green);
- ref float blueBase = ref MemoryMarshal.GetReference(blue);
- ref Rgba32 destinationBase = ref MemoryMarshal.GetReference(destination);
- int length = destination.Length;
- int i = 0;
-
- if (Vector512.IsHardwareAccelerated)
- {
- int oneVectorFromEnd = length - Vector512.Count;
- for (; i <= oneVectorFromEnd; i += Vector512.Count)
- {
- Vector512 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
- Vector512 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
- Vector512 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum);
- StoreRgba32Batch(r.GetLower().GetLower(), g.GetLower().GetLower(), b.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i));
- StoreRgba32Batch(r.GetLower().GetUpper(), g.GetLower().GetUpper(), b.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4));
- StoreRgba32Batch(r.GetUpper().GetLower(), g.GetUpper().GetLower(), b.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, i + 8));
- StoreRgba32Batch(r.GetUpper().GetUpper(), g.GetUpper().GetUpper(), b.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 12));
- }
- }
-
- if (Vector256.IsHardwareAccelerated)
- {
- int oneVectorFromEnd = length - Vector256.Count;
- for (; i <= oneVectorFromEnd; i += Vector256.Count)
- {
- Vector256 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
- Vector256 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
- Vector256 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum);
- StoreRgba32Batch(r.GetLower(), g.GetLower(), b.GetLower(), ref Unsafe.Add(ref destinationBase, i));
- StoreRgba32Batch(r.GetUpper(), g.GetUpper(), b.GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4));
- }
- }
-
- if (Vector128.IsHardwareAccelerated)
- {
- int oneVectorFromEnd = length - Vector128.Count;
- for (; i <= oneVectorFromEnd; i += Vector128.Count)
- {
- Vector128 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
- Vector128 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
- Vector128 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum);
- StoreRgba32Batch(r, g, b, ref Unsafe.Add(ref destinationBase, i));
- }
- }
-
- for (; i < length; i++)
- {
- Unsafe.Add(ref destinationBase, i) = new Rgba32(
- ToSample(Unsafe.Add(ref redBase, i) * ByteMaximum, ByteMaximum),
- ToSample(Unsafe.Add(ref greenBase, i) * ByteMaximum, ByteMaximum),
- ToSample(Unsafe.Add(ref blueBase, i) * ByteMaximum, ByteMaximum));
- }
- }
-
///
/// Packs normalized RGB component rows into opaque 16-bit RGBA pixels.
///
@@ -816,28 +751,6 @@ internal static partial class Av1YuvConverter
return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero));
}
- ///
- /// Interleaves four red, green, and blue integer lanes into four opaque eight-bit RGBA pixels.
- ///
- /// The red component values.
- /// The green component values.
- /// The blue component values.
- /// The first destination pixel.
- private static void StoreRgba32Batch(Vector128 red, Vector128 green, Vector128 blue, ref Rgba32 destination)
- {
- Vector128 red8 = Vector128.Narrow(Vector128.Narrow(red.AsUInt32(), Vector128.Zero), Vector128.Zero);
- Vector128 green8 = Vector128.Narrow(Vector128.Narrow(green.AsUInt32(), Vector128.Zero), Vector128.Zero);
- Vector128 blue8 = Vector128.Narrow(Vector128.Narrow(blue.AsUInt32(), Vector128.Zero), Vector128.Zero);
- Vector128 alpha8 = Vector128.Create(byte.MaxValue);
- Vector128 redGreen = Vector128_.UnpackLow(red8, green8);
- Vector128 blueAlpha = Vector128_.UnpackLow(blue8, alpha8);
-
- // Interleaving the byte pairs as 16-bit lanes produces RGBA memory order on every supported
- // architecture without relying on the host integer endianness of Rgba32.PackedValue.
- Vector128 rgba = Vector128_.UnpackLow(redGreen.AsInt16(), blueAlpha.AsInt16()).AsByte();
- Unsafe.As>(ref destination) = rgba;
- }
-
///
/// Interleaves four red, green, and blue integer lanes into four opaque 16-bit RGBA pixels.
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
index e5da2540a..7ef0638a6 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
@@ -36,43 +36,19 @@ internal static partial class Av1YuvConverter
public static void ConvertToRgb(Configuration configuration, Av1FrameBuffer frameBuffer, ImageFrame image)
where TPixel : unmanaged, IPixel
{
- GetConversionParameters(
- frameBuffer,
- out Av1ColorConversionMode mode,
- out float kr,
- out float kg,
- out float kb,
- out float lumaBias,
- out float lumaScale,
- out float chromaBias,
- out float chromaScale,
- out float sampleMaximum);
-
- ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
- Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
- ? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
- : default;
-
- Av1ColorConversionParameters parameters = new(
- kr,
- kg,
- kb,
- transferCharacteristics,
- in constantLuminanceScales,
- lumaBias,
- lumaScale,
- chromaBias,
- chromaScale);
+ Av1ColorConversionParameters parameters = GetConversionParameters(frameBuffer, out Av1ColorConversionMode mode);
Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, frameBuffer.ColorFormat == Av1ColorFormat.Yuv400);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
YuvToRgbRowConverter converter = new(configuration, frameBuffer, image, colorConverter);
- using IMemoryOwner owner = configuration.MemoryAllocator.Allocate(converter.BufferLength);
- Span scratch = owner.GetSpan();
+ using IMemoryOwner scratchOwner = configuration.MemoryAllocator.Allocate(converter.BufferLength);
+ using IMemoryOwner proxyOwner = configuration.MemoryAllocator.Allocate(image.Width + 3);
+ Span scratch = scratchOwner.GetSpan();
+ Span proxy = proxyOwner.GetSpan()[..(image.Width + 3)];
for (int y = 0; y < image.Height; y++)
{
- converter.Convert(y, scratch);
+ converter.Convert(y, scratch, proxy);
}
}
else
@@ -82,7 +58,7 @@ internal static partial class Av1YuvConverter
Span scratch = owner.GetSpan();
for (int y = 0; y < image.Height; y++)
{
- converter.Convert(y, scratch);
+ converter.Convert(y, scratch, Span.Empty);
}
}
}
@@ -97,33 +73,8 @@ internal static partial class Av1YuvConverter
public static void ConvertFromRgb(Configuration configuration, ImageFrame image, Av1FrameBuffer frameBuffer)
where TPixel : unmanaged, IPixel
{
- GetConversionParameters(
- frameBuffer,
- out Av1ColorConversionMode mode,
- out float kr,
- out float kg,
- out float kb,
- out float lumaBias,
- out float lumaScale,
- out float chromaBias,
- out float chromaScale,
- out float sampleMaximum);
-
- ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
- Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
- ? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
- : default;
-
- Av1ColorConversionParameters parameters = new(
- kr,
- kg,
- kb,
- transferCharacteristics,
- in constantLuminanceScales,
- lumaBias,
- lumaScale,
- chromaBias,
- chromaScale);
+ Av1ColorConversionParameters parameters = GetConversionParameters(frameBuffer, out Av1ColorConversionMode mode);
+ float sampleMaximum = parameters.EncodedSampleMaximum;
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, isMonochrome);
@@ -158,31 +109,14 @@ internal static partial class Av1YuvConverter
///
/// The AV1 frame containing the signaled color configuration.
/// The resolved conversion mode.
- /// The red luma coefficient.
- /// The green luma coefficient.
- /// The blue luma coefficient.
- /// The encoded luma bias.
- /// The encoded luma range.
- /// The encoded chroma midpoint.
- /// The encoded chroma range.
- /// The largest encoded sample value.
- private static void GetConversionParameters(
- Av1FrameBuffer frameBuffer,
- out Av1ColorConversionMode mode,
- out float kr,
- out float kg,
- out float kb,
- out float lumaBias,
- out float lumaScale,
- out float chromaBias,
- out float chromaScale,
- out float sampleMaximum)
+ /// The resolved conversion parameters.
+ private static Av1ColorConversionParameters GetConversionParameters(Av1FrameBuffer frameBuffer, out Av1ColorConversionMode mode)
{
mode = Av1ColorConversionMode.Coefficients;
- kr = 0F;
- kb = 0F;
+ float kr = 0F;
+ float kb = 0F;
- // These values are the matrix table used by libavif and are defined by H.273.
+ // These code points and fixed coefficient matrices are defined by H.273.
switch (frameBuffer.ColorConfig.MatrixCoefficients)
{
case ObuMatrixCoefficients.Identity:
@@ -232,16 +166,24 @@ internal static partial class Av1YuvConverter
case ObuMatrixCoefficients.Bt2100ICtCp:
mode = Av1ColorConversionMode.ICtCp;
break;
+ case ObuMatrixCoefficients.IptC2:
+ mode = Av1ColorConversionMode.IptC2;
+ break;
+ case ObuMatrixCoefficients.YCgCoRe:
+ case ObuMatrixCoefficients.YCgCoRo:
+ mode = Av1ColorConversionMode.YCgCoReversible;
+ break;
default:
throw new NotSupportedException($"AV1 matrix coefficients '{frameBuffer.ColorConfig.MatrixCoefficients}' are not currently supported.");
}
- kg = 1F - kr - kb;
+ float kg = 1F - kr - kb;
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
bool isFullRange = frameBuffer.ColorConfig.ColorRange;
- if (mode == Av1ColorConversionMode.Identity && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
+ bool requiresFullChroma = mode is Av1ColorConversionMode.Identity or Av1ColorConversionMode.YCgCoReversible;
+ if (requiresFullChroma && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
{
- throw new InvalidImageContentException("AV1 identity matrix coefficients require YUV 4:4:4 sampling.");
+ throw new InvalidImageContentException($"AV1 {frameBuffer.ColorConfig.MatrixCoefficients} matrix coefficients require YUV 4:4:4 sampling.");
}
if (frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved)
@@ -251,16 +193,55 @@ internal static partial class Av1YuvConverter
int bitCount = frameBuffer.BitDepth.GetBitCount();
int depthScale = 1 << (bitCount - 8);
- sampleMaximum = (1 << bitCount) - 1;
- chromaBias = 128F * depthScale;
- lumaBias = isFullRange ? 0F : 16F * depthScale;
- lumaScale = isFullRange ? sampleMaximum : 219F * depthScale;
+ float sampleMaximum = (1 << bitCount) - 1;
+ float chromaBias = 128F * depthScale;
+ float lumaBias = isFullRange ? 0F : 16F * depthScale;
+ float lumaScale = isFullRange ? sampleMaximum : 219F * depthScale;
// H.273 limited-range YCgCo first maps R, G, and B through the 219-code luma range, so its
// difference components inherit that scale instead of the 224-code scale used by YCbCr.
- chromaScale = isFullRange || mode == Av1ColorConversionMode.YCgCo
+ float chromaScale = isFullRange || mode == Av1ColorConversionMode.YCgCo
? lumaScale
: 224F * depthScale;
+
+ float rgbBias = 0F;
+ float rgbScale = 1F;
+ float rgbSampleMaximum = 1F;
+ if (mode == Av1ColorConversionMode.YCgCoReversible)
+ {
+ int bitOffset = frameBuffer.ColorConfig.MatrixCoefficients == ObuMatrixCoefficients.YCgCoRe ? 2 : 1;
+ int rgbBitCount = bitCount - bitOffset;
+ float rgbDepthScale = MathF.ScaleB(1F, rgbBitCount - 8);
+ rgbSampleMaximum = (1 << rgbBitCount) - 1;
+ rgbBias = isFullRange ? 0F : 16F * rgbDepthScale;
+ rgbScale = isFullRange ? rgbSampleMaximum : 219F * rgbDepthScale;
+
+ // The reversible lifting transform operates on raw integer code values. Range adjustment therefore
+ // belongs inside the operator, while the row converter only normalizes the encoded Y, Cg, and Co planes.
+ lumaBias = 0F;
+ lumaScale = sampleMaximum;
+ chromaScale = sampleMaximum;
+ }
+
+ ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
+ Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
+ ? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
+ : default;
+
+ return new Av1ColorConversionParameters(
+ kr,
+ kg,
+ kb,
+ transferCharacteristics,
+ in constantLuminanceScales,
+ lumaBias,
+ lumaScale,
+ chromaBias,
+ chromaScale,
+ sampleMaximum,
+ rgbBias,
+ rgbScale,
+ rgbSampleMaximum);
}
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs
index c35ca4ad1..5024a9e9c 100644
--- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs
@@ -77,4 +77,19 @@ internal enum ObuMatrixCoefficients
/// ITU-R BT.2100 ICtCp coefficients.
///
Bt2100ICtCp = 14,
+
+ ///
+ /// IPT-C2 coefficients.
+ ///
+ IptC2 = 15,
+
+ ///
+ /// Reversible YCgCo coefficients with two additional encoded bits.
+ ///
+ YCgCoRe = 16,
+
+ ///
+ /// Reversible YCgCo coefficients with one additional encoded bit.
+ ///
+ YCgCoRo = 17,
}
diff --git a/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs b/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs
index 931beac84..fdf044c6e 100644
--- a/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs
+++ b/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp;
#pragma warning disable CA1707 // Underscores in enum members
///
-/// Matrix coefficients according to ITU-T H.273 / ISO/IEC 23091-2_2019 subclause 8.3
+/// Matrix coefficients according to ITU-T H.273.
///
public enum CicpMatrixCoefficients : byte
{
@@ -91,6 +91,21 @@ public enum CicpMatrixCoefficients : byte
/// Rec. ITU-R BT.2100-2 ICtCp
///
ICtCp = 14,
+
+ ///
+ /// IPT-C2.
+ ///
+ IptC2 = 15,
+
+ ///
+ /// Reversible YCgCo with two additional encoded bits.
+ ///
+ YCgCoRe = 16,
+
+ ///
+ /// Reversible YCgCo with one additional encoded bit.
+ ///
+ YCgCoRo = 17,
}
#pragma warning restore CA1707 // Underscores in enum members
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs
index d749f39be..557bbf5c6 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs
@@ -522,9 +522,15 @@ public class Av1YuvConverterTests
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Bt709)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Identity)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.SmpteYCgCo)]
+ [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.IptC2)]
+ [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe)]
+ [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Bt709)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Identity)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.SmpteYCgCo)]
+ [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.IptC2)]
+ [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe)]
+ [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo)]
public void HighBitDepthRoundTrip(int bitDepth, int matrixCoefficients)
{
// Assign
@@ -556,6 +562,203 @@ public class Av1YuvConverterTests
}
}
+ ///
+ /// Verifies the H.273 IPT-C2 matrices in both directions against independently calculated code values.
+ ///
+ [Fact]
+ public void IptC2MatchesKnownLinearTransferValuesInBothDirections()
+ {
+ // Assign
+ // The linear transfer characteristic isolates the two normative IPT-C2 matrices from transfer-curve error.
+ using Image source = new(1, 1);
+ source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb24(150, 100, 50);
+ ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
+ 1,
+ 1,
+ matrixCoefficients: ObuMatrixCoefficients.IptC2,
+ transferCharacteristics: ObuTransferCharacteristics.Linear);
+
+ using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
+
+ // Act
+ Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
+
+ // Assert
+ Assert.Equal(100, frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]);
+ Assert.Equal(178, frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]);
+ Assert.Equal(198, frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]);
+
+ using Image destination = new(1, 1);
+ Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame);
+
+ Assert.Equal(new Rgb24(150, 100, 51), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
+ }
+
+ ///
+ /// Verifies the reversible YCgCo lifting stages against known pure-red code values in both directions.
+ ///
+ /// The encoded AV1 bit depth.
+ /// The reversible YCgCo variant.
+ /// The expected encoded luma value.
+ /// The expected encoded Cg value.
+ /// The expected encoded Co value.
+ [Theory]
+ [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 15, 97, 191)]
+ [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 63, 385, 767)]
+ [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 255, 1537, 3071)]
+ [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 31, 65, 255)]
+ [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 127, 257, 1023)]
+ [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 511, 1025, 4095)]
+ public void ReversibleYCgCoMatchesKnownPureRedValuesInBothDirections(
+ int bitDepth,
+ int matrixCoefficients,
+ int expectedY,
+ int expectedU,
+ int expectedV)
+ {
+ // Assign
+ // Pure red exercises positive odd Co and negative odd Cg. The expected samples come directly from
+ // the H.273 integer lifting equations at the logical RGB precision selected by each matrix code point.
+ using Image source = new(1, 1);
+ source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(ushort.MaxValue, 0, 0);
+ ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
+ 1,
+ 1,
+ matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients,
+ bitDepth: (Av1BitDepth)bitDepth);
+
+ using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
+
+ // Act
+ Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
+
+ // Assert
+ int actualY;
+ int actualU;
+ int actualV;
+ if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit)
+ {
+ actualY = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0];
+ actualU = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0];
+ actualV = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0];
+ }
+ else
+ {
+ actualY = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0)[0];
+ actualU = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0)[0];
+ actualV = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0)[0];
+ }
+
+ Assert.Equal(expectedY, actualY);
+ Assert.Equal(expectedU, actualU);
+ Assert.Equal(expectedV, actualV);
+
+ using Image destination = new(1, 1);
+ Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame);
+
+ Assert.Equal(new Rgb48(ushort.MaxValue, 0, 0), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
+ }
+
+ ///
+ /// Verifies that limited-range reversible YCgCo applies range adjustment to RGB code values before lifting.
+ ///
+ /// The encoded AV1 bit depth.
+ /// The reversible YCgCo variant.
+ /// The expected black luma code value.
+ /// The expected white luma code value.
+ [Theory]
+ [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 4, 59)]
+ [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 16, 235)]
+ [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 64, 940)]
+ [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 8, 118)]
+ [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 32, 470)]
+ [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 128, 1880)]
+ public void ReversibleYCgCoAppliesLimitedRangeBeforeLifting(
+ int bitDepth,
+ int matrixCoefficients,
+ int expectedBlack,
+ int expectedWhite)
+ {
+ // Assign
+ // Black and white have zero Cg and Co, exposing the RGB-domain range mapping without opponent-axis noise.
+ using Image source = new(2, 1);
+ Span sourcePixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
+ sourcePixels[0] = new Rgb48(0, 0, 0);
+ sourcePixels[1] = new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
+ ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
+ 2,
+ 1,
+ fullRange: false,
+ matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients,
+ bitDepth: (Av1BitDepth)bitDepth);
+
+ using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
+
+ // Act
+ Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
+
+ // Assert
+ int expectedChromaBias = 1 << (((Av1BitDepth)bitDepth).GetBitCount() - 1);
+ if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit)
+ {
+ Span y = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0);
+ Span u = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0);
+ Span v = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0);
+ Assert.Equal(expectedBlack, y[0]);
+ Assert.Equal(expectedWhite, y[1]);
+ Assert.Equal(expectedChromaBias, u[0]);
+ Assert.Equal(expectedChromaBias, u[1]);
+ Assert.Equal(expectedChromaBias, v[0]);
+ Assert.Equal(expectedChromaBias, v[1]);
+ }
+ else
+ {
+ Span y = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0);
+ Span u = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0);
+ Span v = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0);
+ Assert.Equal(expectedBlack, y[0]);
+ Assert.Equal(expectedWhite, y[1]);
+ Assert.Equal(expectedChromaBias, u[0]);
+ Assert.Equal(expectedChromaBias, u[1]);
+ Assert.Equal(expectedChromaBias, v[0]);
+ Assert.Equal(expectedChromaBias, v[1]);
+ }
+
+ using Image destination = new(2, 1);
+ Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame);
+
+ Span destinationPixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
+ Assert.Equal(new Rgb48(0, 0, 0), destinationPixels[0]);
+ Assert.Equal(new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue), destinationPixels[1]);
+ }
+
+ ///
+ /// Verifies that reversible YCgCo rejects chroma subsampling in both conversion directions.
+ ///
+ /// The reversible YCgCo variant.
+ [Theory]
+ [InlineData(ObuMatrixCoefficients.YCgCoRe)]
+ [InlineData(ObuMatrixCoefficients.YCgCoRo)]
+ public void ReversibleYCgCoRequiresFullChroma(int matrixCoefficients)
+ {
+ // Assign
+ using Image image = new(2, 2);
+ ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
+ 2,
+ 2,
+ matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients,
+ colorFormat: Av1ColorFormat.Yuv420);
+
+ using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false);
+
+ // Act and assert
+ Assert.Throws(
+ () => Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer));
+
+ Assert.Throws(
+ () => Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame));
+ }
+
///
/// Verifies that every H.273 operator produces the same result in SIMD batches and the scalar row tail.
///
@@ -571,6 +774,9 @@ public class Av1YuvConverterTests
[InlineData(ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Smpte2084)]
[InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Hlg)]
+ [InlineData(ObuMatrixCoefficients.IptC2, ObuTransferCharacteristics.Bt709)]
+ [InlineData(ObuMatrixCoefficients.YCgCoRe, ObuTransferCharacteristics.Bt709)]
+ [InlineData(ObuMatrixCoefficients.YCgCoRo, ObuTransferCharacteristics.Bt709)]
public void ColorOperatorSimdBatchesMatchScalarTail(int matrixCoefficients, int transferCharacteristics)
{
const int width = 31;