| // Copyright 2024 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| use super::libyuv; |
| use super::rgb_impl; |
| |
| use crate::image::Plane; |
| use crate::image::YuvRange; |
| use crate::internal_utils::pixels::*; |
| use crate::internal_utils::*; |
| use crate::*; |
| |
| #[repr(C)] |
| #[derive(Clone, Copy, Default, PartialEq)] |
| pub enum Format { |
| Rgb, |
| #[default] |
| Rgba, |
| Argb, |
| Bgr, |
| Bgra, |
| Abgr, |
| Rgb565, |
| Rgba1010102, // https://developer.android.com/reference/android/graphics/Bitmap.Config#RGBA_1010102 |
| } |
| |
| impl Format { |
| pub(crate) fn offsets(&self) -> [usize; 4] { |
| match self { |
| Format::Rgb => [0, 1, 2, 0], |
| Format::Rgba => [0, 1, 2, 3], |
| Format::Argb => [1, 2, 3, 0], |
| Format::Bgr => [2, 1, 0, 0], |
| Format::Bgra => [2, 1, 0, 3], |
| Format::Abgr => [3, 2, 1, 0], |
| Format::Rgb565 | Format::Rgba1010102 => [0; 4], |
| } |
| } |
| |
| pub fn r_offset(&self) -> usize { |
| self.offsets()[0] |
| } |
| |
| pub fn g_offset(&self) -> usize { |
| self.offsets()[1] |
| } |
| |
| pub fn b_offset(&self) -> usize { |
| self.offsets()[2] |
| } |
| |
| pub fn alpha_offset(&self) -> usize { |
| self.offsets()[3] |
| } |
| |
| pub(crate) fn has_alpha(&self) -> bool { |
| !matches!(self, Format::Rgb | Format::Bgr | Format::Rgb565) |
| } |
| } |
| |
| #[repr(C)] |
| #[derive(Clone, Copy, Default)] |
| pub enum ChromaUpsampling { |
| #[default] |
| Automatic, |
| Fastest, |
| BestQuality, |
| Nearest, |
| Bilinear, |
| } |
| |
| impl ChromaUpsampling { |
| #[cfg(feature = "libyuv")] |
| pub(crate) fn nearest_neighbor_filter_allowed(&self) -> bool { |
| !matches!(self, Self::Bilinear | Self::BestQuality) |
| } |
| |
| #[cfg(feature = "libyuv")] |
| pub(crate) fn bilinear_or_better_filter_allowed(&self) -> bool { |
| !matches!(self, Self::Nearest | Self::Fastest) |
| } |
| } |
| |
| #[repr(C)] |
| #[derive(Clone, Copy, Default)] |
| pub enum ChromaDownsampling { |
| #[default] |
| Automatic, |
| Fastest, |
| BestQuality, |
| Average, |
| SharpYuv, |
| } |
| |
| #[derive(Default)] |
| pub struct Image { |
| pub width: u32, |
| pub height: u32, |
| pub depth: u8, |
| pub format: Format, |
| pub chroma_upsampling: ChromaUpsampling, |
| pub chroma_downsampling: ChromaDownsampling, |
| pub premultiply_alpha: bool, |
| pub is_float: bool, |
| pub max_threads: i32, |
| pub pixels: Option<Pixels>, |
| pub row_bytes: u32, |
| } |
| |
| #[derive(Debug, Default, PartialEq)] |
| pub enum AlphaMultiplyMode { |
| #[default] |
| NoOp, |
| Multiply, |
| UnMultiply, |
| } |
| |
| impl Image { |
| pub(crate) fn max_channel(&self) -> u16 { |
| ((1i32 << self.depth) - 1) as u16 |
| } |
| |
| pub(crate) fn max_channel_f(&self) -> f32 { |
| self.max_channel() as f32 |
| } |
| |
| pub fn create_from_yuv(image: &image::Image) -> Self { |
| Self { |
| width: image.width, |
| height: image.height, |
| depth: image.depth, |
| format: Format::Rgba, |
| chroma_upsampling: ChromaUpsampling::Automatic, |
| chroma_downsampling: ChromaDownsampling::Automatic, |
| premultiply_alpha: false, |
| is_float: false, |
| max_threads: 1, |
| pixels: None, |
| row_bytes: 0, |
| } |
| } |
| |
| pub(crate) fn pixels(&mut self) -> *mut u8 { |
| if self.pixels.is_none() { |
| return std::ptr::null_mut(); |
| } |
| match self.pixels.unwrap_mut() { |
| Pixels::Pointer(ptr) => ptr.ptr_mut(), |
| Pixels::Pointer16(ptr) => ptr.ptr_mut() as *mut u8, |
| Pixels::Buffer(buffer) => buffer.as_mut_ptr(), |
| Pixels::Buffer16(buffer) => buffer.as_mut_ptr() as *mut u8, |
| } |
| } |
| |
| pub fn row(&self, row: u32) -> AvifResult<&[u8]> { |
| self.pixels |
| .as_ref() |
| .ok_or(AvifError::NoContent)? |
| .slice(checked_mul!(row, self.row_bytes)?, self.row_bytes) |
| } |
| |
| pub fn row_mut(&mut self, row: u32) -> AvifResult<&mut [u8]> { |
| self.pixels |
| .as_mut() |
| .ok_or(AvifError::NoContent)? |
| .slice_mut(checked_mul!(row, self.row_bytes)?, self.row_bytes) |
| } |
| |
| pub fn row16(&self, row: u32) -> AvifResult<&[u16]> { |
| self.pixels |
| .as_ref() |
| .ok_or(AvifError::NoContent)? |
| .slice16(checked_mul!(row, self.row_bytes / 2)?, self.row_bytes / 2) |
| } |
| |
| pub fn row16_mut(&mut self, row: u32) -> AvifResult<&mut [u16]> { |
| self.pixels |
| .as_mut() |
| .ok_or(AvifError::NoContent)? |
| .slice16_mut(checked_mul!(row, self.row_bytes / 2)?, self.row_bytes / 2) |
| } |
| |
| pub fn allocate(&mut self) -> AvifResult<()> { |
| let row_bytes = checked_mul!(self.width, self.pixel_size())?; |
| if self.channel_size() == 1 { |
| let buffer_size: usize = usize_from_u32(checked_mul!(row_bytes, self.height)?)?; |
| let buffer: Vec<u8> = vec![0; buffer_size]; |
| self.pixels = Some(Pixels::Buffer(buffer)); |
| } else { |
| let buffer_size: usize = usize_from_u32(checked_mul!(row_bytes / 2, self.height)?)?; |
| let buffer: Vec<u16> = vec![0; buffer_size]; |
| self.pixels = Some(Pixels::Buffer16(buffer)); |
| } |
| self.row_bytes = row_bytes; |
| Ok(()) |
| } |
| |
| pub(crate) fn depth_valid(&self) -> bool { |
| match (self.format, self.is_float, self.depth) { |
| (Format::Rgb565, false, 8) => true, |
| (Format::Rgb565, _, _) => false, |
| (_, true, 16) => true, // IEEE 754 half-precision binary16 |
| (_, false, 8 | 10 | 12 | 16) => true, |
| _ => false, |
| } |
| } |
| |
| pub fn has_alpha(&self) -> bool { |
| match self.format { |
| Format::Rgba | Format::Bgra | Format::Argb | Format::Abgr | Format::Rgba1010102 => true, |
| Format::Rgb | Format::Bgr | Format::Rgb565 => false, |
| } |
| } |
| |
| pub(crate) fn channel_size(&self) -> u32 { |
| match self.depth { |
| 8 => 1, |
| 10 | 12 | 16 => 2, |
| _ => panic!(), |
| } |
| } |
| |
| pub(crate) fn channel_count(&self) -> u32 { |
| match self.format { |
| Format::Rgba | Format::Bgra | Format::Argb | Format::Abgr => 4, |
| Format::Rgb | Format::Bgr => 3, |
| Format::Rgb565 => 2, |
| Format::Rgba1010102 => 0, // This is never used. |
| } |
| } |
| |
| pub(crate) fn pixel_size(&self) -> u32 { |
| match self.format { |
| Format::Rgba | Format::Bgra | Format::Argb | Format::Abgr => self.channel_size() * 4, |
| Format::Rgb | Format::Bgr => self.channel_size() * 3, |
| Format::Rgb565 => 2, |
| Format::Rgba1010102 => 4, |
| } |
| } |
| |
| fn convert_to_half_float(&mut self) -> AvifResult<()> { |
| let scale = 1.0 / self.max_channel_f(); |
| match libyuv::convert_to_half_float(self, scale) { |
| Ok(_) => return Ok(()), |
| Err(err) => { |
| if err != AvifError::NotImplemented { |
| return Err(err); |
| } |
| } |
| } |
| // This constant comes from libyuv. For details, see here: |
| // https://chromium.googlesource.com/libyuv/libyuv/+/2f87e9a7/source/row_common.cc#3537 |
| let reinterpret_f32_as_u32 = |f: f32| u32::from_le_bytes(f.to_le_bytes()); |
| let multiplier = 1.925_93e-34 * scale; |
| for y in 0..self.height { |
| let row = self.row16_mut(y)?; |
| for pixel in row { |
| *pixel = (reinterpret_f32_as_u32((*pixel as f32) * multiplier) >> 13) as u16; |
| } |
| } |
| Ok(()) |
| } |
| |
| pub fn convert_from_yuv(&mut self, image: &image::Image) -> AvifResult<()> { |
| if !image.has_plane(Plane::Y) || !image.depth_valid() || !self.depth_valid() { |
| return Err(AvifError::ReformatFailed); |
| } |
| if matches!( |
| image.matrix_coefficients, |
| MatrixCoefficients::Reserved |
| | MatrixCoefficients::Bt2020Cl |
| | MatrixCoefficients::Smpte2085 |
| | MatrixCoefficients::ChromaDerivedCl |
| | MatrixCoefficients::Ictcp |
| ) { |
| return Err(AvifError::NotImplemented); |
| } |
| if image.matrix_coefficients == MatrixCoefficients::Ycgco |
| && image.yuv_range == YuvRange::Limited |
| { |
| return Err(AvifError::NotImplemented); |
| } |
| if matches!( |
| image.matrix_coefficients, |
| MatrixCoefficients::YcgcoRe | MatrixCoefficients::YcgcoRo |
| ) { |
| if image.yuv_range == YuvRange::Limited { |
| return Err(AvifError::NotImplemented); |
| } |
| let bit_offset = |
| if image.matrix_coefficients == MatrixCoefficients::YcgcoRe { 2 } else { 1 }; |
| if image.depth - bit_offset != self.depth { |
| return Err(AvifError::NotImplemented); |
| } |
| } |
| // Android MediaCodec maps all underlying YUV formats to PixelFormat::Yuv420. So do not |
| // perform this validation for Android MediaCodec. The libyuv wrapper will simply use Bt601 |
| // coefficients for this color conversion. |
| #[cfg(not(feature = "android_mediacodec"))] |
| if image.matrix_coefficients == MatrixCoefficients::Identity |
| && !matches!(image.yuv_format, PixelFormat::Yuv444 | PixelFormat::Yuv400) |
| { |
| return Err(AvifError::NotImplemented); |
| } |
| |
| let mut alpha_multiply_mode = AlphaMultiplyMode::NoOp; |
| if image.has_alpha() && self.has_alpha() { |
| if !image.alpha_premultiplied && self.premultiply_alpha { |
| alpha_multiply_mode = AlphaMultiplyMode::Multiply; |
| } else if image.alpha_premultiplied && !self.premultiply_alpha { |
| alpha_multiply_mode = AlphaMultiplyMode::UnMultiply; |
| } |
| } |
| |
| let mut converted_with_libyuv: bool = false; |
| let mut alpha_reformatted_with_libyuv = false; |
| if alpha_multiply_mode == AlphaMultiplyMode::NoOp || self.has_alpha() { |
| match libyuv::yuv_to_rgb(image, self) { |
| Ok(alpha_reformatted) => { |
| alpha_reformatted_with_libyuv = alpha_reformatted; |
| converted_with_libyuv = true; |
| } |
| Err(err) => { |
| if err != AvifError::NotImplemented { |
| return Err(err); |
| } |
| } |
| } |
| } |
| if matches!( |
| image.yuv_format, |
| PixelFormat::AndroidNv12 | PixelFormat::AndroidNv21 |
| ) | matches!(self.format, Format::Rgba1010102) |
| { |
| // These conversions are only supported via libyuv. |
| if converted_with_libyuv { |
| if image.has_alpha() && matches!(self.format, Format::Rgba1010102) { |
| // If the source image has an alpha channel, scale them to 2 bits and fill it |
| // into the rgb image. Otherwise, libyuv writes them as opaque by default. |
| self.import_alpha_from(image)?; |
| } |
| return Ok(()); |
| } else { |
| return Err(AvifError::NotImplemented); |
| } |
| } |
| if self.has_alpha() && !alpha_reformatted_with_libyuv { |
| if image.has_alpha() { |
| self.import_alpha_from(image)?; |
| } else { |
| self.set_opaque()?; |
| } |
| } |
| if !converted_with_libyuv { |
| let mut converted_by_fast_path = false; |
| if (matches!( |
| self.chroma_upsampling, |
| ChromaUpsampling::Nearest | ChromaUpsampling::Fastest |
| ) || matches!(image.yuv_format, PixelFormat::Yuv444 | PixelFormat::Yuv400)) |
| && (alpha_multiply_mode == AlphaMultiplyMode::NoOp || self.format.has_alpha()) |
| { |
| match rgb_impl::yuv_to_rgb_fast(image, self) { |
| Ok(_) => converted_by_fast_path = true, |
| Err(err) => { |
| if err != AvifError::NotImplemented { |
| return Err(err); |
| } |
| } |
| } |
| } |
| if !converted_by_fast_path { |
| rgb_impl::yuv_to_rgb_any(image, self, alpha_multiply_mode)?; |
| alpha_multiply_mode = AlphaMultiplyMode::NoOp; |
| } |
| } |
| match alpha_multiply_mode { |
| AlphaMultiplyMode::Multiply => self.premultiply_alpha()?, |
| AlphaMultiplyMode::UnMultiply => self.unpremultiply_alpha()?, |
| AlphaMultiplyMode::NoOp => {} |
| } |
| if self.is_float { |
| self.convert_to_half_float()?; |
| } |
| Ok(()) |
| } |
| |
| pub fn shuffle_channels_to(self, format: Format) -> AvifResult<Image> { |
| if self.format == format { |
| return Ok(self); |
| } |
| if self.format == Format::Rgb565 || format == Format::Rgb565 { |
| return Err(AvifError::NotImplemented); |
| } |
| |
| let mut dst = Image { |
| format, |
| pixels: None, |
| row_bytes: 0, |
| ..self |
| }; |
| dst.allocate()?; |
| |
| let src_channel_count = self.channel_count(); |
| let dst_channel_count = dst.channel_count(); |
| let src_offsets = self.format.offsets(); |
| let dst_offsets = dst.format.offsets(); |
| let src_has_alpha = self.has_alpha(); |
| let dst_has_alpha = dst.has_alpha(); |
| let dst_max_channel = dst.max_channel(); |
| for y in 0..self.height { |
| if self.depth == 8 { |
| let src_row = self.row(y)?; |
| let dst_row = &mut dst.row_mut(y)?; |
| for x in 0..self.width { |
| let src_pixel_i = (src_channel_count * x) as usize; |
| let dst_pixel_i = (dst_channel_count * x) as usize; |
| for c in 0..3 { |
| dst_row[dst_pixel_i + dst_offsets[c]] = |
| src_row[src_pixel_i + src_offsets[c]]; |
| } |
| if dst_has_alpha { |
| dst_row[dst_pixel_i + dst_offsets[3]] = if src_has_alpha { |
| src_row[src_pixel_i + src_offsets[3]] |
| } else { |
| dst_max_channel as u8 |
| }; |
| } |
| } |
| } else { |
| let src_row = self.row16(y)?; |
| let dst_row = &mut dst.row16_mut(y)?; |
| for x in 0..self.width { |
| let src_pixel_i = (src_channel_count * x) as usize; |
| let dst_pixel_i = (dst_channel_count * x) as usize; |
| for c in 0..3 { |
| dst_row[dst_pixel_i + dst_offsets[c]] = |
| src_row[src_pixel_i + src_offsets[c]]; |
| } |
| if dst_has_alpha { |
| dst_row[dst_pixel_i + dst_offsets[3]] = if src_has_alpha { |
| src_row[src_pixel_i + src_offsets[3]] |
| } else { |
| dst_max_channel |
| }; |
| } |
| } |
| } |
| } |
| Ok(dst) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use crate::image::YuvRange; |
| use crate::image::ALL_PLANES; |
| use crate::image::MAX_PLANE_COUNT; |
| use crate::Category; |
| |
| use test_case::test_case; |
| use test_case::test_matrix; |
| |
| const WIDTH: usize = 3; |
| const HEIGHT: usize = 3; |
| struct YuvParams { |
| width: u32, |
| height: u32, |
| depth: u8, |
| format: PixelFormat, |
| yuv_range: YuvRange, |
| color_primaries: ColorPrimaries, |
| matrix_coefficients: MatrixCoefficients, |
| planes: [[&'static [u16]; HEIGHT]; MAX_PLANE_COUNT], |
| } |
| |
| const YUV_PARAMS: [YuvParams; 1] = [YuvParams { |
| width: WIDTH as u32, |
| height: HEIGHT as u32, |
| depth: 12, |
| format: PixelFormat::Yuv420, |
| yuv_range: YuvRange::Limited, |
| color_primaries: ColorPrimaries::Srgb, |
| matrix_coefficients: MatrixCoefficients::Bt709, |
| planes: [ |
| [ |
| &[1001, 1001, 1001], |
| &[1001, 1001, 1001], |
| &[1001, 1001, 1001], |
| ], |
| [&[1637, 1637], &[1637, 1637], &[1637, 1637]], |
| [&[3840, 3840], &[3840, 3840], &[3840, 3840]], |
| [&[0, 0, 2039], &[0, 2039, 4095], &[2039, 4095, 4095]], |
| ], |
| }]; |
| |
| struct RgbParams { |
| params: ( |
| /*yuv_param_index:*/ usize, |
| /*format:*/ Format, |
| /*depth:*/ u8, |
| /*premultiply_alpha:*/ bool, |
| /*is_float:*/ bool, |
| ), |
| expected_rgba: [&'static [u16]; HEIGHT], |
| } |
| |
| const RGB_PARAMS: [RgbParams; 5] = [ |
| RgbParams { |
| params: (0, Format::Rgba, 16, true, false), |
| expected_rgba: [ |
| &[0, 0, 0, 0, 0, 0, 0, 0, 32631, 1, 0, 32631], |
| &[0, 0, 0, 0, 32631, 1, 0, 32631, 65535, 2, 0, 65535], |
| &[32631, 1, 0, 32631, 65535, 2, 0, 65535, 65535, 2, 0, 65535], |
| ], |
| }, |
| RgbParams { |
| params: (0, Format::Rgba, 16, true, true), |
| expected_rgba: [ |
| &[0, 0, 0, 0, 0, 0, 0, 0, 14327, 256, 0, 14327], |
| &[0, 0, 0, 0, 14327, 256, 0, 14327, 15360, 512, 0, 15360], |
| &[ |
| 14327, 256, 0, 14327, 15360, 512, 0, 15360, 15360, 512, 0, 15360, |
| ], |
| ], |
| }, |
| RgbParams { |
| params: (0, Format::Rgba, 16, false, true), |
| expected_rgba: [ |
| &[15360, 512, 0, 0, 15360, 512, 0, 0, 15360, 512, 0, 14327], |
| &[15360, 512, 0, 0, 15360, 512, 0, 14327, 15360, 512, 0, 15360], |
| &[ |
| 15360, 512, 0, 14327, 15360, 512, 0, 15360, 15360, 512, 0, 15360, |
| ], |
| ], |
| }, |
| RgbParams { |
| params: (0, Format::Rgba, 16, false, false), |
| expected_rgba: [ |
| &[65535, 2, 0, 0, 65535, 2, 0, 0, 65535, 2, 0, 32631], |
| &[65535, 2, 0, 0, 65535, 2, 0, 32631, 65535, 2, 0, 65535], |
| &[65535, 2, 0, 32631, 65535, 2, 0, 65535, 65535, 2, 0, 65535], |
| ], |
| }, |
| RgbParams { |
| params: (0, Format::Bgra, 16, true, false), |
| expected_rgba: [ |
| &[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 32631, 32631], |
| &[0, 0, 0, 0, 0, 1, 32631, 32631, 0, 2, 65535, 65535], |
| &[0, 1, 32631, 32631, 0, 2, 65535, 65535, 0, 2, 65535, 65535], |
| ], |
| }, |
| ]; |
| |
| #[test_matrix(0usize..5)] |
| fn rgb_conversion(rgb_param_index: usize) -> AvifResult<()> { |
| let rgb_params = &RGB_PARAMS[rgb_param_index]; |
| let yuv_params = &YUV_PARAMS[rgb_params.params.0]; |
| let mut image = image::Image { |
| width: yuv_params.width, |
| height: yuv_params.height, |
| depth: yuv_params.depth, |
| yuv_format: yuv_params.format, |
| color_primaries: yuv_params.color_primaries, |
| matrix_coefficients: yuv_params.matrix_coefficients, |
| yuv_range: yuv_params.yuv_range, |
| ..image::Image::default() |
| }; |
| image.allocate_planes(Category::Color)?; |
| image.allocate_planes(Category::Alpha)?; |
| let yuva_planes = &yuv_params.planes; |
| for plane in ALL_PLANES { |
| let plane_index = plane.as_usize(); |
| if yuva_planes[plane_index].is_empty() { |
| continue; |
| } |
| for y in 0..image.height(plane) { |
| let row16 = image.row16_mut(plane, y as u32)?; |
| assert_eq!(row16.len(), yuva_planes[plane_index][y].len()); |
| let dst = &mut row16[..]; |
| dst.copy_from_slice(yuva_planes[plane_index][y]); |
| } |
| } |
| |
| let mut rgb = Image::create_from_yuv(&image); |
| assert_eq!(rgb.width, image.width); |
| assert_eq!(rgb.height, image.height); |
| assert_eq!(rgb.depth, image.depth); |
| |
| rgb.format = rgb_params.params.1; |
| rgb.depth = rgb_params.params.2; |
| rgb.premultiply_alpha = rgb_params.params.3; |
| rgb.is_float = rgb_params.params.4; |
| |
| rgb.allocate()?; |
| rgb.convert_from_yuv(&image)?; |
| |
| for y in 0..rgb.height as usize { |
| let row16 = rgb.row16(y as u32)?; |
| assert_eq!(&row16[..], rgb_params.expected_rgba[y]); |
| } |
| Ok(()) |
| } |
| |
| #[test_case(Format::Rgba, &[0, 1, 2, 3])] |
| #[test_case(Format::Abgr, &[3, 2, 1, 0])] |
| #[test_case(Format::Rgb, &[0, 1, 2])] |
| fn shuffle_channels_to(format: Format, expected: &[u8]) { |
| let image = Image { |
| width: 1, |
| height: 1, |
| depth: 8, |
| format: Format::Rgba, |
| pixels: Some(Pixels::Buffer(vec![0u8, 1, 2, 3])), |
| row_bytes: 4, |
| ..Default::default() |
| }; |
| assert_eq!( |
| image.shuffle_channels_to(format).unwrap().row(0).unwrap(), |
| expected |
| ); |
| } |
| } |