diff --git a/Cargo.toml b/Cargo.toml index 90c01e7e5b..8fb04935da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,6 @@ num-traits = { version = "0.2.0" } # Optional dependencies color_quant = { version = "1.1", optional = true } dav1d = { version = "0.10.3", optional = true } -dcv-color-primitives = { version = "0.6.1", optional = true } exr = { version = "1.5.0", optional = true } gif = { version = "0.13", optional = true } image-webp = { version = "0.2.0", optional = true } @@ -91,7 +90,7 @@ webp = ["dep:image-webp"] rayon = ["dep:rayon", "ravif?/threading"] # Enables multi-threading nasm = ["ravif?/asm"] # Enables use of nasm by rav1e (requires nasm to be installed) color_quant = ["dep:color_quant"] # Enables color quantization -avif-native = ["dep:mp4parse", "dep:dcv-color-primitives", "dep:dav1d"] # Enable native dependency libdav1d +avif-native = ["dep:mp4parse", "dep:dav1d"] # Enable native dependency libdav1d benchmarks = [] # Build some inline benchmarks. Useful only during development (requires nightly Rust) serde = ["dep:serde"] diff --git a/src/codecs/avif/decoder.rs b/src/codecs/avif/decoder.rs index 15b2244b09..04e43f666a 100644 --- a/src/codecs/avif/decoder.rs +++ b/src/codecs/avif/decoder.rs @@ -1,17 +1,20 @@ //! Decoding of AVIF images. +use crate::error::{ + DecodingError, ImageFormatHint, LimitError, LimitErrorKind, UnsupportedError, + UnsupportedErrorKind, +}; +use crate::{ColorType, ImageDecoder, ImageError, ImageFormat, ImageResult}; /// /// The [AVIF] specification defines an image derivative of the AV1 bitstream, an open video codec. /// /// [AVIF]: https://aomediacodec.github.io/av1-avif/ use std::error::Error; +use std::fmt::{Display, Formatter}; use std::io::Read; use std::marker::PhantomData; -use crate::error::{DecodingError, ImageFormatHint, UnsupportedError, UnsupportedErrorKind}; -use crate::{ColorType, ImageDecoder, ImageError, ImageFormat, ImageResult}; - +use crate::codecs::avif::yuv::*; use dav1d::{PixelLayout, PlanarImageComponent}; -use dcv_color_primitives as dcp; use mp4parse::{read_avif, ParseStrictness}; fn error_map>>(err: E) -> ImageError { @@ -28,6 +31,39 @@ pub struct AvifDecoder { icc_profile: Option>, } +#[derive(Debug, Clone, PartialEq, Eq)] +enum AvifDecoderError { + AlphaPlaneFormat(PixelLayout), + YuvLayoutOnIdentityMatrix(PixelLayout), +} + +impl Display for AvifDecoderError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + 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"), + }, + 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") + } + PixelLayout::I420 => { + 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") + } + PixelLayout::I444 => unreachable!("This option must be handled correctly"), + }, + } + } +} + +impl Error for AvifDecoderError {} + impl AvifDecoder { /// Create a new decoder that reads its input from `r`. pub fn new(mut r: R) -> ImageResult { @@ -56,17 +92,7 @@ impl AvifDecoder { match picture.bit_depth() { 8 => (), - 10 | 12 => { - return ImageResult::Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormatHint::Exact(ImageFormat::Avif), - UnsupportedErrorKind::GenericFeature(format!( - "Only 8 bit depth is supported but was {}", - picture.bit_depth() - )), - ), - )) - } + 10 | 12 => (), _ => { return ImageResult::Err(ImageError::Decoding(DecodingError::new( ImageFormatHint::Exact(ImageFormat::Avif), @@ -86,13 +112,209 @@ impl AvifDecoder { } } +/// Reshaping incorrectly aligned or sized FFI data into Rust constraints +fn reshape_plane(source: &[u8], stride: usize, width: usize, height: usize) -> Vec { + let mut target_plane = vec![0u16; width * height]; + for (shaped_row, src_row) in target_plane + .chunks_exact_mut(width) + .zip(source.chunks_exact(stride)) + { + for (dst, src) in shaped_row.iter_mut().zip(src_row.chunks_exact(2)) { + *dst = u16::from_ne_bytes([src[0], src[1]]); + } + } + target_plane +} + +struct Plane16View<'a> { + data: std::borrow::Cow<'a, [u16]>, + stride: usize, +} + +impl Default for Plane16View<'_> { + fn default() -> Self { + Plane16View { + data: std::borrow::Cow::Owned(vec![]), + stride: 0, + } + } +} + +/// This is correct to transmute FFI data for Y plane and Alpha plane +fn transmute_y_plane16( + plane: &dav1d::Plane, + stride: usize, + width: usize, + height: usize, +) -> Plane16View { + let mut y_plane_stride = stride >> 1; + + let mut bind_y = vec![]; + let plane_ref = plane.as_ref(); + + let mut shape_y_plane = || { + y_plane_stride = width; + bind_y = reshape_plane(plane_ref, stride, width, height); + }; + + if stride & 1 == 0 { + match bytemuck::try_cast_slice(plane_ref) { + Ok(slice) => Plane16View { + data: std::borrow::Cow::Borrowed(slice), + stride: y_plane_stride, + }, + Err(_) => { + shape_y_plane(); + Plane16View { + data: std::borrow::Cow::Owned(bind_y), + stride: y_plane_stride, + } + } + } + } else { + shape_y_plane(); + Plane16View { + data: std::borrow::Cow::Owned(bind_y), + stride: y_plane_stride, + } + } +} + +/// This is correct to transmute FFI data for Y plane and Alpha plane +fn transmute_chroma_plane16( + plane: &dav1d::Plane, + pixel_layout: PixelLayout, + stride: usize, + width: usize, + height: usize, +) -> Plane16View { + let plane_ref = plane.as_ref(); + let mut chroma_plane_stride = stride >> 1; + let mut bind_chroma = vec![]; + + let mut shape_chroma_plane = || { + chroma_plane_stride = match pixel_layout { + PixelLayout::I400 => unreachable!(), + PixelLayout::I420 | PixelLayout::I422 => (width + 1) / 2, + PixelLayout::I444 => width, + }; + let u_plane_height = match pixel_layout { + PixelLayout::I400 => unreachable!(), + PixelLayout::I420 => (height + 1) / 2, + PixelLayout::I422 | PixelLayout::I444 => height, + }; + bind_chroma = reshape_plane(plane_ref, stride, chroma_plane_stride, u_plane_height); + }; + + if stride & 1 == 0 { + match bytemuck::try_cast_slice(plane_ref) { + Ok(slice) => Plane16View { + data: std::borrow::Cow::Borrowed(slice), + stride: chroma_plane_stride, + }, + Err(_) => { + shape_chroma_plane(); + Plane16View { + data: std::borrow::Cow::Owned(bind_chroma), + stride: chroma_plane_stride, + } + } + } + } else { + shape_chroma_plane(); + Plane16View { + data: std::borrow::Cow::Owned(bind_chroma), + stride: chroma_plane_stride, + } + } +} + +/// Getting one of prebuilt matrix of fails +fn get_matrix( + david_matrix: dav1d::pixel::MatrixCoefficients, +) -> Result { + match david_matrix { + dav1d::pixel::MatrixCoefficients::Identity => Ok(YuvStandardMatrix::Identity), + dav1d::pixel::MatrixCoefficients::BT709 => Ok(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::Reserved => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Avif.into(), + UnsupportedErrorKind::GenericFeature( + "Using 'Reserved' color matrix is not supported".to_string(), + ), + ), + )), + 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::BT2020NonConstantLuminance => { + Ok(YuvStandardMatrix::Bt2020) + } + dav1d::pixel::MatrixCoefficients::BT2020ConstantLuminance => { + // This matrix significantly differs from others because linearize values is required + // to compute Y instead of Y'. + // Actually it is almost everywhere is not implemented. + // Libavif + libheif missing this also so actually AVIF images + // with CL BT.2020 might be made only by mistake + Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Avif.into(), + UnsupportedErrorKind::GenericFeature( + "BT2020ConstantLuminance matrix is not supported".to_string(), + ), + ), + )) + } + dav1d::pixel::MatrixCoefficients::ST2085 => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Avif.into(), + UnsupportedErrorKind::GenericFeature("ST2085 matrix is not supported".to_string()), + ), + )), + dav1d::pixel::MatrixCoefficients::ChromaticityDerivedConstantLuminance + | dav1d::pixel::MatrixCoefficients::ChromaticityDerivedNonConstantLuminance => Err( + ImageError::Unsupported(UnsupportedError::from_format_and_kind( + ImageFormat::Avif.into(), + UnsupportedErrorKind::GenericFeature( + "Chromaticity Derived Luminance matrix is not supported".to_string(), + ), + )), + ), + dav1d::pixel::MatrixCoefficients::ICtCp => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Avif.into(), + UnsupportedErrorKind::GenericFeature( + "ICtCp Derived Luminance matrix is not supported".to_string(), + ), + ), + )), + } +} + impl ImageDecoder for AvifDecoder { fn dimensions(&self) -> (u32, u32) { (self.picture.width(), self.picture.height()) } fn color_type(&self) -> ColorType { - ColorType::Rgba8 + if self.picture.bit_depth() == 8 { + ColorType::Rgba8 + } else { + ColorType::Rgba16 + } } fn icc_profile(&mut self) -> ImageResult>> { @@ -102,93 +324,231 @@ impl ImageDecoder for AvifDecoder { fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - if self.picture.pixel_layout() != PixelLayout::I400 { - let pixel_format = match self.picture.pixel_layout() { - PixelLayout::I400 => todo!(), - PixelLayout::I420 => dcp::PixelFormat::I420, - PixelLayout::I422 => dcp::PixelFormat::I422, - PixelLayout::I444 => dcp::PixelFormat::I444, + let bit_depth = self.picture.bit_depth(); + + // Normally this should never happen, + // if this happens then there is an incorrect implementation somewhere else + assert!(bit_depth == 8 || bit_depth == 10 || bit_depth == 12); + + let (width, height) = self.dimensions(); + // This is suspicious if this happens, better fail early + if width == 0 || height == 0 { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); + } + + let yuv_range = match self.picture.color_range() { + dav1d::pixel::YUVRange::Limited => YuvIntensityRange::Tv, + dav1d::pixel::YUVRange::Full => YuvIntensityRange::Pc, + }; + + let color_matrix = get_matrix(self.picture.matrix_coefficients())?; + + // Identity matrix should be possible only on 4:4:4 + if color_matrix == YuvStandardMatrix::Identity + && self.picture.pixel_layout() != PixelLayout::I444 + { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Avif.into(), + AvifDecoderError::YuvLayoutOnIdentityMatrix(self.picture.pixel_layout()), + ))); + } + + if bit_depth == 8 { + let ref_y = self.picture.plane(PlanarImageComponent::Y); + let ref_u = self.picture.plane(PlanarImageComponent::U); + let ref_v = self.picture.plane(PlanarImageComponent::V); + + let image = YuvPlanarImage { + y_plane: ref_y.as_ref(), + y_stride: self.picture.stride(PlanarImageComponent::Y) as usize, + u_plane: ref_u.as_ref(), + u_stride: self.picture.stride(PlanarImageComponent::U) as usize, + v_plane: ref_v.as_ref(), + v_stride: self.picture.stride(PlanarImageComponent::V) as usize, + width: width as usize, + 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, }; - let src_color_space = match (self.picture.color_primaries(), self.picture.color_range()) - { - (dav1d::pixel::ColorPrimaries::BT709, dav1d::pixel::YUVRange::Full) => { - dcp::ColorSpace::Bt709FR + + worker(image, buf, yuv_range, color_matrix)?; + + // Squashing alpha plane into a picture + if let Some(picture) = self.alpha_picture { + if picture.pixel_layout() != PixelLayout::I400 { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Avif.into(), + AvifDecoderError::AlphaPlaneFormat(picture.pixel_layout()), + ))); } - (dav1d::pixel::ColorPrimaries::BT709, dav1d::pixel::YUVRange::Limited) => { - dcp::ColorSpace::Bt709 + + let stride = picture.stride(PlanarImageComponent::Y) as usize; + let plane = picture.plane(PlanarImageComponent::Y); + + for (buf, slice) in Iterator::zip( + buf.chunks_exact_mut(width as usize * 4), + plane.as_ref().chunks_exact(stride), + ) { + for (rgba, a_src) in buf.chunks_exact_mut(4).zip(slice) { + rgba[3] = *a_src; + } } - (_, dav1d::pixel::YUVRange::Full) => dcp::ColorSpace::Bt601FR, - (_, dav1d::pixel::YUVRange::Limited) => dcp::ColorSpace::Bt601, - }; - let src_format = dcp::ImageFormat { - pixel_format, - color_space: src_color_space, - num_planes: 3, - }; - let dst_format = dcp::ImageFormat { - pixel_format: dcp::PixelFormat::Rgba, - color_space: dcp::ColorSpace::Rgb, - num_planes: 1, - }; - let (width, height) = self.dimensions(); - let planes = &[ - self.picture.plane(PlanarImageComponent::Y), - self.picture.plane(PlanarImageComponent::U), - self.picture.plane(PlanarImageComponent::V), - ]; - let src_buffers = planes.iter().map(AsRef::as_ref).collect::>(); - let strides = &[ - self.picture.stride(PlanarImageComponent::Y) as usize, + } + } else { + // // 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)?; + } 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)?; + for (dst, src) in buf.chunks_exact_mut(2).zip(aligned_store.iter()) { + let bytes = src.to_ne_bytes(); + dst[0] = bytes[0]; + dst[1] = bytes[1]; + } + } + } + + Ok(()) + } + + fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { + (*self).read_image(buf) + } +} + +impl AvifDecoder { + fn process_16bit_picture( + &self, + target: &mut [u16], + yuv_range: YuvIntensityRange, + color_matrix: YuvStandardMatrix, + ) -> ImageResult<()> { + let y_dav1d_plane = self.picture.plane(PlanarImageComponent::Y); + + let (width, height) = (self.picture.width(), self.picture.height()); + let bit_depth = self.picture.bit_depth(); + + // dav1d may return not aligned and not correctly constrained data, + // or at least I can't find guarantees on that + // so if it is happened, instead casting we'll need to reshape it into a target slice + // required criteria: bytemuck allows this align of this data, and stride must be dividable by 2 + + let y_plane_view = transmute_y_plane16( + &y_dav1d_plane, + self.picture.stride(PlanarImageComponent::Y) as usize, + width as usize, + height as usize, + ); + + let u_dav1d_plane = self.picture.plane(PlanarImageComponent::U); + let v_dav1d_plane = self.picture.plane(PlanarImageComponent::V); + let mut u_plane_view = Plane16View::default(); + let mut v_plane_view = Plane16View::default(); + + if self.picture.pixel_layout() != PixelLayout::I400 { + u_plane_view = transmute_chroma_plane16( + &u_dav1d_plane, + self.picture.pixel_layout(), self.picture.stride(PlanarImageComponent::U) as usize, + width as usize, + height as usize, + ); + v_plane_view = transmute_chroma_plane16( + &v_dav1d_plane, + self.picture.pixel_layout(), self.picture.stride(PlanarImageComponent::V) as usize, - ]; - let dst_buffers = &mut [&mut buf[..]]; - dcp::convert_image( - width, - height, - &src_format, - Some(strides), - &src_buffers, - &dst_format, - None, - dst_buffers, - ) - .map_err(error_map)?; - } else { - let plane = self.picture.plane(PlanarImageComponent::Y); - buf.copy_from_slice(plane.as_ref()); + width as usize, + height as usize, + ); } - if let Some(picture) = self.alpha_picture { + let image = YuvPlanarImage { + y_plane: y_plane_view.data.as_ref(), + y_stride: y_plane_view.stride, + u_plane: u_plane_view.data.as_ref(), + u_stride: u_plane_view.stride, + v_plane: v_plane_view.data.as_ref(), + v_stride: v_plane_view.stride, + width: width as usize, + 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 + } + } + 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, color_matrix)?; + + // Squashing alpha plane into a picture + if let Some(picture) = &self.alpha_picture { if picture.pixel_layout() != PixelLayout::I400 { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature(format!( - "Alpha must be PixelLayout::I400 but was: {:?}", - picture.pixel_layout() // PixelLayout does not implement display - )), - ), - )); + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Avif.into(), + AvifDecoderError::AlphaPlaneFormat(picture.pixel_layout()), + ))); } - let stride = picture.stride(PlanarImageComponent::Y) as usize; - let plane = picture.plane(PlanarImageComponent::Y); - let width = picture.width(); + + let a_dav1d_plane = picture.plane(PlanarImageComponent::Y); + let a_plane_view = transmute_y_plane16( + &a_dav1d_plane, + picture.stride(PlanarImageComponent::Y) as usize, + width as usize, + height as usize, + ); + for (buf, slice) in Iterator::zip( - buf.chunks_exact_mut(width as usize * 4), - plane.as_ref().chunks_exact(stride), + target.chunks_exact_mut(width as usize * 4), + a_plane_view.data.as_ref().chunks_exact(a_plane_view.stride), ) { - for i in 0..width as usize { - buf[3 + i * 4] = slice[i]; + for (rgba, a_src) in buf.chunks_exact_mut(4).zip(slice) { + rgba[3] = *a_src; } } } - Ok(()) - } + // Expand current bit depth to target 16 + let target_expand_bits = 16u32 - self.picture.bit_depth() as u32; + for item in target.iter_mut() { + *item = (*item << target_expand_bits) | (*item >> (16 - target_expand_bits)); + } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(()) } } diff --git a/src/codecs/avif/mod.rs b/src/codecs/avif/mod.rs index 89edfc2c97..7c18cc5f30 100644 --- a/src/codecs/avif/mod.rs +++ b/src/codecs/avif/mod.rs @@ -12,3 +12,5 @@ pub use self::encoder::{AvifEncoder, ColorSpace}; mod decoder; #[cfg(feature = "avif")] mod encoder; +#[cfg(feature = "avif-native")] +mod yuv; diff --git a/src/codecs/avif/yuv.rs b/src/codecs/avif/yuv.rs new file mode 100644 index 0000000000..77155b3286 --- /dev/null +++ b/src/codecs/avif/yuv.rs @@ -0,0 +1,1322 @@ +use crate::error::DecodingError; +use crate::{ImageError, ImageFormat}; +use num_traits::AsPrimitive; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Copy, Clone)] +/// Representation of inversion matrix +struct CbCrInverseTransform { + pub y_coef: T, + pub cr_coef: T, + pub cb_coef: T, + pub g_coeff_1: T, + pub g_coeff_2: T, +} + +impl CbCrInverseTransform { + fn to_integers(self, precision: u32) -> CbCrInverseTransform { + let precision_scale: i32 = 1i32 << (precision as i32); + let cr_coef = (self.cr_coef * precision_scale as f32) as i32; + let cb_coef = (self.cb_coef * precision_scale as f32) as i32; + let y_coef = (self.y_coef * precision_scale as f32) as i32; + let g_coef_1 = (self.g_coeff_1 * precision_scale as f32) as i32; + let g_coef_2 = (self.g_coeff_2 * precision_scale as f32) as i32; + CbCrInverseTransform:: { + y_coef, + cr_coef, + cb_coef, + g_coeff_1: g_coef_1, + g_coeff_2: g_coef_2, + } + } +} + +#[derive(Copy, Clone, Debug)] +struct ErrorSize { + expected: usize, + received: usize, +} + +#[derive(Copy, Clone, Debug)] +enum PlaneDefinition { + Y, + U, + V, +} + +impl Display for PlaneDefinition { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + PlaneDefinition::Y => f.write_str("Luma"), + PlaneDefinition::U => f.write_str("U chroma"), + PlaneDefinition::V => f.write_str("V chroma"), + } + } +} + +#[derive(Debug, Clone, Copy)] +enum YuvConversionError { + YuvPlaneSizeMismatch(PlaneDefinition, ErrorSize), + RgbDestinationSizeMismatch(ErrorSize), +} + +impl Display for YuvConversionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + YuvConversionError::YuvPlaneSizeMismatch(plane, error_size) => { + f.write_fmt(format_args!( + "For plane {} expected size is {} but was received {}", + plane, error_size.received, error_size.expected, + )) + } + YuvConversionError::RgbDestinationSizeMismatch(error_size) => { + f.write_fmt(format_args!( + "For RGB destination expected size is {} but was received {}", + error_size.received, error_size.expected, + )) + } + } + } +} + +impl std::error::Error for YuvConversionError {} + +#[inline] +fn check_yuv_plane_preconditions( + plane: &[V], + plane_definition: PlaneDefinition, + stride: usize, + height: usize, +) -> Result<(), ImageError> { + if plane.len() != stride * height { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Avif.into(), + YuvConversionError::YuvPlaneSizeMismatch( + plane_definition, + ErrorSize { + expected: stride * height, + received: plane.len(), + }, + ), + ))); + } + Ok(()) +} + +#[inline] +fn check_rgb_preconditions( + rgb_data: &[V], + stride: usize, + height: usize, +) -> Result<(), ImageError> { + if rgb_data.len() != stride * height { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Avif.into(), + YuvConversionError::RgbDestinationSizeMismatch(ErrorSize { + expected: stride * height, + received: rgb_data.len(), + }), + ))); + } + Ok(()) +} + +/// Transformation YUV to RGB with coefficients as specified in [ITU-R](https://www.itu.int/rec/T-REC-H.273/en) +fn get_inverse_transform( + range_bgra: u32, + range_y: u32, + range_uv: u32, + kr: f32, + kb: f32, + precision: u32, +) -> CbCrInverseTransform { + let range_uv = range_bgra as f32 / range_uv as f32; + let y_coef = range_bgra as f32 / range_y as f32; + let cr_coeff = (2f32 * (1f32 - kr)) * range_uv; + let cb_coeff = (2f32 * (1f32 - kb)) * range_uv; + let kg = 1.0f32 - kr - kb; + assert_ne!(kg, 0., "1.0f - kr - kg must not be 0"); + let g_coeff_1 = (2f32 * ((1f32 - kr) * kr / kg)) * range_uv; + let g_coeff_2 = (2f32 * ((1f32 - kb) * kb / kg)) * range_uv; + let exact_transform = CbCrInverseTransform { + y_coef, + cr_coef: cr_coeff, + cb_coef: cb_coeff, + g_coeff_1, + g_coeff_2, + }; + exact_transform.to_integers(precision) +} + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +/// Declares YUV range TV (limited) or PC (full), +/// more info [ITU-R](https://www.itu.int/rec/T-REC-H.273/en) +pub(crate) enum YuvIntensityRange { + /// Limited range Y ∈ [16 << (depth - 8), 16 << (depth - 8) + 224 << (depth - 8)], + /// UV ∈ [-1 << (depth - 1), -1 << (depth - 1) + 1 << (depth - 1)] + Tv, + /// Full range Y ∈ [0, 2^bit_depth - 1], + /// UV ∈ [-1 << (depth - 1), -1 << (depth - 1) + 2^bit_depth - 1] + Pc, +} + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +struct YuvChromaRange { + pub bias_y: u32, + pub bias_uv: u32, + pub range_y: u32, + pub range_uv: u32, + pub range: YuvIntensityRange, +} + +impl YuvIntensityRange { + const fn get_yuv_range(self, depth: u32) -> YuvChromaRange { + match self { + YuvIntensityRange::Tv => YuvChromaRange { + bias_y: 16 << (depth - 8), + bias_uv: 1 << (depth - 1), + range_y: 219 << (depth - 8), + range_uv: 224 << (depth - 8), + range: self, + }, + YuvIntensityRange::Pc => YuvChromaRange { + bias_y: 0, + bias_uv: 1 << (depth - 1), + range_uv: (1 << depth) - 1, + range_y: (1 << depth) - 1, + range: self, + }, + } + } +} + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +/// 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 { + Bt601, + Bt709, + Bt2020, + Smpte240, + Bt470_6, + Identity, +} + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +struct YuvBias { + kr: f32, + kb: f32, +} + +impl YuvStandardMatrix { + const fn get_kr_kb(self) -> YuvBias { + match self { + YuvStandardMatrix::Bt601 => YuvBias { + kr: 0.299f32, + kb: 0.114f32, + }, + YuvStandardMatrix::Bt709 => YuvBias { + kr: 0.2126f32, + kb: 0.0722f32, + }, + YuvStandardMatrix::Bt2020 => YuvBias { + kr: 0.2627f32, + kb: 0.0593f32, + }, + YuvStandardMatrix::Smpte240 => YuvBias { + kr: 0.087f32, + kb: 0.212f32, + }, + YuvStandardMatrix::Bt470_6 => YuvBias { + kr: 0.2220f32, + kb: 0.0713f32, + }, + YuvStandardMatrix::Identity => unreachable!(), + } + } +} + +pub(crate) struct YuvPlanarImage<'a, T> { + pub(crate) y_plane: &'a [T], + pub(crate) y_stride: usize, + pub(crate) u_plane: &'a [T], + pub(crate) u_stride: usize, + pub(crate) v_plane: &'a [T], + pub(crate) v_stride: usize, + pub(crate) width: usize, + pub(crate) height: usize, +} + +#[inline(always)] +/// Saturating rounding shift right against bit depth +fn qrshr(val: i32) -> i32 { + let rounding: i32 = 1 << (PRECISION - 1); + let max_value: i32 = (1 << BIT_DEPTH) - 1; + ((val + rounding) >> PRECISION).clamp(0, max_value) +} + +/// Converts Yuv 400 planar format 8 bit to Rgba 8 bit +/// +/// # Arguments +/// +/// * `image`: see [YuvGrayImage] +/// * `rgba`: RGBA image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +pub(crate) fn yuv400_to_rgba8( + image: YuvPlanarImage, + rgba: &mut [u8], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv400_to_rgbx_impl::(image, rgba, range, matrix) +} + +/// Converts Yuv 400 planar format 10 bit to Rgba 10 bit +/// +/// Stride here is not supported as it can be in passed from FFI. +/// +/// # Arguments +/// +/// * `image`: see [YuvGrayImage] +/// * `rgba`: RGBA image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +pub(crate) fn yuv400_to_rgba10( + image: YuvPlanarImage, + rgba: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv400_to_rgbx_impl::(image, rgba, range, matrix) +} + +/// Converts Yuv 400 planar format 12 bit to Rgba 12 bit +/// +/// Stride here is not supported as it can be in passed from FFI. +/// +/// # Arguments +/// +/// * `image`: see [YuvGrayImage] +/// * `rgba`: RGBA image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +pub(crate) fn yuv400_to_rgba12( + image: YuvPlanarImage, + rgba: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv400_to_rgbx_impl::(image, rgba, range, matrix) +} + +/// Converts Yuv 400 planar format to Rgba +/// +/// Stride here is not supported as it can be in passed from FFI. +/// +/// # Arguments +/// +/// * `image`: see [YuvGrayImage] +/// * `rgba`: RGBA image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +#[inline] +fn yuv400_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( + image: YuvPlanarImage, + rgba: &mut [V], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> +where + i32: AsPrimitive, +{ + assert!( + CHANNELS == 3 || CHANNELS == 4, + "YUV 4:0:0 -> 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::() == 2 + } else { + size_of::() == 1 + }, + "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; + let height = image.height; + let width = image.width; + + check_yuv_plane_preconditions(y_plane, PlaneDefinition::Y, y_stride, height)?; + check_rgb_preconditions(rgba, width * CHANNELS, height)?; + + let rgba_stride = width * CHANNELS; + + let max_value = (1 << BIT_DEPTH) - 1; + + // If luma plane is in full range it can be just redistributed across the image + if range == YuvIntensityRange::Pc { + let y_iter = y_plane.chunks_exact(y_stride); + let rgb_iter = rgba.chunks_exact_mut(rgba_stride); + + // All branches on generic const will be optimized out. + for (y_src, rgb) in y_iter.zip(rgb_iter) { + let rgb_chunks = rgb.chunks_exact_mut(CHANNELS); + + for (y_src, rgb_dst) in y_src.iter().zip(rgb_chunks) { + let r = *y_src; + rgb_dst[0] = r; + rgb_dst[1] = r; + rgb_dst[2] = r; + if CHANNELS == 4 { + rgb_dst[3] = max_value.as_(); + } + } + } + return Ok(()); + } + + 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, + range.range_y, + range.range_uv, + kr_kb.kr, + kr_kb.kb, + PRECISION as u32, + ); + let y_coef = inverse_transform.y_coef; + + let bias_y = range.bias_y as i32; + + let y_iter = y_plane.chunks_exact(y_stride); + let rgb_iter = rgba.chunks_exact_mut(rgba_stride); + + // All branches on generic const will be optimized out. + for (y_src, rgb) in y_iter.zip(rgb_iter) { + let rgb_chunks = rgb.chunks_exact_mut(CHANNELS); + + for (y_src, rgb_dst) in y_src.iter().zip(rgb_chunks) { + let y_value = (y_src.as_() - bias_y) * y_coef; + + let r = qrshr::(y_value); + rgb_dst[0] = r.as_(); + rgb_dst[1] = r.as_(); + rgb_dst[2] = r.as_(); + if CHANNELS == 4 { + rgb_dst[3] = max_value.as_(); + } + } + } + + Ok(()) +} + +/// Converts YUV420 8 bit-depth to Rgba 8 bit +/// +/// # Arguments +/// +/// * `image`: see [YuvPlanarImage] +/// * `rgb`: RGB image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +pub(crate) fn yuv420_to_rgba8( + image: YuvPlanarImage, + rgb: &mut [u8], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv420_to_rgbx::(image, rgb, range, matrix) +} + +/// Converts YUV420 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 yuv420_to_rgba10( + image: YuvPlanarImage, + rgb: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv420_to_rgbx::(image, rgb, range, matrix) +} + +/// Converts YUV420 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 yuv420_to_rgba12( + image: YuvPlanarImage, + rgb: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv420_to_rgbx::(image, rgb, range, matrix) +} + +#[inline] +fn process_halved_chroma_row< + V: Copy + AsPrimitive + 'static + Sized, + const PRECISION: i32, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( + image: YuvPlanarImage, + rgba: &mut [V], + transform: &CbCrInverseTransform, + range: &YuvChromaRange, +) where + i32: AsPrimitive, +{ + let cr_coef = transform.cr_coef; + let cb_coef = transform.cb_coef; + let y_coef = transform.y_coef; + let g_coef_1 = transform.g_coeff_1; + let g_coef_2 = transform.g_coeff_2; + + let max_value = (1 << BIT_DEPTH) - 1; + + let bias_y = range.bias_y as i32; + let bias_uv = range.bias_uv as i32; + let y_iter = image.y_plane.chunks_exact(2); + let rgb_chunks = rgba.chunks_exact_mut(CHANNELS * 2); + for (((y_src, &u_src), &v_src), rgb_dst) in + y_iter.zip(image.u_plane).zip(image.v_plane).zip(rgb_chunks) + { + let y_value: i32 = (y_src[0].as_() - bias_y) * y_coef; + let cb_value: i32 = u_src.as_() - bias_uv; + let cr_value: i32 = v_src.as_() - bias_uv; + + let r = qrshr::(y_value + cr_coef * cr_value); + let b = qrshr::(y_value + cb_coef * cb_value); + let g = qrshr::(y_value - g_coef_1 * cr_value - g_coef_2 * cb_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!(); + } + + let y_value = (y_src[1].as_() - bias_y) * y_coef; + + let r = qrshr::(y_value + cr_coef * cr_value); + let b = qrshr::(y_value + cb_coef * cb_value); + let g = qrshr::(y_value - g_coef_1 * cr_value - g_coef_2 * cb_value); + + 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 = image.y_plane.chunks_exact(2).remainder(); + let rgb_chunks = rgba + .chunks_exact_mut(CHANNELS * 2) + .into_remainder() + .chunks_exact_mut(CHANNELS); + let u_iter = image.u_plane.iter().rev(); + let v_iter = image.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) * y_coef; + let cb_value = u_src.as_() - bias_uv; + let cr_value = v_src.as_() - bias_uv; + + let r = qrshr::(y_value + cr_coef * cr_value); + let b = qrshr::(y_value + cb_coef * cb_value); + let g = + qrshr::(y_value - g_coef_1 * cr_value - g_coef_2 * cb_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!(); + } + } + } +} + +/// Converts YUV420 to Rgba +/// +/// 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] +/// +/// +#[inline] +fn yuv420_to_rgbx< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( + image: YuvPlanarImage, + rgb: &mut [V], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> +where + i32: AsPrimitive, +{ + assert!( + CHANNELS == 3 || CHANNELS == 4, + "YUV 4:2:0 -> 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::() == 2 + } else { + size_of::() == 1 + }, + "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; + let y_stride = image.y_stride; + let u_stride = image.u_stride; + let v_stride = image.v_stride; + let chroma_height = (image.height + 1) / 2; + + check_yuv_plane_preconditions(y_plane, PlaneDefinition::Y, y_stride, image.height)?; + check_yuv_plane_preconditions(u_plane, PlaneDefinition::U, u_stride, chroma_height)?; + check_yuv_plane_preconditions(v_plane, PlaneDefinition::V, v_stride, chroma_height)?; + + 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( + (1 << BIT_DEPTH) - 1, + range.range_y, + range.range_uv, + kr_kb.kr, + kr_kb.kb, + PRECISION as u32, + ); + + let rgb_stride = image.width * CHANNELS; + + let y_iter = y_plane.chunks_exact(y_stride * 2); + let rgb_iter = rgb.chunks_exact_mut(rgb_stride * 2); + let u_iter = u_plane.chunks_exact(u_stride); + let v_iter = v_plane.chunks_exact(v_stride); + + /* + Sample 4x4 YUV420 planar image + start_y + 0: Y00 Y01 Y02 Y03 + start_y + 4: Y04 Y05 Y06 Y07 + start_y + 8: Y08 Y09 Y10 Y11 + start_y + 12: Y12 Y13 Y14 Y15 + start_cb + 0: Cb00 Cb01 + start_cb + 2: Cb02 Cb03 + start_cr + 0: Cr00 Cr01 + start_cr + 2: Cr02 Cr03 + + For 4 luma components (2x2 on rows and cols) there are 1 chroma Cb/Cr components. + Luma channel must have always exact size as RGB target layout, but chroma is not. + + We're sectioning an image by pair of rows, then for each pair of luma and RGB row, + there is one chroma row. + + As chroma is shrunk by factor of 2 then we're processing by pairs of RGB and luma, + for each RGB and luma pair there is one chroma component. + + If image have odd width then luma channel must be exact, and we're replicating last + chroma component. + + If image have odd height then luma channel is exact, and we're replicating last chroma rows. + */ + + // 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) { + // Since we're processing two rows in one loop we need to re-slice once more + let y_iter = y_src.chunks_exact(y_stride); + let rgb_iter = rgb.chunks_exact_mut(rgb_stride); + for (y_src, rgba) in y_iter.zip(rgb_iter) { + let image = YuvPlanarImage { + y_plane: y_src, + y_stride: 0, + u_plane: u_src, + u_stride: 0, + v_plane: v_src, + v_stride: 0, + width: image.width, + height: image.height, + }; + process_halved_chroma_row::( + image, + rgba, + &inverse_transform, + &range, + ); + } + } + + // Process remainder if height is odd + + let y_iter = y_plane + .chunks_exact(y_stride * 2) + .remainder() + .chunks_exact(y_stride); + let rgb_iter = rgb.chunks_exact_mut(rgb_stride).rev(); + let u_iter = u_plane.chunks_exact(u_stride).rev(); + let v_iter = v_plane.chunks_exact(v_stride).rev(); + + for (((y_src, u_src), v_src), rgba) in y_iter.zip(u_iter).zip(v_iter).zip(rgb_iter) { + let image = YuvPlanarImage { + y_plane: y_src, + y_stride: 0, + u_plane: u_src, + u_stride: 0, + v_plane: v_src, + v_stride: 0, + width: image.width, + height: image.height, + }; + process_halved_chroma_row::( + image, + rgba, + &inverse_transform, + &range, + ); + } + + Ok(()) +} + +/// Converts Yuv 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 yuv422_to_rgba8( + image: YuvPlanarImage, + rgb: &mut [u8], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv422_to_rgbx_impl::(image, rgb, range, matrix) +} + +/// Converts Yuv 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 yuv422_to_rgba10( + image: YuvPlanarImage, + rgb: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv422_to_rgbx_impl::(image, rgb, range, matrix) +} + +/// 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 yuv422_to_rgba12( + image: YuvPlanarImage, + rgb: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv422_to_rgbx_impl::(image, rgb, range, matrix) +} + +/// Converts Yuv 422 planar format to Rgba +/// +/// Stride here is not supports u16 as it can be in passed from FFI. +/// +/// # Arguments +/// +/// * `image`: see [YuvPlanarImage] +/// * `rgb`: RGB image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +fn yuv422_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( + image: YuvPlanarImage, + rgb: &mut [V], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> +where + i32: AsPrimitive, +{ + assert!( + CHANNELS == 3 || CHANNELS == 4, + "YUV 4:2:2 -> 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::() == 2 + } else { + size_of::() == 1 + }, + "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; + let y_stride = image.y_stride; + let u_stride = image.u_stride; + let v_stride = image.v_stride; + let width = image.width; + + check_yuv_plane_preconditions(y_plane, PlaneDefinition::Y, y_stride, image.height)?; + check_yuv_plane_preconditions(u_plane, PlaneDefinition::U, u_stride, image.height)?; + check_yuv_plane_preconditions(v_plane, PlaneDefinition::V, v_stride, image.height)?; + + check_rgb_preconditions(rgb, image.width * CHANNELS, image.height)?; + + 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, + range.range_y, + range.range_uv, + kr_kb.kr, + kr_kb.kb, + PRECISION as u32, + ); + + /* + Sample 4x4 YUV422 planar image + start_y + 0: Y00 Y01 Y02 Y03 + start_y + 4: Y04 Y05 Y06 Y07 + start_y + 8: Y08 Y09 Y10 Y11 + start_y + 12: Y12 Y13 Y14 Y15 + start_cb + 0: Cb00 Cb01 + start_cb + 2: Cb02 Cb03 + start_cb + 4: Cb04 Cb05 + start_cb + 6: Cb06 Cb07 + start_cr + 0: Cr00 Cr01 + start_cr + 2: Cr02 Cr03 + start_cr + 4: Cr04 Cr05 + start_cr + 6: Cr06 Cr07 + + For 2 luma components there are 1 chroma Cb/Cr components. + Luma channel must have always exact size as RGB target layout, but chroma is not. + + As chroma is shrunk by factor of 2 then we're processing by pairs of RGB and luma, + for each RGB and luma pair there is one chroma component. + + If image have odd width then luma channel must be exact, and we're replicating last + chroma component. + */ + + let rgb_stride = width * CHANNELS; + + let y_iter = y_plane.chunks_exact(y_stride); + let rgb_iter = rgb.chunks_exact_mut(rgb_stride); + let u_iter = u_plane.chunks_exact(u_stride); + let v_iter = v_plane.chunks_exact(v_stride); + + // All branches on generic const will be optimized out. + for (((y_src, u_src), v_src), rgba) in y_iter.zip(u_iter).zip(v_iter).zip(rgb_iter) { + let image = YuvPlanarImage { + y_plane: y_src, + y_stride: 0, + u_plane: u_src, + u_stride: 0, + v_plane: v_src, + v_stride: 0, + width: image.width, + height: image.height, + }; + process_halved_chroma_row::( + image, + rgba, + &inverse_transform, + &range, + ); + } + + Ok(()) +} + +/// Converts Yuv 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 yuv444_to_rgba8( + image: YuvPlanarImage, + rgba: &mut [u8], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + if matrix == YuvStandardMatrix::Identity { + gbr_to_rgba8(image, rgba, range) + } else { + yuv444_to_rgbx_impl::(image, rgba, range, matrix) + } +} + +/// Converts Yuv 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 yuv444_to_rgba10( + image: YuvPlanarImage, + rgba: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + if matrix == YuvStandardMatrix::Identity { + gbr_to_rgba10(image, rgba, range) + } else { + yuv444_to_rgbx_impl::(image, rgba, range, matrix) + } +} + +/// Converts Yuv 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 yuv444_to_rgba12( + image: YuvPlanarImage, + rgba: &mut [u16], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + if matrix == YuvStandardMatrix::Identity { + gbr_to_rgba12(image, rgba, range) + } else { + yuv444_to_rgbx_impl::(image, rgba, range, matrix) + } +} + +/// Converts Yuv 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] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +#[inline] +fn yuv444_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( + image: YuvPlanarImage, + rgba: &mut [V], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> +where + i32: AsPrimitive, +{ + 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::() == 2 + } else { + size_of::() == 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 = 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, + range.range_y, + range.range_uv, + kr_kb.kr, + kr_kb.kb, + PRECISION as u32, + ); + let cr_coef = inverse_transform.cr_coef; + let cb_coef = inverse_transform.cb_coef; + let y_coef = inverse_transform.y_coef; + let g_coef_1 = inverse_transform.g_coeff_1; + let g_coef_2 = inverse_transform.g_coeff_2; + + let bias_y = range.bias_y as i32; + let bias_uv = range.bias_uv as i32; + + let max_value = (1 << BIT_DEPTH) - 1; + + 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); + + // 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); + + 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 cb_value = u_src.as_() - bias_uv; + let cr_value = v_src.as_() - bias_uv; + + let r = qrshr::(y_value + cr_coef * cr_value); + let b = qrshr::(y_value + cb_coef * cb_value); + let g = + qrshr::(y_value - g_coef_1 * cr_value - g_coef_2 * cb_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 +/// +/// * `image`: see [YuvPlanarImage] +/// * `rgb`: RGB image layout +/// * `range`: see [YuvIntensityRange] +/// +/// +fn gbr_to_rgba8( + image: YuvPlanarImage, + rgb: &mut [u8], + range: YuvIntensityRange, +) -> Result<(), ImageError> { + gbr_to_rgbx_impl::(image, rgb, range) +} + +/// Converts Gbr 10 bit planar format to Rgba 10 bit-depth +/// +/// Stride here is not supported as it can be in passed from FFI. +/// +/// # Arguments +/// +/// * `image`: see [YuvPlanarImage] +/// * `rgba`: RGBx image layout +/// * `range`: see [YuvIntensityRange] +/// +/// +fn gbr_to_rgba10( + image: YuvPlanarImage, + rgba: &mut [u16], + range: YuvIntensityRange, +) -> Result<(), ImageError> { + gbr_to_rgbx_impl::(image, rgba, range) +} + +/// Converts Gbr 12 bit planar format to Rgba 12 bit-depth +/// +/// Stride here is not supported as it can be in passed from FFI. +/// +/// # Arguments +/// +/// * `image`: see [YuvPlanarImage] +/// * `rgba`: RGBx image layout +/// * `range`: see [YuvIntensityRange] +/// +/// +fn gbr_to_rgba12( + image: YuvPlanarImage, + rgba: &mut [u16], + range: YuvIntensityRange, +) -> Result<(), ImageError> { + gbr_to_rgbx_impl::(image, rgba, range) +} + +/// Converts Gbr planar format to Rgba +/// +/// Stride here is not supported as it can be in passed from FFI. +/// +/// # Arguments +/// +/// * `image`: see [YuvPlanarImage] +/// * `rgb`: RGB image layout +/// * `range`: see [YuvIntensityRange] +/// +/// +#[inline] +fn gbr_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( + image: YuvPlanarImage, + rgba: &mut [V], + yuv_range: YuvIntensityRange, +) -> Result<(), ImageError> +where + i32: AsPrimitive, +{ + assert!( + CHANNELS == 3 || CHANNELS == 4, + "GBR -> 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::() == 2 + } else { + size_of::() == 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, width * CHANNELS, height)?; + + let max_value = (1 << BIT_DEPTH) - 1; + + 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); + + match yuv_range { + YuvIntensityRange::Tv => { + const PRECISION: i32 = 11; + // All channels on identity should use Y range + let range = yuv_range.get_yuv_range(BIT_DEPTH as u32); + let range_rgba = (1 << BIT_DEPTH) - 1; + let y_coef = + ((range_rgba as f32 / range.range_y as f32) * (1 << PRECISION) as f32) as i32; + let y_bias = range.bias_y as i32; + + 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); + + for (((&y_src, &u_src), &v_src), rgb_dst) in + y_src.iter().zip(u_src).zip(v_src).zip(rgb_chunks) + { + rgb_dst[0] = + qrshr::((v_src.as_() - y_bias) * y_coef).as_(); + rgb_dst[1] = + qrshr::((y_src.as_() - y_bias) * y_coef).as_(); + rgb_dst[2] = + qrshr::((u_src.as_() - y_bias) * y_coef).as_(); + if CHANNELS == 4 { + rgb_dst[3] = max_value.as_(); + } + } + } + } + YuvIntensityRange::Pc => { + 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); + + for (((&y_src, &u_src), &v_src), rgb_dst) in + y_src.iter().zip(u_src).zip(v_src).zip(rgb_chunks) + { + rgb_dst[0] = v_src; + rgb_dst[1] = y_src; + rgb_dst[2] = u_src; + if CHANNELS == 4 { + rgb_dst[3] = max_value.as_(); + } + } + } + } + } + + Ok(()) +}