Repository: image-rs/image
Pull request #2463: AVIF: YCgCo
Author: login: awxkee
Created: 2025-05-10T20:09:19Z | Merged: 2025-05-15T22:09:18Z

Here is YCgCo decoder implementation.
ITU-R CICP spec [link](https://www.itu.int/rec/T-REC-H.273/en).

This is YCgCo only implementation. YCgCo-R de-facto is not used in AVIFs.
libavif introduced YCgCo-Re and YCgCo-Ro types (matrix coefficients 16 and 17), but dav1d-rs does not support them at the moment. They are more complex due to bit-depth shifts, so their implementation is postponed for the future.

Note: eq. 27-29, colors range in YCgCo handled differently than in YCbCr.

[Here](https://github.com/awxkee/image/tree/ycgco_samples/samples) is valid YCgCo images to try out. I don't know software that could open them correctly, except Chrome and avifdec ( part of libavif ).

I spent few hours trying to figure out why colors didn't match Chrome after simple decoding those images, because I already did it and everything matched perfectly. I tried a basic re-encoding and noticed visible color shifts. After several hours I realized that issue is not in YCgCo paths, but in any AVIF file. There's almost definitely something wrong in dav1d-rs, or in image encoding path, or both.


Diff:
diff --git a/src/codecs/avif/decoder.rs b/src/codecs/avif/decoder.rs
index 04e43f666a..8610d1e17b 100644
--- a/src/codecs/avif/decoder.rs
+++ b/src/codecs/avif/decoder.rs
@@ -35,6 +35,7 @@ pub struct AvifDecoder<R> {
 enum AvifDecoderError {
     AlphaPlaneFormat(PixelLayout),
     YuvLayoutOnIdentityMatrix(PixelLayout),
+    UnsupportedLayoutAndMatrix(PixelLayout, YuvMatrixStrategy),
 }
 
 impl Display for AvifDecoderError {
@@ -42,22 +43,25 @@ impl Display for AvifDecoderError {
         match self {
             AvifDecoderError::AlphaPlaneFormat(pixel_layout) => match pixel_layout {
                 PixelLayout::I400 => unreachable!("This option must be handled correctly"),
-                PixelLayout::I420 => f.write_str("Alpha layout must be 4:0:0 but it was 4:2:0"),
-                PixelLayout::I422 => f.write_str("Alpha layout must be 4:0:0 but it was 4:2:2"),
-                PixelLayout::I444 => f.write_str("Alpha layout must be 4:0:0 but it was 4:4:4"),
+                PixelLayout::I420 => f.write_str("Alpha layout must be 4:0:0, but it was 4:2:0"),
+                PixelLayout::I422 => f.write_str("Alpha layout must be 4:0:0, but it was 4:2:2"),
+                PixelLayout::I444 => f.write_str("Alpha layout must be 4:0:0, but it was 4:4:4"),
             },
             AvifDecoderError::YuvLayoutOnIdentityMatrix(pixel_layout) => match pixel_layout {
                 PixelLayout::I400 => {
-                    f.write_str("YUV layout on 'Identity' matrix must be 4:4:4 but it was 4:0:0")
+                    f.write_str("YUV layout on 'Identity' matrix must be 4:4:4, but it was 4:0:0")
                 }
                 PixelLayout::I420 => {
-                    f.write_str("YUV layout on 'Identity' matrix must be 4:4:4 but it was 4:2:0")
+                    f.write_str("YUV layout on 'Identity' matrix must be 4:4:4, but it was 4:2:0")
                 }
                 PixelLayout::I422 => {
-                    f.write_str("YUV layout on 'Identity' matrix must be 4:4:4 but it was 4:2:2")
+                    f.write_str("YUV layout on 'Identity' matrix must be 4:4:4, but it was 4:2:2")
                 }
                 PixelLayout::I444 => unreachable!("This option must be handled correctly"),
             },
+            AvifDecoderError::UnsupportedLayoutAndMatrix(layout, matrix) => f.write_fmt(
+                format_args!("YUV layout {layout:?} on matrix {matrix:?} is not supported",),
+            ),
         }
     }
 }
@@ -229,18 +233,29 @@ fn transmute_chroma_plane16(
     }
 }
 
+#[derive(Copy, Clone, Debug, PartialOrd, Eq, PartialEq)]
+enum YuvMatrixStrategy {
+    KrKb(YuvStandardMatrix),
+    CgCo,
+    Identity,
+}
+
 /// Getting one of prebuilt matrix of fails
 fn get_matrix(
     david_matrix: dav1d::pixel::MatrixCoefficients,
-) -> Result<YuvStandardMatrix, ImageError> {
+) -> Result<YuvMatrixStrategy, ImageError> {
     match david_matrix {
-        dav1d::pixel::MatrixCoefficients::Identity => Ok(YuvStandardMatrix::Identity),
-        dav1d::pixel::MatrixCoefficients::BT709 => Ok(YuvStandardMatrix::Bt709),
+        dav1d::pixel::MatrixCoefficients::Identity => Ok(YuvMatrixStrategy::Identity),
+        dav1d::pixel::MatrixCoefficients::BT709 => {
+            Ok(YuvMatrixStrategy::KrKb(YuvStandardMatrix::Bt709))
+        }
         // This is arguable, some applications prefer to go with Bt.709 as default,
         // and some applications prefer Bt.601 as default.
         // For ex. `Chrome` always prefer Bt.709 even for SD content
         // However, nowadays standard should be Bt.709 for HD+ size otherwise Bt.601
-        dav1d::pixel::MatrixCoefficients::Unspecified => Ok(YuvStandardMatrix::Bt709),
+        dav1d::pixel::MatrixCoefficients::Unspecified => {
+            Ok(YuvMatrixStrategy::KrKb(YuvStandardMatrix::Bt709))
+        }
         dav1d::pixel::MatrixCoefficients::Reserved => Err(ImageError::Unsupported(
             UnsupportedError::from_format_and_kind(
                 ImageFormat::Avif.into(),
@@ -249,19 +264,21 @@ fn get_matrix(
                 ),
             ),
         )),
-        dav1d::pixel::MatrixCoefficients::BT470M => Ok(YuvStandardMatrix::Bt470_6),
-        dav1d::pixel::MatrixCoefficients::BT470BG => Ok(YuvStandardMatrix::Bt601),
-        dav1d::pixel::MatrixCoefficients::ST170M => Ok(YuvStandardMatrix::Smpte240),
-        dav1d::pixel::MatrixCoefficients::ST240M => Ok(YuvStandardMatrix::Smpte240),
-        // This is an experimental matrix in libavif yet.
-        dav1d::pixel::MatrixCoefficients::YCgCo => Err(ImageError::Unsupported(
-            UnsupportedError::from_format_and_kind(
-                ImageFormat::Avif.into(),
-                UnsupportedErrorKind::GenericFeature("YCgCo matrix is not supported".to_string()),
-            ),
-        )),
+        dav1d::pixel::MatrixCoefficients::BT470M => {
+            Ok(YuvMatrixStrategy::KrKb(YuvStandardMatrix::Bt470_6))
+        }
+        dav1d::pixel::MatrixCoefficients::BT470BG => {
+            Ok(YuvMatrixStrategy::KrKb(YuvStandardMatrix::Bt601))
+        }
+        dav1d::pixel::MatrixCoefficients::ST170M => {
+            Ok(YuvMatrixStrategy::KrKb(YuvStandardMatrix::Smpte240))
+        }
+        dav1d::pixel::MatrixCoefficients::ST240M => {
+            Ok(YuvMatrixStrategy::KrKb(YuvStandardMatrix::Smpte240))
+        }
+        dav1d::pixel::MatrixCoefficients::YCgCo => Ok(YuvMatrixStrategy::CgCo),
         dav1d::pixel::MatrixCoefficients::BT2020NonConstantLuminance => {
-            Ok(YuvStandardMatrix::Bt2020)
+            Ok(YuvMatrixStrategy::KrKb(YuvStandardMatrix::Bt2020))
         }
         dav1d::pixel::MatrixCoefficients::BT2020ConstantLuminance => {
             // This matrix significantly differs from others because linearize values is required
@@ -343,10 +360,10 @@ impl<R: Read> ImageDecoder for AvifDecoder<R> {
             dav1d::pixel::YUVRange::Full => YuvIntensityRange::Pc,
         };
 
-        let color_matrix = get_matrix(self.picture.matrix_coefficients())?;
+        let matrix_strategy = get_matrix(self.picture.matrix_coefficients())?;
 
         // Identity matrix should be possible only on 4:4:4
-        if color_matrix == YuvStandardMatrix::Identity
+        if matrix_strategy == YuvMatrixStrategy::Identity
             && self.picture.pixel_layout() != PixelLayout::I444
         {
             return Err(ImageError::Decoding(DecodingError::new(
@@ -355,6 +372,18 @@ impl<R: Read> ImageDecoder for AvifDecoder<R> {
             )));
         }
 
+        if matrix_strategy == YuvMatrixStrategy::CgCo
+            && self.picture.pixel_layout() == PixelLayout::I400
+        {
+            return Err(ImageError::Decoding(DecodingError::new(
+                ImageFormat::Avif.into(),
+                AvifDecoderError::UnsupportedLayoutAndMatrix(
+                    self.picture.pixel_layout(),
+                    matrix_strategy,
+                ),
+            )));
+        }
+
         if bit_depth == 8 {
             let ref_y = self.picture.plane(PlanarImageComponent::Y);
             let ref_u = self.picture.plane(PlanarImageComponent::U);
@@ -371,14 +400,38 @@ impl<R: Read> ImageDecoder for AvifDecoder<R> {
                 height: height as usize,
             };
 
-            let worker = match self.picture.pixel_layout() {
-                PixelLayout::I400 => yuv400_to_rgba8,
-                PixelLayout::I420 => yuv420_to_rgba8,
-                PixelLayout::I422 => yuv422_to_rgba8,
-                PixelLayout::I444 => yuv444_to_rgba8,
-            };
+            match matrix_strategy {
+                YuvMatrixStrategy::KrKb(standard) => {
+                    let worker = match self.picture.pixel_layout() {
+                        PixelLayout::I400 => yuv400_to_rgba8,
+                        PixelLayout::I420 => yuv420_to_rgba8,
+                        PixelLayout::I422 => yuv422_to_rgba8,
+                        PixelLayout::I444 => yuv444_to_rgba8,
+                    };
 
-            worker(image, buf, yuv_range, color_matrix)?;
+                    worker(image, buf, yuv_range, standard)?;
+                }
+                YuvMatrixStrategy::CgCo => {
+                    let worker = match self.picture.pixel_layout() {
+                        PixelLayout::I400 => unreachable!(),
+                        PixelLayout::I420 => ycgco420_to_rgba8,
+                        PixelLayout::I422 => ycgco422_to_rgba8,
+                        PixelLayout::I444 => ycgco444_to_rgba8,
+                    };
+
+                    worker(image, buf, yuv_range)?;
+                }
+                YuvMatrixStrategy::Identity => {
+                    let worker = match self.picture.pixel_layout() {
+                        PixelLayout::I400 => unreachable!(),
+                        PixelLayout::I420 => unreachable!(),
+                        PixelLayout::I422 => unreachable!(),
+                        PixelLayout::I444 => gbr_to_rgba8,
+                    };
+
+                    worker(image, buf, yuv_range)?;
+                }
+            }
 
             // Squashing alpha plane into a picture
             if let Some(picture) = self.alpha_picture {
@@ -405,11 +458,11 @@ impl<R: Read> ImageDecoder for AvifDecoder<R> {
             // // 8+ bit-depth case
             if let Ok(buf) = bytemuck::try_cast_slice_mut(buf) {
                 let target_slice: &mut [u16] = buf;
-                self.process_16bit_picture(target_slice, yuv_range, color_matrix)?;
+                self.process_16bit_picture(target_slice, yuv_range, matrix_strategy)?;
             } else {
                 // If buffer from Decoder is unaligned
                 let mut aligned_store = vec![0u16; buf.len() / 2];
-                self.process_16bit_picture(&mut aligned_store, yuv_range, color_matrix)?;
+                self.process_16bit_picture(&mut aligned_store, yuv_range, matrix_strategy)?;
                 for (dst, src) in buf.chunks_exact_mut(2).zip(aligned_store.iter()) {
                     let bytes = src.to_ne_bytes();
                     dst[0] = bytes[0];
@@ -431,7 +484,7 @@ impl<R: Read> AvifDecoder<R> {
         &self,
         target: &mut [u16],
         yuv_range: YuvIntensityRange,
-        color_matrix: YuvStandardMatrix,
+        matrix_strategy: YuvMatrixStrategy,
     ) -> ImageResult<()> {
         let y_dav1d_plane = self.picture.plane(PlanarImageComponent::Y);
 
@@ -483,37 +536,83 @@ impl<R: Read> AvifDecoder<R> {
             height: height as usize,
         };
 
-        let worker = match self.picture.pixel_layout() {
-            PixelLayout::I400 => {
-                if bit_depth == 10 {
-                    yuv400_to_rgba10
-                } else {
-                    yuv400_to_rgba12
-                }
-            }
-            PixelLayout::I420 => {
-                if bit_depth == 10 {
-                    yuv420_to_rgba10
-                } else {
-                    yuv420_to_rgba12
-                }
+        match matrix_strategy {
+            YuvMatrixStrategy::KrKb(standard) => {
+                let worker = match self.picture.pixel_layout() {
+                    PixelLayout::I400 => {
+                        if bit_depth == 10 {
+                            yuv400_to_rgba10
+                        } else {
+                            yuv400_to_rgba12
+                        }
+                    }
+                    PixelLayout::I420 => {
+                        if bit_depth == 10 {
+                            yuv420_to_rgba10
+                        } else {
+                            yuv420_to_rgba12
+                        }
+                    }
+                    PixelLayout::I422 => {
+                        if bit_depth == 10 {
+                            yuv422_to_rgba10
+                        } else {
+                            yuv422_to_rgba12
+                        }
+                    }
+                    PixelLayout::I444 => {
+                        if bit_depth == 10 {
+                            yuv444_to_rgba10
+                        } else {
+                            yuv444_to_rgba12
+                        }
+                    }
+                };
+                worker(image, target, yuv_range, standard)?;
             }
-            PixelLayout::I422 => {
-                if bit_depth == 10 {
-                    yuv422_to_rgba10
-                } else {
-                    yuv422_to_rgba12
-                }
+            YuvMatrixStrategy::CgCo => {
+                let worker = match self.picture.pixel_layout() {
+                    PixelLayout::I400 => unreachable!(),
+                    PixelLayout::I420 => {
+                        if bit_depth == 10 {
+                            ycgco420_to_rgba10
+                        } else {
+                            ycgco420_to_rgba12
+                        }
+                    }
+                    PixelLayout::I422 => {
+                        if bit_depth == 10 {
+                            ycgco422_to_rgba10
+                        } else {
+                            ycgco422_to_rgba12
+                        }
+                    }
+                    PixelLayout::I444 => {
+                        if bit_depth == 10 {
+                            ycgco444_to_rgba10
+                        } else {
+                            ycgco444_to_rgba12
+                        }
+                    }
+                };
+                worker(image, target, yuv_range)?;
             }
-            PixelLayout::I444 => {
-                if bit_depth == 10 {
-                    yuv444_to_rgba10
-                } else {
-                    yuv444_to_rgba12
-                }
+            YuvMatrixStrategy::Identity => {
+                let worker = match self.picture.pixel_layout() {
+                    PixelLayout::I400 => unreachable!(),
+                    PixelLayout::I420 => unreachable!(),
+                    PixelLayout::I422 => unreachable!(),
+                    PixelLayout::I444 => {
+                        if bit_depth == 10 {
+                            gbr_to_rgba10
+                        } else {
+                            gbr_to_rgba12
+                        }
+                    }
+                };
+                worker(image, target, yuv_range)?;
             }
-        };
-        worker(image, target, yuv_range, color_matrix)?;
+        }
 
         // Squashing alpha plane into a picture
         if let Some(picture) = &self.alpha_picture {
diff --git a/src/codecs/avif/yuv.rs b/src/codecs/avif/yuv.rs
index 62190c9038..61bc07f5fe 100644
--- a/src/codecs/avif/yuv.rs
+++ b/src/codecs/avif/yuv.rs
@@ -191,7 +191,7 @@ impl YuvIntensityRange {
     }
 }
 
-#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
+#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Eq)]
 /// Declares standard prebuilt YUV conversion matrices,
 /// check [ITU-R](https://www.itu.int/rec/T-REC-H.273/en) information for more info
 pub(crate) enum YuvStandardMatrix {
@@ -200,7 +200,6 @@ pub(crate) enum YuvStandardMatrix {
     Bt2020,
     Smpte240,
     Bt470_6,
-    Identity,
 }
 
 #[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
@@ -232,7 +231,6 @@ impl YuvStandardMatrix {
                 kr: 0.2220f32,
                 kb: 0.0713f32,
             },
-            YuvStandardMatrix::Identity => unreachable!(),
         }
     }
 }
@@ -355,11 +353,6 @@ where
         },
         "Unsupported bit depth and data type combination"
     );
-    assert_ne!(
-        matrix,
-        YuvStandardMatrix::Identity,
-        "Identity matrix cannot be used on 4:0:0"
-    );
 
     let y_plane = image.y_plane;
     let y_stride = image.y_stride;
@@ -449,7 +442,38 @@ pub(crate) fn yuv420_to_rgba8(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    yuv420_to_rgbx::<u8, 4, 8>(image, rgb, range, matrix)
+    const P: i32 = 13;
+    yuv420_to_rgbx::<u8, HalvedRowHandler<u8>, 4, 8, P>(
+        image,
+        rgb,
+        range,
+        matrix,
+        process_halved_chroma_row_cbcr::<u8, P, 4, 8>,
+    )
+}
+
+/// Converts YCgCo 8 bit-depth to Rgba 8 bit
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgb`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(crate) fn ycgco420_to_rgba8(
+    image: YuvPlanarImage<u8>,
+    rgb: &mut [u8],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    const P: i32 = 13;
+    yuv420_to_rgbx::<u8, HalvedRowHandler<u8>, 4, 8, P>(
+        image,
+        rgb,
+        range,
+        YuvStandardMatrix::Bt709,
+        process_halved_chroma_row_cbcr::<u8, P, 4, 8>,
+    )
 }
 
 /// Converts YUV420 10 bit-depth to Rgba 10 bit-depth
@@ -469,9 +493,45 @@ pub(crate) fn yuv420_to_rgba10(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    yuv420_to_rgbx::<u16, 4, 10>(image, rgb, range, matrix)
+    const P: i32 = 13;
+    yuv420_to_rgbx::<u16, HalvedRowHandler<u16>, 4, 10, P>(
+        image,
+        rgb,
+        range,
+        matrix,
+        process_halved_chroma_row_cbcr::<u16, P, 4, 10>,
+    )
+}
+
+/// Converts YCgCo 10 bit-depth to Rgba 10 bit-depth
+///
+/// Stride here is not supported as it can be in passed from FFI.
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgb`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(crate) fn ycgco420_to_rgba10(
+    image: YuvPlanarImage<u16>,
+    rgb: &mut [u16],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    const P: i32 = 13;
+    yuv420_to_rgbx::<u16, HalvedRowHandler<u16>, 4, 10, P>(
+        image,
+        rgb,
+        range,
+        YuvStandardMatrix::Bt709,
+        process_halved_chroma_row_cgco::<u16, P, 4, 10>,
+    )
 }
 
+type HalvedRowHandler<V> =
+    fn(YuvPlanarImage<V>, &mut [V], &CbCrInverseTransform<i32>, &YuvChromaRange);
+
 /// Converts YUV420 12 bit-depth to Rgba 12 bit-depth
 ///
 /// Stride here is not supported as it can be in passed from FFI.
@@ -489,11 +549,44 @@ pub(crate) fn yuv420_to_rgba12(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    yuv420_to_rgbx::<u16, 4, 12>(image, rgb, range, matrix)
+    const P: i32 = 13;
+    yuv420_to_rgbx::<u16, HalvedRowHandler<u16>, 4, 12, P>(
+        image,
+        rgb,
+        range,
+        matrix,
+        process_halved_chroma_row_cbcr::<u16, P, 4, 12>,
+    )
+}
+
+/// Converts YCgCo 420 12 bit-depth to Rgba 12 bit-depth
+///
+/// Stride here is not supported as it can be in passed from FFI.
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgb`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(crate) fn ycgco420_to_rgba12(
+    image: YuvPlanarImage<u16>,
+    rgb: &mut [u16],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    const P: i32 = 13;
+    yuv420_to_rgbx::<u16, HalvedRowHandler<u16>, 4, 12, P>(
+        image,
+        rgb,
+        range,
+        YuvStandardMatrix::Bt709,
+        process_halved_chroma_row_cgco::<u16, P, 4, 12>,
+    )
 }
 
 #[inline]
-fn process_halved_chroma_row<
+fn process_halved_chroma_row_cbcr<
     V: Copy + AsPrimitive<i32> + 'static + Sized,
     const PRECISION: i32,
     const CHANNELS: usize,
@@ -512,7 +605,7 @@ fn process_halved_chroma_row<
     let g_coef_1 = transform.g_coeff_1;
     let g_coef_2 = transform.g_coeff_2;
 
-    let max_value = (1 << BIT_DEPTH) - 1;
+    let max_value = (1i32 << BIT_DEPTH) - 1;
 
     // If the stride is larger than the plane size,
     // it might contain junk data beyond the actual valid region.
@@ -610,6 +703,125 @@ fn process_halved_chroma_row<
     }
 }
 
+fn process_halved_chroma_row_cgco<
+    V: Copy + AsPrimitive<i32> + 'static + Sized,
+    const PRECISION: i32,
+    const CHANNELS: usize,
+    const BIT_DEPTH: usize,
+>(
+    image: YuvPlanarImage<V>,
+    rgba: &mut [V],
+    _: &CbCrInverseTransform<i32>,
+    range: &YuvChromaRange,
+) where
+    i32: AsPrimitive<V>,
+{
+    let max_value = (1i32 << BIT_DEPTH) - 1;
+
+    // If the stride is larger than the plane size,
+    // it might contain junk data beyond the actual valid region.
+    // To avoid processing artifacts when working with odd-sized images,
+    // the buffer is reshaped to its actual size,
+    // preventing accidental use of invalid values from the trailing region.
+
+    let y_plane = &image.y_plane[0..image.width];
+    let chroma_size = (image.width + 1) / 2;
+    let u_plane = &image.u_plane[0..chroma_size];
+    let v_plane = &image.v_plane[0..chroma_size];
+    let rgba = &mut rgba[0..image.width * CHANNELS];
+
+    let bias_y = range.bias_y as i32;
+    let bias_uv = range.bias_uv as i32;
+    let y_iter = y_plane.chunks_exact(2);
+    let rgb_chunks = rgba.chunks_exact_mut(CHANNELS * 2);
+
+    let r_coef = ((max_value as f32 / range.range_y as f32) * (1 << PRECISION) as f32) as i32;
+
+    for (((y_src, &u_src), &v_src), rgb_dst) in y_iter.zip(u_plane).zip(v_plane).zip(rgb_chunks) {
+        let y_value0: i32 = y_src[0].as_() - bias_y;
+        let cg_value: i32 = u_src.as_() - bias_uv;
+        let co_value: i32 = v_src.as_() - bias_uv;
+
+        let t0 = y_value0 - cg_value;
+
+        let r = qrshr::<PRECISION, BIT_DEPTH>((t0 + co_value) * r_coef);
+        let b = qrshr::<PRECISION, BIT_DEPTH>((t0 - co_value) * r_coef);
+        let g = qrshr::<PRECISION, BIT_DEPTH>((y_value0 + cg_value) * r_coef);
+
+        if CHANNELS == 4 {
+            rgb_dst[0] = r.as_();
+            rgb_dst[1] = g.as_();
+            rgb_dst[2] = b.as_();
+            rgb_dst[3] = max_value.as_();
+        } else if CHANNELS == 3 {
+            rgb_dst[0] = r.as_();
+            rgb_dst[1] = g.as_();
+            rgb_dst[2] = b.as_();
+        } else {
+            unreachable!();
+        }
+
+        let y_value1 = y_src[1].as_() - bias_y;
+
+        let t1 = y_value1 - cg_value;
+
+        let r = qrshr::<PRECISION, BIT_DEPTH>((t1 + co_value) * r_coef);
+        let b = qrshr::<PRECISION, BIT_DEPTH>((t1 - co_value) * r_coef);
+        let g = qrshr::<PRECISION, BIT_DEPTH>((y_value1 + cg_value) * r_coef);
+
+        if CHANNELS == 4 {
+            rgb_dst[4] = r.as_();
+            rgb_dst[5] = g.as_();
+            rgb_dst[6] = b.as_();
+            rgb_dst[7] = max_value.as_();
+        } else if CHANNELS == 3 {
+            rgb_dst[3] = r.as_();
+            rgb_dst[4] = g.as_();
+            rgb_dst[5] = b.as_();
+        } else {
+            unreachable!();
+        }
+    }
+
+    // Process remainder if width is odd.
+    if image.width & 1 != 0 {
+        let y_left = y_plane.chunks_exact(2).remainder();
+        let rgb_chunks = rgba
+            .chunks_exact_mut(CHANNELS * 2)
+            .into_remainder()
+            .chunks_exact_mut(CHANNELS);
+        let u_iter = u_plane.iter().rev();
+        let v_iter = v_plane.iter().rev();
+
+        for (((y_src, u_src), v_src), rgb_dst) in
+            y_left.iter().zip(u_iter).zip(v_iter).zip(rgb_chunks)
+        {
+            let y_value = y_src.as_() - bias_y;
+            let cg_value = u_src.as_() - bias_uv;
+            let co_value = v_src.as_() - bias_uv;
+
+            let t0 = y_value - cg_value;
+
+            let r = qrshr::<PRECISION, BIT_DEPTH>((t0 + co_value) * r_coef);
+            let b = qrshr::<PRECISION, BIT_DEPTH>((t0 - co_value) * r_coef);
+            let g = qrshr::<PRECISION, BIT_DEPTH>((y_value + cg_value) * r_coef);
+
+            if CHANNELS == 4 {
+                rgb_dst[0] = r.as_();
+                rgb_dst[1] = g.as_();
+                rgb_dst[2] = b.as_();
+                rgb_dst[3] = max_value.as_();
+            } else if CHANNELS == 3 {
+                rgb_dst[0] = r.as_();
+                rgb_dst[1] = g.as_();
+                rgb_dst[2] = b.as_();
+            } else {
+                unreachable!();
+            }
+        }
+    }
+}
+
 /// Converts YUV420 to Rgba
 ///
 /// Stride here is not supported as it can be in passed from FFI.
@@ -624,13 +836,16 @@ fn process_halved_chroma_row<
 #[inline]
 fn yuv420_to_rgbx<
     V: Copy + AsPrimitive<i32> + 'static + Sized,
+    W: Fn(YuvPlanarImage<V>, &mut [V], &CbCrInverseTransform<i32>, &YuvChromaRange),
     const CHANNELS: usize,
     const BIT_DEPTH: usize,
+    const PRECISION: i32,
 >(
     image: YuvPlanarImage<V>,
     rgb: &mut [V],
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
+    worker: W,
 ) -> Result<(), ImageError>
 where
     i32: AsPrimitive<V>,
@@ -651,11 +866,6 @@ where
         },
         "Unsupported bit depth and data type combination"
     );
-    assert_ne!(
-        matrix,
-        YuvStandardMatrix::Identity,
-        "Identity matrix cannot be used on 4:2:0"
-    );
     let y_plane = image.y_plane;
     let u_plane = image.u_plane;
     let v_plane = image.v_plane;
@@ -670,8 +880,6 @@ where
 
     check_rgb_preconditions(rgb, image.width * CHANNELS, image.height)?;
 
-    const PRECISION: i32 = 11;
-
     let range = range.get_yuv_range(BIT_DEPTH as u32);
     let kr_kb = matrix.get_kr_kb();
     let inverse_transform = get_inverse_transform(
@@ -732,12 +940,7 @@ where
                 width: image.width,
                 height: image.height,
             };
-            process_halved_chroma_row::<V, PRECISION, CHANNELS, BIT_DEPTH>(
-                image,
-                rgba,
-                &inverse_transform,
-                &range,
-            );
+            worker(image, rgba, &inverse_transform, &range);
         }
     }
 
@@ -762,12 +965,7 @@ where
             width: image.width,
             height: image.height,
         };
-        process_halved_chroma_row::<V, PRECISION, CHANNELS, BIT_DEPTH>(
-            image,
-            rgba,
-            &inverse_transform,
-            &range,
-        );
+        worker(image, rgba, &inverse_transform, &range);
     }
 
     Ok(())
@@ -788,7 +986,38 @@ pub(crate) fn yuv422_to_rgba8(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    yuv422_to_rgbx_impl::<u8, 4, 8>(image, rgb, range, matrix)
+    const P: i32 = 13;
+    yuv422_to_rgbx_impl::<u8, HalvedRowHandler<u8>, P, 4, 8>(
+        image,
+        rgb,
+        range,
+        matrix,
+        process_halved_chroma_row_cbcr::<u8, P, 4, 8>,
+    )
+}
+
+/// Converts YCgCo 422 8-bit planar format to Rgba 8-bit
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgb`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(crate) fn ycgco422_to_rgba8(
+    image: YuvPlanarImage<u8>,
+    rgb: &mut [u8],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    const P: i32 = 13;
+    yuv422_to_rgbx_impl::<u8, HalvedRowHandler<u8>, P, 4, 8>(
+        image,
+        rgb,
+        range,
+        YuvStandardMatrix::Bt709,
+        process_halved_chroma_row_cgco::<u8, P, 4, 8>,
+    )
 }
 
 /// Converts Yuv 422 10-bit planar format to Rgba 10-bit
@@ -808,7 +1037,40 @@ pub(crate) fn yuv422_to_rgba10(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    yuv422_to_rgbx_impl::<u16, 4, 10>(image, rgb, range, matrix)
+    const P: i32 = 13;
+    yuv422_to_rgbx_impl::<u16, HalvedRowHandler<u16>, P, 4, 10>(
+        image,
+        rgb,
+        range,
+        matrix,
+        process_halved_chroma_row_cbcr::<u16, P, 4, 10>,
+    )
+}
+
+/// Converts YCgCo 422 10-bit planar format to Rgba 10-bit
+///
+/// Stride here is not supported as it can be in passed from FFI.
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgb`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(crate) fn ycgco422_to_rgba10(
+    image: YuvPlanarImage<u16>,
+    rgb: &mut [u16],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    const P: i32 = 13;
+    yuv422_to_rgbx_impl::<u16, HalvedRowHandler<u16>, P, 4, 10>(
+        image,
+        rgb,
+        range,
+        YuvStandardMatrix::Bt709,
+        process_halved_chroma_row_cgco::<u16, P, 4, 10>,
+    )
 }
 
 /// Converts Yuv 422 12-bit planar format to Rgba 12-bit
@@ -828,7 +1090,40 @@ pub(crate) fn yuv422_to_rgba12(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    yuv422_to_rgbx_impl::<u16, 4, 12>(image, rgb, range, matrix)
+    const P: i32 = 13;
+    yuv422_to_rgbx_impl::<u16, HalvedRowHandler<u16>, P, 4, 12>(
+        image,
+        rgb,
+        range,
+        matrix,
+        process_halved_chroma_row_cbcr::<u16, P, 4, 12>,
+    )
+}
+
+/// Converts Yuv 422 12-bit planar format to Rgba 12-bit
+///
+/// Stride here is not supported as it can be in passed from FFI.
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgb`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(crate) fn ycgco422_to_rgba12(
+    image: YuvPlanarImage<u16>,
+    rgb: &mut [u16],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    const P: i32 = 13;
+    yuv422_to_rgbx_impl::<u16, HalvedRowHandler<u16>, P, 4, 12>(
+        image,
+        rgb,
+        range,
+        YuvStandardMatrix::Bt709,
+        process_halved_chroma_row_cgco::<u16, P, 4, 12>,
+    )
 }
 
 /// Converts Yuv 422 planar format to Rgba
@@ -844,6 +1139,8 @@ pub(crate) fn yuv422_to_rgba12(
 ///
 fn yuv422_to_rgbx_impl<
     V: Copy + AsPrimitive<i32> + 'static + Sized,
+    W: Fn(YuvPlanarImage<V>, &mut [V], &CbCrInverseTransform<i32>, &YuvChromaRange),
+    const PRECISION: i32,
     const CHANNELS: usize,
     const BIT_DEPTH: usize,
 >(
@@ -851,6 +1148,7 @@ fn yuv422_to_rgbx_impl<
     rgb: &mut [V],
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
+    worker: W,
 ) -> Result<(), ImageError>
 where
     i32: AsPrimitive<V>,
@@ -863,6 +1161,7 @@ where
         (8..=16).contains(&BIT_DEPTH),
         "Invalid bit depth is provided"
     );
+    assert!(PRECISION < 16);
     assert!(
         if BIT_DEPTH > 8 {
             size_of::<V>() == 2
@@ -871,11 +1170,6 @@ where
         },
         "Unsupported bit depth and data type combination"
     );
-    assert_ne!(
-        matrix,
-        YuvStandardMatrix::Identity,
-        "Identity matrix cannot be used on 4:2:2"
-    );
     let y_plane = image.y_plane;
     let u_plane = image.u_plane;
     let v_plane = image.v_plane;
@@ -892,7 +1186,6 @@ where
 
     let range = range.get_yuv_range(BIT_DEPTH as u32);
     let kr_kb = matrix.get_kr_kb();
-    const PRECISION: i32 = 11;
 
     let inverse_transform = get_inverse_transform(
         (1 << BIT_DEPTH) - 1,
@@ -947,12 +1240,7 @@ where
             width: image.width,
             height: image.height,
         };
-        process_halved_chroma_row::<V, PRECISION, CHANNELS, BIT_DEPTH>(
-            image,
-            rgba,
-            &inverse_transform,
-            &range,
-        );
+        worker(image, rgba, &inverse_transform, &range);
     }
 
     Ok(())
@@ -973,11 +1261,24 @@ pub(crate) fn yuv444_to_rgba8(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    if matrix == YuvStandardMatrix::Identity {
-        gbr_to_rgba8(image, rgba, range)
-    } else {
-        yuv444_to_rgbx_impl::<u8, 4, 8>(image, rgba, range, matrix)
-    }
+    yuv444_to_rgbx_impl::<u8, 4, 8>(image, rgba, range, matrix)
+}
+
+/// Converts YCgCo 444 planar format 8 bit-depth to Rgba 8 bit
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgba`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(crate) fn ycgco444_to_rgba8(
+    image: YuvPlanarImage<u8>,
+    rgba: &mut [u8],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    ycgco444_to_rgbx_impl::<u8, 4, 8>(image, rgba, range)
 }
 
 /// Converts Yuv 444 planar format 10 bit-depth to Rgba 10 bit
@@ -997,11 +1298,26 @@ pub(super) fn yuv444_to_rgba10(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    if matrix == YuvStandardMatrix::Identity {
-        gbr_to_rgba10(image, rgba, range)
-    } else {
-        yuv444_to_rgbx_impl::<u16, 4, 10>(image, rgba, range, matrix)
-    }
+    yuv444_to_rgbx_impl::<u16, 4, 10>(image, rgba, range, matrix)
+}
+
+/// Converts YCgCo 444 planar format 10 bit-depth to Rgba 10 bit
+///
+/// Stride here is not supports u16 as it can be in passed from FFI.
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgba`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(super) fn ycgco444_to_rgba10(
+    image: YuvPlanarImage<u16>,
+    rgba: &mut [u16],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    ycgco444_to_rgbx_impl::<u16, 4, 10>(image, rgba, range)
 }
 
 /// Converts Yuv 444 planar format 12 bit-depth to Rgba 12 bit
@@ -1021,11 +1337,26 @@ pub(super) fn yuv444_to_rgba12(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    if matrix == YuvStandardMatrix::Identity {
-        gbr_to_rgba12(image, rgba, range)
-    } else {
-        yuv444_to_rgbx_impl::<u16, 4, 12>(image, rgba, range, matrix)
-    }
+    yuv444_to_rgbx_impl::<u16, 4, 12>(image, rgba, range, matrix)
+}
+
+/// Converts YCgCo 444 planar format 12 bit-depth to Rgba 12 bit
+///
+/// Stride here is not supports u16 as it can be in passed from FFI.
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgba`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+/// * `matrix`: see [YuvStandardMatrix]
+///
+pub(super) fn ycgco444_to_rgba12(
+    image: YuvPlanarImage<u16>,
+    rgba: &mut [u16],
+    range: YuvIntensityRange,
+) -> Result<(), ImageError> {
+    ycgco444_to_rgbx_impl::<u16, 4, 12>(image, rgba, range)
 }
 
 /// Converts Yuv 444 planar format to Rgba
@@ -1148,6 +1479,143 @@ where
     Ok(())
 }
 
+/// Converts YCgCo 444 planar format to Rgba
+///
+/// Stride here is not supports u16 as it can be in passed from FFI.
+///
+/// # Arguments
+///
+/// * `image`: see [YuvPlanarImage]
+/// * `rgba`: RGB image layout
+/// * `range`: see [YuvIntensityRange]
+///
+fn ycgco444_to_rgbx_impl<
+    V: Copy + AsPrimitive<i32> + 'static + Sized,
+    const CHANNELS: usize,
+    const BIT_DEPTH: usize,
+>(
+    image: YuvPlanarImage<V>,
+    rgba: &mut [V],
+    yuv_range: YuvIntensityRange,
+) -> Result<(), ImageError>
+where
+    i32: AsPrimitive<V>,
+{
+    assert!(
+        CHANNELS == 3 || CHANNELS == 4,
+        "YUV 4:4:4 -> RGB is implemented only on 3 and 4 channels"
+    );
+    assert!(
+        (8..=16).contains(&BIT_DEPTH),
+        "Invalid bit depth is provided"
+    );
+    assert!(
+        if BIT_DEPTH > 8 {
+            size_of::<V>() == 2
+        } else {
+            size_of::<V>() == 1
+        },
+        "Unsupported bit depth and data type combination"
+    );
+
+    let y_plane = image.y_plane;
+    let u_plane = image.u_plane;
+    let v_plane = image.v_plane;
+    let y_stride = image.y_stride;
+    let u_stride = image.u_stride;
+    let v_stride = image.v_stride;
+    let height = image.height;
+    let width = image.width;
+
+    check_yuv_plane_preconditions(y_plane, PlaneDefinition::Y, y_stride, height)?;
+    check_yuv_plane_preconditions(u_plane, PlaneDefinition::U, u_stride, height)?;
+    check_yuv_plane_preconditions(v_plane, PlaneDefinition::V, v_stride, height)?;
+
+    check_rgb_preconditions(rgba, image.width * CHANNELS, height)?;
+
+    let range = yuv_range.get_yuv_range(BIT_DEPTH as u32);
+    const PRECISION: i32 = 13;
+
+    let bias_y = range.bias_y as i32;
+    let bias_uv = range.bias_uv as i32;
+
+    let rgb_stride = width * CHANNELS;
+
+    let y_iter = y_plane.chunks_exact(y_stride);
+    let rgb_iter = rgba.chunks_exact_mut(rgb_stride);
+    let u_iter = u_plane.chunks_exact(u_stride);
+    let v_iter = v_plane.chunks_exact(v_stride);
+
+    let max_value: i32 = (1 << BIT_DEPTH) - 1;
+
+    // All branches on generic const will be optimized out.
+    for (((y_src, u_src), v_src), rgb) in y_iter.zip(u_iter).zip(v_iter).zip(rgb_iter) {
+        let rgb_chunks = rgb.chunks_exact_mut(CHANNELS);
+        match yuv_range {
+            YuvIntensityRange::Tv => {
+                let y_coef =
+                    ((max_value as f32 / range.range_y as f32) * (1 << PRECISION) as f32) as i32;
+                for (((y_src, u_src), v_src), rgb_dst) in
+                    y_src.iter().zip(u_src).zip(v_src).zip(rgb_chunks)
+                {
+                    let y_value = (y_src.as_() - bias_y) * y_coef;
+                    let cg_value = (u_src.as_() - bias_uv) * y_coef;
+                    let co_value = (v_src.as_() - bias_uv) * y_coef;
+
+                    let t0 = y_value - cg_value;
+
+                    let r = qrshr::<PRECISION, BIT_DEPTH>(t0 + co_value);
+                    let b = qrshr::<PRECISION, BIT_DEPTH>(t0 - co_value);
+                    let g = qrshr::<PRECISION, BIT_DEPTH>(y_value + cg_value);
+
+                    if CHANNELS == 4 {
+                        rgb_dst[0] = r.as_();
+                        rgb_dst[1] = g.as_();
+                        rgb_dst[2] = b.as_();
+                        rgb_dst[3] = max_value.as_();
+                    } else if CHANNELS == 3 {
+                        rgb_dst[0] = r.as_();
+                        rgb_dst[1] = g.as_();
+                        rgb_dst[2] = b.as_();
+                    } else {
+                        unreachable!();
+                    }
+                }
+            }
+            YuvIntensityRange::Pc => {
+                for (((y_src, u_src), v_src), rgb_dst) in
+                    y_src.iter().zip(u_src).zip(v_src).zip(rgb_chunks)
+                {
+                    let y_value = y_src.as_() - bias_y;
+                    let cg_value = u_src.as_() - bias_uv;
+                    let co_value = v_src.as_() - bias_uv;
+
+                    let t0 = y_value - cg_value;
+
+                    let r = (t0 + co_value).clamp(0, max_value);
+                    let b = (t0 - co_value).clamp(0, max_value);
+                    let g = (y_value + cg_value).clamp(0, max_value);
+
+                    if CHANNELS == 4 {
+                        rgb_dst[0] = r.as_();
+                        rgb_dst[1] = g.as_();
+                        rgb_dst[2] = b.as_();
+                        rgb_dst[3] = max_value.as_();
+                    } else if CHANNELS == 3 {
+                        rgb_dst[0] = r.as_();
+                        rgb_dst[1] = g.as_();
+                        rgb_dst[2] = b.as_();
+                    } else {
+                        unreachable!();
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
 /// Converts Gbr 8 bit planar format to Rgba 8 bit-depth
 ///
 /// # Arguments
@@ -1156,7 +1624,7 @@ where
 /// * `rgb`: RGB image layout
 /// * `range`: see [YuvIntensityRange]
 ///
-fn gbr_to_rgba8(
+pub(crate) fn gbr_to_rgba8(
     image: YuvPlanarImage<u8>,
     rgb: &mut [u8],
     range: YuvIntensityRange,
@@ -1174,7 +1642,7 @@ fn gbr_to_rgba8(
 /// * `rgba`: RGBx image layout
 /// * `range`: see [YuvIntensityRange]
 ///
-fn gbr_to_rgba10(
+pub(crate) fn gbr_to_rgba10(
     image: YuvPlanarImage<u16>,
     rgba: &mut [u16],
     range: YuvIntensityRange,
@@ -1192,7 +1660,7 @@ fn gbr_to_rgba10(
 /// * `rgba`: RGBx image layout
 /// * `range`: see [YuvIntensityRange]
 ///
-fn gbr_to_rgba12(
+pub(crate) fn gbr_to_rgba12(
     image: YuvPlanarImage<u16>,
     rgba: &mut [u16],
     range: YuvIntensityRange,


Reviews:
user: login: 197g
body: YCgCo is just a color model, not an absolutely defined system. It could be that any part along the way defines a different whitepoint, RGB primiaries unless I'm missing that AVIF defines one normative convention here, i.e. you can choose any of the cross product of primaries and transfer characteristics. There are probably some parameters encoded in the avif that are not being utilized by the decoding path.

All that being said; this is going to happen more often with more recent formats until non-rgb representations are better. Doing more conversions to fit everything into the sRGB box is lossy and expensive. With a bit of luck, I'll be able to spend significant amounts of time on aligning that the rest of the year.. So no need to worry about in the PR immediately.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T23:09:38Z
user: login: 197g
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T23:28:37Z
user: login: awxkee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T23:43:25Z
user: login: 197g
body: I like that modelling much better :+1:  The document is unfortunately complex, so it isn't exactly surprising that there would be some necessary complexity in processing the data. Integrations like this should hopefully become simpler at some point. My biggest headache in understanding any code at a glance is that it is never _locally_ obvious what the interpretation of some numbers should be i.e. is `r` linear or a signal? etc, obviously complicated by the fact that all is writing to the same target for efficiency which is at odds with encoding such information as a static type.

Imo, this actually does a decent job of abstracting away the processing paths exhaustively.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T08:28:52Z
user: login: 197g
body: Actually seems quite straightforward, very similar style to the other impls but using less parameters as the transform is fixed. If anything it's hard to review because everything looks similar (but not quite the same)  We'll see about the color profile issue in the future, hopefully. :+1: with the nit fixed.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T18:57:26Z
user: login: awxkee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T19:57:30Z

Review comments:
diff_hunk: @@ -449,7 +456,36 @@ pub(crate) fn yuv420_to_rgba8(
     range: YuvIntensityRange,
     matrix: YuvStandardMatrix,
 ) -> Result<(), ImageError> {
-    yuv420_to_rgbx::<u8, 4, 8>(image, rgb, range, matrix)
+    const P: i32 = 13;
+    if matrix == YuvStandardMatrix::CgCo {
+        yuv420_to_rgbx::<
+            u8,
+            fn(YuvPlanarImage<u8>, &mut [u8], &CbCrInverseTransform<i32>, &YuvChromaRange),
path: src/codecs/avif/yuv.rs
user: login: 197g
body: Consider a type alias for this function type to mirror the structure of the bound:

```suggestion
type FnPtrPlanar<Sample> = fn(YuvPlanarImage<Sample>, &mut [Sample], &CbCrInverseTransform<i32>, &YuvChromaRange);
```
created_at: 2025-05-10T22:58:21Z
author_association: MEMBER
diff_hunk: @@ -201,6 +201,7 @@ pub(crate) enum YuvStandardMatrix {
     Smpte240,
     Bt470_6,
     Identity,
+    CgCo,
path: src/codecs/avif/yuv.rs
user: login: 197g
body: I'm really not sure what this is supposed to mean. That enum defines the combined transfer matrix you get when combining a specific primary with the Yuv encoding, so that you have coefficients for directly computing RGB' from YCgCr (see eq 39, 66-71 n ITU H.273). Those coefficients are undefined for YCoCg (Matrix coefficients = 8, eq 51-54)?

 Instead, a new enum of `YCgCoStandardMatrix` might be the way to go if we want to go directly to linear 
created_at: 2025-05-10T23:28:17Z
author_association: MEMBER
diff_hunk: @@ -201,6 +201,7 @@ pub(crate) enum YuvStandardMatrix {
     Smpte240,
     Bt470_6,
     Identity,
+    CgCo,
path: src/codecs/avif/yuv.rs
user: login: awxkee
body: That's right. I wanted to avoid introducing a new subset of methods or enums.
Adding more methods or enums would make the decoder more complex, and last time, people weren’t very happy with a decoder that had tricky logic.
Instead somewhat vague definition in the standard matrix, it would make the decoder even more complicated to support additional YCgCo methods.
If you think that something like this is better, then I'll do it. However, it won't work for YCgCo-R/YCgCo-Re/YCgCo-Ro, because they cannot fit exactly in the same data size. 
```rust
if matrix == YCgCo {
      let worker = match self.picture.pixel_layout() {
                PixelLayout::I400 => unreachable(),
                PixelLayout::I420 => ycgco420_to_rgba8,
                PixelLayout::I422 => ycgco422_to_rgba8,
                PixelLayout::I444 => ycgco444_to_rgba8,
            };

            worker(image, buf, yuv_range, color_matrix)?;
} else {
      let worker = match self.picture.pixel_layout() {
                PixelLayout::I400 => yuv400_to_rgba8,
                PixelLayout::I420 => yuv420_to_rgba8,
                PixelLayout::I422 => yuv422_to_rgba8,
                PixelLayout::I444 => yuv444_to_rgba8,
            };

            worker(image, buf, yuv_range, color_matrix)?;
}
```

created_at: 2025-05-10T23:43:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -610,6 +703,125 @@ fn process_halved_chroma_row<
     }
 }
 
+fn process_halved_chroma_row_cgco<
+    V: Copy + AsPrimitive<i32> + 'static + Sized,
+    const PRECISION: i32,
+    const CN: usize,
path: src/codecs/avif/yuv.rs
user: login: 197g
body: I shortly pondered if could make a static assertion here to clarify `CN in {3,4}` but then noticed that all my attempted would skyrocket the wtf-rate-per-minute so let's not. We might do it if bounds on const-expressions get implemented in Rust.
created_at: 2025-05-12T18:47:15Z
author_association: MEMBER
diff_hunk: @@ -610,6 +703,125 @@ fn process_halved_chroma_row<
     }
 }
 
+fn process_halved_chroma_row_cgco<
+    V: Copy + AsPrimitive<i32> + 'static + Sized,
+    const PRECISION: i32,
+    const CN: usize,
path: src/codecs/avif/yuv.rs
user: login: 197g
body: Nit: In many other instances this is spelled out:

```suggestion
    const CHANNELS: usize,
```
created_at: 2025-05-12T18:48:32Z
author_association: MEMBER
diff_hunk: @@ -610,6 +703,125 @@ fn process_halved_chroma_row<
     }
 }
 
+fn process_halved_chroma_row_cgco<
+    V: Copy + AsPrimitive<i32> + 'static + Sized,
+    const PRECISION: i32,
+    const CN: usize,
path: src/codecs/avif/yuv.rs
user: login: awxkee
body: I'm not sure this is important to make asserts in such deep private methods. Anyways the method that calls it have assert to ensure that channels are valid.
created_at: 2025-05-12T19:57:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: awxkee
created_at: 2025-05-10T23:30:09Z
body: > There are probably some parameters encoded in the avif that are not being utilized by the decoding path.

Ah, yes, I embedded Display P3 profile.
author_association: CONTRIBUTOR
user: login: awxkee
created_at: 2025-05-11T08:54:48Z
body: H.273 defines only matrix coefficients values 10 and 13 as operating on linear intensities to compute Y, so any other matrix is not linear. Thus, for any other matrix, transfer characteristics should be applied.

However, the standard does not specify whether it is correct to apply linear transfer characteristics in those cases.
author_association: CONTRIBUTOR
user: login: 197g
created_at: 2025-05-11T09:13:33Z
body: I believe it says that eq 20-22 apply in these cases too, and eq 45, 76, 82, 85 give the non-linear E_y signal in all other cases where a luminance is involved as derived form the non-linear rgb/lms signals. The 'constant luminance' term refers to the fact that luminance is calculated from the linear values afaik.
author_association: MEMBER
user: login: awxkee
created_at: 2025-05-12T19:55:35Z
body: I think the issue with color profiles was my fault — my own software correctly recognized the tagged image, then it embedded a valid profile, but software author wasn't able to figure it out :)

As for readability yes, before adding something else, it's likely that this needs to be split into a few modules. IMHO, adding traits to make it dynamic or sophisticated static dispatch will make it even more complex. I'm not sure what else here to do, because transform is more or less straightforward, and it is hard to simplify it further. I even intentionally skipped ycgco420/422 full range simplified implementation to avoid code bloating. 

If anything I did [cms](https://github.com/awxkee/moxcms). I can't say it is full implementation of ICC v4, but it should work for the most of web images, and it is really fast on mobile devices what is the most importance to me. We've integrated it into jxl-oxide, and everything seems to be going well. So if you're planning to implement profile support, you could copy the biggest part of it, except hot paths it is safe Rust.
author_association: CONTRIBUTOR
user: login: awxkee
created_at: 2025-05-15T21:32:43Z
body: Is there something else to address?
author_association: CONTRIBUTOR
user: login: 197g
created_at: 2025-05-15T22:09:22Z
body: Given your comment and the commit after the approval, I wasn't entirely sure either if there may be something you still wanted to cleanup. But I'll take this question as a no.
author_association: MEMBER
user: login: awxkee
created_at: 2025-05-15T22:11:01Z
body: Ah, no. I'll do separate PR for cleaning up
author_association: CONTRIBUTOR
user: login: awxkee
created_at: 2025-05-15T22:52:27Z
body: ycgco420_to_rgba8 invokes invalid executor. I'll do tomorrow PR with refactorings, if it won't this is reminder that this one still needs to be fixed.
author_association: CONTRIBUTOR