diff --git a/src/codecs/avif/decoder.rs b/src/codecs/avif/decoder.rs index bafdb99a05..1e23dcebfa 100644 --- a/src/codecs/avif/decoder.rs +++ b/src/codecs/avif/decoder.rs @@ -4,10 +4,14 @@ /// /// [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::error::{ + DecodingError, ImageFormatHint, LimitError, LimitErrorKind, UnsupportedError, + UnsupportedErrorKind, +}; use crate::{ColorType, ImageDecoder, ImageError, ImageFormat, ImageResult}; use crate::codecs::avif::yuv::*; @@ -28,6 +32,43 @@ pub struct AvifDecoder { icc_profile: Option>, } +#[derive(Debug, Clone, PartialEq, Eq)] +enum AvifDecoderError { + AlphaPlaneFormat(PixelLayout), + MemoryLayout, + 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::MemoryLayout => { + f.write_str("Unexpected data size for current RGBx layout") + } + 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 { @@ -90,6 +131,100 @@ fn reshape_plane(source: &[u8], stride: usize, width: usize, height: usize) -> V target_plane } +struct Plane16View<'a> { + data: std::borrow::Cow<'a, [u16]>, + stride: usize, +} + +/// 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, @@ -166,6 +301,60 @@ fn get_matrix( } } +fn check_target_rgba_dimension_preconditions( + width: usize, + height: usize, +) -> Result<(), ImageError> { + // This is suspicious if this happens, better fail early + if width == 0 || height == 0 { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); + } + // Image dimensions must not exceed pointer size + let (v_stride, ow) = width.overflowing_mul(4); + if ow { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::InsufficientMemory, + ))); + } + let (_, ow) = v_stride.overflowing_mul(height); + if ow { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::InsufficientMemory, + ))); + } + Ok(()) +} + +fn check_plane_dimension_preconditions( + width: usize, + height: usize, + target_width: usize, + target_height: usize, +) -> Result<(), ImageError> { + // This is suspicious if this happens, better fail early + if width == 0 || height == 0 { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); + } + // Plane dimensions must not exceed pointer size + let (_, ow) = width.overflowing_mul(height); + if ow { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::InsufficientMemory, + ))); + } + // This should never happen that plane size differs from target size + if target_width != width || target_height != height { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); + } + Ok(()) +} + impl ImageDecoder for AvifDecoder { fn dimensions(&self) -> (u32, u32) { (self.picture.width(), self.picture.height()) @@ -186,17 +375,14 @@ impl ImageDecoder for AvifDecoder { fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - let (width, height) = self.dimensions(); + let bit_depth = self.picture.bit_depth(); - // This is suspicious if this happens, better fail early - if width == 0 || height == 0 { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature("Invalid image dimensions".to_string()), - ), - )); - } + // 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(); + check_target_rgba_dimension_preconditions(width as usize, height as usize)?; let yuv_range = match self.picture.color_range() { dav1d::pixel::YUVRange::Limited => YuvIntensityRange::Tv, @@ -208,101 +394,75 @@ impl ImageDecoder for AvifDecoder { let color_matrix = get_matrix(self.picture.matrix_coefficients())?; - // Identity matrix should be possible only on 444 + // Identity matrix should be possible only on 4:4:4 if is_identity && self.picture.pixel_layout() != PixelLayout::I444 { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature( - "Impossible YUV layout for Identity matrix".to_string(), - ), - ), - )); + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Avif.into(), + AvifDecoderError::YuvLayoutOnIdentityMatrix(self.picture.pixel_layout()), + ))); } - if self.picture.bit_depth() == 8 { + if bit_depth == 8 { if self.picture.pixel_layout() != PixelLayout::I400 { 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::new( - ref_y.as_ref(), - self.picture.stride(PlanarImageComponent::Y) as usize, - ref_u.as_ref(), - self.picture.stride(PlanarImageComponent::U) as usize, - ref_v.as_ref(), - self.picture.stride(PlanarImageComponent::V) as usize, - width as usize, - height as usize, - ); + 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, + }; if !is_identity { let worker = match self.picture.pixel_layout() { PixelLayout::I400 => unreachable!(), - PixelLayout::I420 => yuv420_to_rgba, - PixelLayout::I422 => yuv422_to_rgba, - PixelLayout::I444 => yuv444_to_rgba, + PixelLayout::I420 => yuv420_to_rgba8, + PixelLayout::I422 => yuv422_to_rgba8, + PixelLayout::I444 => yuv444_to_rgba8, }; - let res = worker(image, buf, 8, yuv_range, color_matrix); - - if let Err(err) = res { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature(err), - ), - )); - } + worker(image, buf, yuv_range, color_matrix)?; } else { - let res = gbr_to_rgba(image, buf, 8); - - if let Err(err) = res { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature(err), - ), - )); - } + gbr_to_rgba8(image, buf)?; } } else { let plane = self.picture.plane(PlanarImageComponent::Y); - let gray_image = YuvGrayImage::new( - plane.as_ref(), - self.picture.stride(PlanarImageComponent::Y) as usize, - width as usize, - height as usize, - ); + let gray_image = YuvGrayImage { + y_plane: plane.as_ref(), + y_stride: self.picture.stride(PlanarImageComponent::Y) as usize, + width: width as usize, + height: height as usize, + }; - let cr = yuv400_to_rgba(gray_image, buf, 8, yuv_range, color_matrix); - if let Err(err) = cr { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature(err), - ), - )); - } + yuv400_to_rgba8(gray_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::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()), + ))); } + + check_plane_dimension_preconditions( + picture.width() as usize, + picture.height() as usize, + width as usize, + height as usize, + )?; + let stride = picture.stride(PlanarImageComponent::Y) as usize; let plane = picture.plane(PlanarImageComponent::Y); - let width = picture.width(); + for (buf, slice) in Iterator::zip( buf.chunks_exact_mut(width as usize * 4), plane.as_ref().chunks_exact(stride), @@ -317,14 +477,10 @@ impl ImageDecoder for AvifDecoder { let rgba16_buf: &mut [u16] = match bytemuck::try_cast_slice_mut(buf) { Ok(slice) => slice, Err(_) => { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature( - "Incorrectly determined image type".to_string(), - ), - ), - )); + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Avif.into(), + AvifDecoderError::MemoryLayout, + ))); } }; @@ -333,227 +489,122 @@ impl ImageDecoder for AvifDecoder { // 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 mut y_plane_stride = self.picture.stride(PlanarImageComponent::Y) >> 1; + let y_dav1d_plane = self.picture.plane(PlanarImageComponent::Y); - let ref_y = self.picture.plane(PlanarImageComponent::Y); - let mut _bind_y = vec![]; + let y_plane_view = transmute_y_plane16( + &y_dav1d_plane, + self.picture.stride(PlanarImageComponent::Y) as usize, + width as usize, + height as usize, + ); + + if self.picture.pixel_layout() != PixelLayout::I400 { + let u_dav1d_plane = self.picture.plane(PlanarImageComponent::U); + let v_dav1d_plane = self.picture.plane(PlanarImageComponent::V); - let mut shape_y_plane = || { - y_plane_stride = width; - _bind_y = reshape_plane( - ref_y.as_ref(), - self.picture.stride(PlanarImageComponent::Y) as usize, + let 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, ); - }; - - let y_plane: &[u16] = if self.picture.stride(PlanarImageComponent::Y) as usize & 1 == 0 - { - match bytemuck::try_cast_slice(ref_y.as_ref()) { - Ok(slice) => slice, - Err(_) => { - shape_y_plane(); - _bind_y.as_slice() - } - } - } else { - shape_y_plane(); - _bind_y.as_slice() - }; - - if self.picture.pixel_layout() != PixelLayout::I400 { - let mut u_plane_stride = self.picture.stride(PlanarImageComponent::U) >> 1; - - let ref_u = self.picture.plane(PlanarImageComponent::U); - let mut _bind_u = vec![]; - let ref_v = self.picture.plane(PlanarImageComponent::V); - let mut _bind_v = vec![]; - - let mut shape_u_plane = || { - u_plane_stride = match self.picture.pixel_layout() { - PixelLayout::I400 => unreachable!(), - PixelLayout::I420 | PixelLayout::I422 => (width + 1) / 2, - PixelLayout::I444 => width, - }; - let u_plane_height = match self.picture.pixel_layout() { - PixelLayout::I400 => unreachable!(), - PixelLayout::I420 => (height + 1) / 2, - PixelLayout::I422 | PixelLayout::I444 => height, - }; - _bind_u = reshape_plane( - ref_u.as_ref(), - self.picture.stride(PlanarImageComponent::U) as usize, - u_plane_stride as usize, - u_plane_height as usize, - ); - }; - - let u_plane: &[u16] = - if self.picture.stride(PlanarImageComponent::U) as usize & 1 == 0 { - match bytemuck::try_cast_slice(ref_u.as_ref()) { - Ok(slice) => slice, - Err(_) => { - shape_u_plane(); - _bind_u.as_slice() - } - } - } else { - shape_u_plane(); - _bind_u.as_slice() - }; - - let mut v_plane_stride = self.picture.stride(PlanarImageComponent::V) >> 1; - - let mut shape_v_plane = || { - v_plane_stride = match self.picture.pixel_layout() { - PixelLayout::I400 => unreachable!(), - PixelLayout::I420 | PixelLayout::I422 => (width + 1) / 2, - PixelLayout::I444 => width, - }; - let v_plane_height = match self.picture.pixel_layout() { - PixelLayout::I400 => unreachable!(), - PixelLayout::I420 => (height + 1) / 2, - PixelLayout::I422 | PixelLayout::I444 => height, - }; - _bind_v = reshape_plane( - ref_v.as_ref(), - self.picture.stride(PlanarImageComponent::V) as usize, - v_plane_stride as usize, - v_plane_height as usize, - ); - }; - - let v_plane: &[u16] = - if self.picture.stride(PlanarImageComponent::V) as usize & 1 == 0 { - match bytemuck::try_cast_slice(ref_v.as_ref()) { - Ok(slice) => slice, - Err(_) => { - shape_v_plane(); - _bind_v.as_slice() - } - } - } else { - shape_v_plane(); - _bind_v.as_slice() - }; - - let image = YuvPlanarImage::new( - y_plane, - y_plane_stride as usize, - u_plane, - u_plane_stride as usize, - v_plane, - v_plane_stride as usize, + let v_plane_view = transmute_chroma_plane16( + &v_dav1d_plane, + self.picture.pixel_layout(), + self.picture.stride(PlanarImageComponent::V) as usize, width as usize, height as usize, ); + 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, + }; + if !is_identity { let worker = match self.picture.pixel_layout() { PixelLayout::I400 => unreachable!(), - PixelLayout::I420 => yuv420_to_rgba, - PixelLayout::I422 => yuv422_to_rgba, - PixelLayout::I444 => yuv444_to_rgba, + 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 + } + } }; - let res = worker( - image, - rgba16_buf, - self.picture.bit_depth() as u32, - yuv_range, - color_matrix, - ); - - if let Err(err) = res { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature(err), - ), - )); - } + worker(image, rgba16_buf, yuv_range, color_matrix)?; } else { - let res = gbr_to_rgba(image, rgba16_buf, self.picture.bit_depth() as u32); - - if let Err(err) = res { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature(err), - ), - )); - } + let worker = if bit_depth == 10 { + gbr_to_rgba10 + } else { + gbr_to_rgba12 + }; + worker(image, rgba16_buf)?; } } else { - let gray_image = YuvGrayImage::new( - y_plane, - y_plane_stride as usize, - width as usize, - height as usize, - ); - let cr = yuv400_to_rgba( - gray_image, - rgba16_buf, - self.picture.bit_depth() as u32, - yuv_range, - color_matrix, - ); - if let Err(err) = cr { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Avif.into(), - UnsupportedErrorKind::GenericFeature(err), - ), - )); - } + let gray_image = YuvGrayImage { + y_plane: y_plane_view.data.as_ref(), + y_stride: y_plane_view.stride, + width: width as usize, + height: height as usize, + }; + let worker = if bit_depth == 10 { + yuv400_to_rgba10 + } else { + yuv400_to_rgba12 + }; + worker(gray_image, rgba16_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::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 ref_a = picture.plane(PlanarImageComponent::Y); - let mut _bind_a = vec![]; - - let mut a_plane_stride = picture.stride(PlanarImageComponent::Y) >> 1; - - let mut shape_a_plane = || { - a_plane_stride = width; - _bind_a = reshape_plane( - ref_a.as_ref(), - picture.stride(PlanarImageComponent::Y) as usize, - width as usize, - height as usize, - ); - }; - let a_plane: &[u16] = if picture.stride(PlanarImageComponent::Y) as usize & 1 == 0 { - match bytemuck::try_cast_slice(ref_y.as_ref()) { - Ok(slice) => slice, - Err(_) => { - shape_a_plane(); - _bind_a.as_slice() - } - } - } else { - shape_a_plane(); - _bind_a.as_slice() - }; + check_plane_dimension_preconditions( + picture.width() as usize, + picture.height() as usize, + width as usize, + height as usize, + )?; + + 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, + ); - let width = picture.width(); for (buf, slice) in Iterator::zip( rgba16_buf.chunks_exact_mut(width as usize * 4), - a_plane.chunks_exact(a_plane_stride as usize), + a_plane_view.data.as_ref().chunks_exact(a_plane_view.stride), ) { for (rgba, a_src) in buf.chunks_exact_mut(4).zip(slice) { rgba[3] = *a_src; @@ -564,11 +615,8 @@ impl ImageDecoder for AvifDecoder { // Expand current bit depth to target 16 let target_expand_bits = 16u32.saturating_sub(self.picture.bit_depth() as u32); if target_expand_bits > 0 { - for rgba in rgba16_buf.chunks_exact_mut(4) { - rgba[0] <<= target_expand_bits; - rgba[1] <<= target_expand_bits; - rgba[2] <<= target_expand_bits; - rgba[3] <<= target_expand_bits; + for item in rgba16_buf.iter_mut() { + *item <<= target_expand_bits; } } } diff --git a/src/codecs/avif/yuv.rs b/src/codecs/avif/yuv.rs index 9d20b5f130..4f1f386c8b 100644 --- a/src/codecs/avif/yuv.rs +++ b/src/codecs/avif/yuv.rs @@ -1,4 +1,7 @@ +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 @@ -10,24 +13,6 @@ struct CbCrInverseTransform { pub g_coeff_2: T, } -impl CbCrInverseTransform { - fn new( - y_coef: T, - cr_coef: T, - cb_coef: T, - g_coeff_1: T, - g_coeff_2: T, - ) -> CbCrInverseTransform { - CbCrInverseTransform { - y_coef, - cr_coef, - cb_coef, - g_coeff_1, - g_coeff_2, - } - } -} - impl CbCrInverseTransform { fn to_integers(self, precision: u32) -> CbCrInverseTransform { let precision_scale: i32 = 1i32 << (precision as i32); @@ -46,7 +31,97 @@ impl CbCrInverseTransform { } } -/// Transformation RGB to YUV with coefficients as specified in [ITU-R](https://www.itu.int/rec/T-REC-H.273/en) +#[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, @@ -54,25 +129,27 @@ fn get_inverse_transform( kr: f32, kb: f32, precision: u32, -) -> Result, String> { +) -> 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; - if kg == 0f32 { - return Err("1.0f - kr - kg must not be 0".parse().unwrap()); - } + 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::new(y_coef, cr_coeff, cb_coeff, g_coeff_1, g_coeff_2); - Ok(exact_transform.to_integers(precision)) + 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) } -#[repr(C)] #[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -/// Declares YUV range TV (limited) or Full, +/// 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)], @@ -92,26 +169,27 @@ struct YuvChromaRange { pub range: YuvIntensityRange, } -const fn get_yuv_range(depth: u32, range: YuvIntensityRange) -> YuvChromaRange { - match range { - YuvIntensityRange::Tv => YuvChromaRange { - bias_y: 16 << (depth - 8), - bias_uv: 1 << (depth - 1), - range_y: 219 << (depth - 8), - range_uv: 224 << (depth - 8), - range, - }, - YuvIntensityRange::Pc => YuvChromaRange { - bias_y: 0, - bias_uv: 1 << (depth - 1), - range_uv: (1 << depth) - 1, - range_y: (1 << depth) - 1, - range, - }, +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, + }, + } } } -#[repr(C)] #[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 @@ -125,92 +203,119 @@ pub(crate) enum YuvStandardMatrix { #[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] struct YuvBias { - pub kr: f32, - pub kb: f32, + kr: f32, + kb: f32, } -const fn get_kr_kb(matrix: YuvStandardMatrix) -> YuvBias { - match matrix { - 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, - }, +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, + }, + } } } pub(crate) struct YuvPlanarImage<'a, T> { - y_plane: &'a [T], - y_stride: usize, - u_plane: &'a [T], - u_stride: usize, - v_plane: &'a [T], - v_stride: usize, - width: usize, - height: usize, + 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, } -impl<'a, T> YuvPlanarImage<'a, T> { - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - y_plane: &'a [T], - y_stride: usize, - u_plane: &'a [T], - u_stride: usize, - v_plane: &'a [T], - v_stride: usize, - width: usize, - height: usize, - ) -> Self { - YuvPlanarImage { - y_plane, - y_stride, - u_plane, - u_stride, - v_plane, - v_stride, - width, - height, - } - } +pub(crate) struct YuvGrayImage<'a, T> { + pub(crate) y_plane: &'a [T], + pub(crate) y_stride: usize, + pub(crate) width: usize, + pub(crate) height: usize, } -pub(crate) struct YuvGrayImage<'a, T> { - y_plane: &'a [T], - y_stride: usize, - width: usize, - height: usize, +/// 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: YuvGrayImage, + rgba: &mut [u8], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + yuv400_to_rgbx_impl::(image, rgba, range, matrix) } -impl<'a, T> YuvGrayImage<'a, T> { - pub(crate) fn new(y_plane: &'a [T], y_stride: usize, width: usize, height: usize) -> Self { - YuvGrayImage { - y_plane, - y_stride, - width, - height, - } - } +/// 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: YuvGrayImage, + 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: YuvGrayImage, + 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 supports u16 as it can be in passed from FFI. +/// Stride here is not supported as it can be in passed from FFI. /// /// # Arguments /// @@ -220,45 +325,55 @@ impl<'a, T> YuvGrayImage<'a, T> { /// * `matrix`: see [YuvStandardMatrix] /// /// -pub(crate) fn yuv400_to_rgba + 'static>( +#[inline] +fn yuv400_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( image: YuvGrayImage, rgba: &mut [V], - bit_depth: u32, range: YuvIntensityRange, matrix: YuvStandardMatrix, -) -> Result<(), String> +) -> 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" + ); + let y_plane = image.y_plane; let y_stride = image.y_stride; let height = image.height; let width = image.width; - if y_plane.len() != y_stride * height { - return Err(format!( - "Luma plane expected {} bytes, got {}", - y_stride * height, - y_plane.len() - )); - } + check_yuv_plane_preconditions(y_plane, PlaneDefinition::Y, y_stride, height)?; + check_rgb_preconditions(rgba, width * CHANNELS, height)?; - if !(8..=16).contains(&bit_depth) { - return Err(format!( - "Unexpected bit depth value {}, only 8...16 is supported", - bit_depth - )); - } - const CHANNELS: usize = 4; let rgba_stride = width * CHANNELS; - let max_value = (1 << bit_depth) - 1; + 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); @@ -267,39 +382,34 @@ where rgb_dst[0] = r; rgb_dst[1] = r; rgb_dst[2] = r; - rgb_dst[3] = max_value.as_(); + if CHANNELS == 4 { + rgb_dst[3] = max_value.as_(); + } } } return Ok(()); } - let range = get_yuv_range(bit_depth, range); - let kr_kb = get_kr_kb(matrix); + let range = range.get_yuv_range(BIT_DEPTH as u32); + let kr_kb = matrix.get_kr_kb(); const PRECISION: i32 = 11; const ROUNDING: i32 = 1 << (PRECISION - 1); let inverse_transform = get_inverse_transform( - (1 << bit_depth) - 1, + (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; - if rgba.len() != width * height * CHANNELS { - return Err(format!( - "RGB image layout expected {} bytes, got {}", - width * height * CHANNELS, - rgba.len() - )); - } - 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); @@ -310,16 +420,37 @@ where rgb_dst[0] = r.as_(); rgb_dst[1] = r.as_(); rgb_dst[2] = r.as_(); - rgb_dst[3] = max_value.as_(); + if CHANNELS == 4 { + rgb_dst[3] = max_value.as_(); + } } } Ok(()) } -/// Converts YUV420 to Rgb +/// Converts YUV420 8 bit-depth to Rgba 8 bit +/// +/// # Arguments +/// +/// * `image`: see [YuvPlanarImage] +/// * `rgb`: RGB image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] /// -/// Stride here is not supports u16 as it can be in passed from FFI. +/// +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 /// @@ -329,16 +460,78 @@ where /// * `matrix`: see [YuvStandardMatrix] /// /// -pub(crate) fn yuv420_to_rgba + 'static>( +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) +} + +/// 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], - bit_depth: u32, range: YuvIntensityRange, matrix: YuvStandardMatrix, -) -> Result<(), String> +) -> 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" + ); let y_plane = image.y_plane; let u_plane = image.u_plane; let v_plane = image.v_plane; @@ -347,52 +540,27 @@ where let v_stride = image.v_stride; let chroma_height = (image.height + 1) / 2; - if y_plane.len() != y_stride * image.height { - return Err(format!( - "Luma plane expected {} bytes, got {}", - y_stride * image.height, - y_plane.len() - )); - } - - if u_plane.len() != u_stride * chroma_height { - return Err(format!( - "U plane expected {} bytes, got {}", - u_stride * chroma_height, - u_plane.len() - )); - } + 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)?; - if v_plane.len() != v_stride * chroma_height { - return Err(format!( - "V plane expected {} bytes, got {}", - v_stride * chroma_height, - v_plane.len() - )); - } + check_rgb_preconditions(rgb, image.width * CHANNELS, image.height)?; - if !(8..=16).contains(&bit_depth) { - return Err(format!( - "Unexpected bit depth value {}, only 8...16 is supported", - bit_depth - )); - } - - let max_value = (1 << bit_depth) - 1; + let max_value = (1 << BIT_DEPTH) - 1; const PRECISION: i32 = 11; const ROUNDING: i32 = 1 << (PRECISION - 1); - let range = get_yuv_range(bit_depth, range); - let kr_kb = get_kr_kb(matrix); + 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, + (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; @@ -402,16 +570,6 @@ where let bias_y = range.bias_y as i32; let bias_uv = range.bias_uv as i32; - const CHANNELS: usize = 4; - - if rgb.len() != image.width * image.height * CHANNELS { - return Err(format!( - "RGB image layout expected {} bytes, got {}", - image.width * image.height * CHANNELS, - rgb.len() - )); - } - let rgb_stride = image.width * CHANNELS; let y_iter = y_plane.chunks_exact(y_stride * 2); @@ -445,6 +603,7 @@ where 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); @@ -466,10 +625,18 @@ where >> PRECISION) .clamp(0, max_value); - rgb_dst[0] = r.as_(); - rgb_dst[1] = g.as_(); - rgb_dst[2] = b.as_(); - rgb_dst[3] = max_value.as_(); + 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; @@ -481,10 +648,18 @@ where >> PRECISION) .clamp(0, max_value); - rgb_dst[4] = r.as_(); - rgb_dst[5] = g.as_(); - rgb_dst[6] = b.as_(); - rgb_dst[7] = max_value.as_(); + 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. @@ -512,10 +687,18 @@ where >> PRECISION) .clamp(0, max_value); - rgb_dst[0] = r.as_(); - rgb_dst[1] = g.as_(); - rgb_dst[2] = b.as_(); - rgb_dst[3] = max_value.as_(); + 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!(); + } } } } @@ -544,10 +727,18 @@ where let g = ((y_value - g_coef_1 * cr_value - g_coef_2 * cb_value + ROUNDING) >> PRECISION) .clamp(0, max_value); - rgb_dst[0] = r.as_(); - rgb_dst[1] = g.as_(); - rgb_dst[2] = b.as_(); - rgb_dst[3] = max_value.as_(); + 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; @@ -556,10 +747,18 @@ where let g = ((y_value - g_coef_1 * cr_value - g_coef_2 * cb_value + ROUNDING) >> PRECISION) .clamp(0, max_value); - rgb_dst[4] = r.as_(); - rgb_dst[5] = g.as_(); - rgb_dst[6] = b.as_(); - rgb_dst[7] = max_value.as_(); + 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. @@ -588,10 +787,18 @@ where >> PRECISION) .clamp(0, max_value); - rgb_dst[0] = r.as_(); - rgb_dst[1] = g.as_(); - rgb_dst[2] = b.as_(); - rgb_dst[3] = max_value.as_(); + 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!(); + } } } } @@ -599,6 +806,67 @@ where 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. @@ -611,70 +879,63 @@ where /// * `matrix`: see [YuvStandardMatrix] /// /// -pub(crate) fn yuv422_to_rgba + 'static>( +fn yuv422_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( image: YuvPlanarImage, rgb: &mut [V], - bit_depth: u32, range: YuvIntensityRange, matrix: YuvStandardMatrix, -) -> Result<(), String> +) -> 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" + ); 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; - if y_plane.len() != y_stride * height { - return Err(format!( - "Luma plane expected {} bytes, got {}", - y_stride * height, - y_plane.len() - )); - } + 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)?; - if u_plane.len() != u_stride * height { - return Err(format!( - "U plane expected {} bytes, got {}", - u_stride * height, - u_plane.len() - )); - } + check_rgb_preconditions(rgb, image.width * CHANNELS, image.height)?; - if v_plane.len() != v_stride * height { - return Err(format!( - "V plane expected {} bytes, got {}", - v_stride * height, - v_plane.len() - )); - } + let max_value = (1 << BIT_DEPTH) - 1; - if !(8..=16).contains(&bit_depth) { - return Err(format!( - "Unexpected bit depth value {}, only 8...16 is supported", - bit_depth - )); - } - - let max_value = (1 << bit_depth) - 1; - - let range = get_yuv_range(bit_depth, range); - let kr_kb = get_kr_kb(matrix); + let range = range.get_yuv_range(BIT_DEPTH as u32); + let kr_kb = matrix.get_kr_kb(); const PRECISION: i32 = 11; const ROUNDING: i32 = 1 << (PRECISION - 1); let inverse_transform = get_inverse_transform( - (1 << bit_depth) - 1, + (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; @@ -684,16 +945,6 @@ where let bias_y = range.bias_y as i32; let bias_uv = range.bias_uv as i32; - const CHANNELS: usize = 4; - - if rgb.len() != width * height * CHANNELS { - return Err(format!( - "RGB image layout expected {} bytes, got {}", - width * height * CHANNELS, - rgb.len() - )); - } - /* Sample 4x4 YUV422 planar image start_y + 0: Y00 Y01 Y02 Y03 @@ -726,6 +977,7 @@ where 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 y_iter = y_src.chunks_exact(2); let rgb_chunks = rgb.chunks_exact_mut(CHANNELS * 2); @@ -740,10 +992,18 @@ where let g = ((y_value - g_coef_1 * cr_value - g_coef_2 * cb_value + ROUNDING) >> PRECISION) .clamp(0, max_value); - rgb_dst[0] = r.as_(); - rgb_dst[1] = g.as_(); - rgb_dst[2] = b.as_(); - rgb_dst[3] = max_value.as_(); + 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; @@ -752,10 +1012,18 @@ where let g = ((y_value - g_coef_1 * cr_value - g_coef_2 * cb_value + ROUNDING) >> PRECISION) .clamp(0, max_value); - rgb_dst[4] = r.as_(); - rgb_dst[5] = g.as_(); - rgb_dst[6] = b.as_(); - rgb_dst[7] = max_value.as_(); + 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 left pixels for odd images, this should work since luma must be always exact @@ -783,10 +1051,18 @@ where >> PRECISION) .clamp(0, max_value); - rgb_dst[0] = r.as_(); - rgb_dst[1] = g.as_(); - rgb_dst[2] = b.as_(); - rgb_dst[3] = max_value.as_(); + 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!(); + } } } } @@ -794,6 +1070,67 @@ where 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(super) fn yuv444_to_rgba8( + image: YuvPlanarImage, + rgba: &mut [u8], + range: YuvIntensityRange, + matrix: YuvStandardMatrix, +) -> Result<(), ImageError> { + 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> { + 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> { + 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. @@ -801,21 +1138,42 @@ where /// # Arguments /// /// * `image`: see [YuvPlanarImage] -/// * `rgb`: RGB image layout +/// * `rgba`: RGB image layout /// * `range`: see [YuvIntensityRange] /// * `matrix`: see [YuvStandardMatrix] /// /// -pub(crate) fn yuv444_to_rgba + 'static>( +#[inline] +fn yuv444_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( image: YuvPlanarImage, - rgb: &mut [V], - bit_depth: u32, + rgba: &mut [V], range: YuvIntensityRange, matrix: YuvStandardMatrix, -) -> Result<(), String> +) -> 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; @@ -825,49 +1183,24 @@ where let height = image.height; let width = image.width; - if y_plane.len() != y_stride * height { - return Err(format!( - "Luma plane expected {} bytes, got {}", - y_stride * height, - y_plane.len() - )); - } - - if u_plane.len() != u_stride * height { - return Err(format!( - "U plane expected {} bytes, got {}", - u_stride * height, - u_plane.len() - )); - } + 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)?; - if v_plane.len() != v_stride * height { - return Err(format!( - "V plane expected {} bytes, got {}", - v_stride * height, - v_plane.len() - )); - } - - if !(8..=16).contains(&bit_depth) { - return Err(format!( - "Unexpected bit depth value {}, only 8...16 is supported", - bit_depth - )); - } + check_rgb_preconditions(rgba, image.width * CHANNELS, height)?; - let range = get_yuv_range(bit_depth, range); - let kr_kb = get_kr_kb(matrix); + let range = range.get_yuv_range(BIT_DEPTH as u32); + let kr_kb = matrix.get_kr_kb(); const PRECISION: i32 = 11; const ROUNDING: i32 = 1 << (PRECISION - 1); let inverse_transform = get_inverse_transform( - (1 << bit_depth) - 1, + (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; @@ -877,25 +1210,16 @@ where let bias_y = range.bias_y as i32; let bias_uv = range.bias_uv as i32; - const CHANNELS: usize = 4; - - if rgb.len() != width * height * CHANNELS { - return Err(format!( - "RGB image layout expected {} bytes, got {}", - width * height * CHANNELS, - rgb.len() - )); - } - - let max_value = (1 << bit_depth) - 1; + let max_value = (1 << BIT_DEPTH) - 1; let rgb_stride = width * CHANNELS; let y_iter = y_plane.chunks_exact(y_stride); - let rgb_iter = rgb.chunks_exact_mut(rgb_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); @@ -910,19 +1234,73 @@ where let g = ((y_value - g_coef_1 * cr_value - g_coef_2 * cb_value + ROUNDING) >> PRECISION) .clamp(0, max_value); - rgb_dst[0] = r.as_(); - rgb_dst[1] = g.as_(); - rgb_dst[2] = b.as_(); - rgb_dst[3] = max_value.as_(); + 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] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +pub(crate) fn gbr_to_rgba8(image: YuvPlanarImage, rgb: &mut [u8]) -> Result<(), ImageError> { + gbr_to_rgbx_impl::(image, rgb) +} + +/// 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] +/// * `rgb`: RGB image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +pub(crate) fn gbr_to_rgba10(image: YuvPlanarImage, rgb: &mut [u16]) -> Result<(), ImageError> { + gbr_to_rgbx_impl::(image, rgb) +} + +/// 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] +/// * `rgb`: RGB image layout +/// * `range`: see [YuvIntensityRange] +/// * `matrix`: see [YuvStandardMatrix] +/// +/// +pub(crate) fn gbr_to_rgba12(image: YuvPlanarImage, rgb: &mut [u16]) -> Result<(), ImageError> { + gbr_to_rgbx_impl::(image, rgb) +} + /// Converts Gbr planar format to Rgba /// -/// Stride here is not supports u16 as it can be in passed from FFI. +/// Stride here is not supported as it can be in passed from FFI. /// /// # Arguments /// @@ -932,14 +1310,34 @@ where /// * `matrix`: see [YuvStandardMatrix] /// /// -pub(crate) fn gbr_to_rgba + 'static>( +#[inline] +fn gbr_to_rgbx_impl< + V: Copy + AsPrimitive + 'static + Sized, + const CHANNELS: usize, + const BIT_DEPTH: usize, +>( image: YuvPlanarImage, rgb: &mut [V], - bit_depth: u32, -) -> Result<(), String> +) -> 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; @@ -949,48 +1347,13 @@ where let height = image.height; let width = image.width; - if y_plane.len() != y_stride * height { - return Err(format!( - "Luma plane expected {} bytes, got {}", - y_stride * height, - y_plane.len() - )); - } - - if u_plane.len() != u_stride * height { - return Err(format!( - "U plane expected {} bytes, got {}", - u_stride * height, - u_plane.len() - )); - } - - if v_plane.len() != v_stride * height { - return Err(format!( - "V plane expected {} bytes, got {}", - v_stride * height, - v_plane.len() - )); - } - - if !(8..=16).contains(&bit_depth) { - return Err(format!( - "Unexpected bit depth value {}, only 8...16 is supported", - bit_depth - )); - } - - const CHANNELS: usize = 4; + 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)?; - if rgb.len() != width * height * CHANNELS { - return Err(format!( - "RGB image layout expected {} bytes, got {}", - width * height * CHANNELS, - rgb.len() - )); - } + check_rgb_preconditions(rgb, width * CHANNELS, height)?; - let max_value = (1 << bit_depth) - 1; + let max_value = (1 << BIT_DEPTH) - 1; let rgb_stride = width * CHANNELS; @@ -1008,7 +1371,9 @@ where rgb_dst[0] = v_src; rgb_dst[1] = y_src; rgb_dst[2] = u_src; - rgb_dst[3] = max_value.as_(); + if CHANNELS == 4 { + rgb_dst[3] = max_value.as_(); + } } }