| 1 | //! Encoding of AVIF images. |
| 2 | /// |
| 3 | /// The [AVIF] specification defines an image derivative of the AV1 bitstream, an open video codec. |
| 4 | /// |
| 5 | /// [AVIF]: https://aomediacodec.github.io/av1-avif/ |
| 6 | use std::borrow::Cow; |
| 7 | use std::cmp::min; |
| 8 | use std::io::Write; |
| 9 | use std::mem::size_of; |
| 10 | |
| 11 | use crate::buffer::ConvertBuffer; |
| 12 | use crate::color::{FromColor, Luma, LumaA, Rgb, Rgba}; |
| 13 | use crate::error::{ |
| 14 | EncodingError, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, |
| 15 | }; |
| 16 | use crate::{ExtendedColorType, ImageBuffer, ImageEncoder, ImageFormat, Pixel}; |
| 17 | use crate::{ImageError, ImageResult}; |
| 18 | |
| 19 | use bytemuck::{try_cast_slice, try_cast_slice_mut, Pod, PodCastError}; |
| 20 | use num_traits::Zero; |
| 21 | use ravif::{Encoder, Img, RGB8, RGBA8}; |
| 22 | use rgb::AsPixels; |
| 23 | |
| 24 | /// AVIF Encoder. |
| 25 | /// |
| 26 | /// Writes one image into the chosen output. |
| 27 | pub struct AvifEncoder<W> { |
| 28 | inner: W, |
| 29 | encoder: Encoder, |
| 30 | } |
| 31 | |
| 32 | /// An enumeration over supported AVIF color spaces |
| 33 | #[derive (Debug, Copy, Clone, PartialEq, Eq)] |
| 34 | #[non_exhaustive ] |
| 35 | pub enum ColorSpace { |
| 36 | /// sRGB colorspace |
| 37 | Srgb, |
| 38 | /// BT.709 colorspace |
| 39 | Bt709, |
| 40 | } |
| 41 | |
| 42 | impl ColorSpace { |
| 43 | fn to_ravif(self) -> ravif::ColorSpace { |
| 44 | match self { |
| 45 | Self::Srgb => ravif::ColorSpace::RGB, |
| 46 | Self::Bt709 => ravif::ColorSpace::YCbCr, |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | enum RgbColor<'buf> { |
| 52 | Rgb8(Img<&'buf [RGB8]>), |
| 53 | Rgba8(Img<&'buf [RGBA8]>), |
| 54 | } |
| 55 | |
| 56 | impl<W: Write> AvifEncoder<W> { |
| 57 | /// Create a new encoder that writes its output to `w`. |
| 58 | pub fn new(w: W) -> Self { |
| 59 | AvifEncoder::new_with_speed_quality(w, 4, 80) // `cavif` uses these defaults |
| 60 | } |
| 61 | |
| 62 | /// Create a new encoder with a specified speed and quality that writes its output to `w`. |
| 63 | /// `speed` accepts a value in the range 1-10, where 1 is the slowest and 10 is the fastest. |
| 64 | /// Slower speeds generally yield better compression results. |
| 65 | /// `quality` accepts a value in the range 1-100, where 1 is the worst and 100 is the best. |
| 66 | pub fn new_with_speed_quality(w: W, speed: u8, quality: u8) -> Self { |
| 67 | // Clamp quality and speed to range |
| 68 | let quality = min(quality, 100); |
| 69 | let speed = min(speed, 10); |
| 70 | |
| 71 | let encoder = Encoder::new() |
| 72 | .with_quality(f32::from(quality)) |
| 73 | .with_alpha_quality(f32::from(quality)) |
| 74 | .with_speed(speed) |
| 75 | .with_depth(Some(8)); |
| 76 | |
| 77 | AvifEncoder { inner: w, encoder } |
| 78 | } |
| 79 | |
| 80 | /// Encode with the specified `color_space`. |
| 81 | pub fn with_colorspace(mut self, color_space: ColorSpace) -> Self { |
| 82 | self.encoder = self |
| 83 | .encoder |
| 84 | .with_internal_color_space(color_space.to_ravif()); |
| 85 | self |
| 86 | } |
| 87 | |
| 88 | /// Configures `rayon` thread pool size. |
| 89 | /// The default `None` is to use all threads in the default `rayon` thread pool. |
| 90 | pub fn with_num_threads(mut self, num_threads: Option<usize>) -> Self { |
| 91 | self.encoder = self.encoder.with_num_threads(num_threads); |
| 92 | self |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | impl<W: Write> ImageEncoder for AvifEncoder<W> { |
| 97 | /// Encode image data with the indicated color type. |
| 98 | /// |
| 99 | /// The encoder currently requires all data to be RGBA8, it will be converted internally if |
| 100 | /// necessary. When data is suitably aligned, i.e. u16 channels to two bytes, then the |
| 101 | /// conversion may be more efficient. |
| 102 | #[track_caller ] |
| 103 | fn write_image( |
| 104 | mut self, |
| 105 | data: &[u8], |
| 106 | width: u32, |
| 107 | height: u32, |
| 108 | color: ExtendedColorType, |
| 109 | ) -> ImageResult<()> { |
| 110 | let expected_buffer_len = color.buffer_size(width, height); |
| 111 | assert_eq!( |
| 112 | expected_buffer_len, |
| 113 | data.len() as u64, |
| 114 | "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x {height} image" , |
| 115 | data.len(), |
| 116 | ); |
| 117 | |
| 118 | self.set_color(color); |
| 119 | // `ravif` needs strongly typed data so let's convert. We can either use a temporarily |
| 120 | // owned version in our own buffer or zero-copy if possible by using the input buffer. |
| 121 | // This requires going through `rgb`. |
| 122 | let mut fallback = vec![]; // This vector is used if we need to do a color conversion. |
| 123 | let result = match Self::encode_as_img(&mut fallback, data, width, height, color)? { |
| 124 | RgbColor::Rgb8(buffer) => self.encoder.encode_rgb(buffer), |
| 125 | RgbColor::Rgba8(buffer) => self.encoder.encode_rgba(buffer), |
| 126 | }; |
| 127 | let data = result.map_err(|err| { |
| 128 | ImageError::Encoding(EncodingError::new(ImageFormat::Avif.into(), err)) |
| 129 | })?; |
| 130 | self.inner.write_all(&data.avif_file)?; |
| 131 | Ok(()) |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | impl<W: Write> AvifEncoder<W> { |
| 136 | // Does not currently do anything. Mirrors behaviour of old config function. |
| 137 | fn set_color(&mut self, _color: ExtendedColorType) { |
| 138 | // self.config.color_space = ColorSpace::RGB; |
| 139 | } |
| 140 | |
| 141 | fn encode_as_img<'buf>( |
| 142 | fallback: &'buf mut Vec<u8>, |
| 143 | data: &'buf [u8], |
| 144 | width: u32, |
| 145 | height: u32, |
| 146 | color: ExtendedColorType, |
| 147 | ) -> ImageResult<RgbColor<'buf>> { |
| 148 | // Error wrapping utility for color dependent buffer dimensions. |
| 149 | fn try_from_raw<P: Pixel + 'static>( |
| 150 | data: &[P::Subpixel], |
| 151 | width: u32, |
| 152 | height: u32, |
| 153 | ) -> ImageResult<ImageBuffer<P, &[P::Subpixel]>> { |
| 154 | ImageBuffer::from_raw(width, height, data).ok_or_else(|| { |
| 155 | ImageError::Parameter(ParameterError::from_kind( |
| 156 | ParameterErrorKind::DimensionMismatch, |
| 157 | )) |
| 158 | }) |
| 159 | } |
| 160 | |
| 161 | // Convert to target color type using few buffer allocations. |
| 162 | fn convert_into<'buf, P>( |
| 163 | buf: &'buf mut Vec<u8>, |
| 164 | image: ImageBuffer<P, &[P::Subpixel]>, |
| 165 | ) -> Img<&'buf [RGBA8]> |
| 166 | where |
| 167 | P: Pixel + 'static, |
| 168 | Rgba<u8>: FromColor<P>, |
| 169 | { |
| 170 | let (width, height) = image.dimensions(); |
| 171 | // TODO: conversion re-using the target buffer? |
| 172 | let image: ImageBuffer<Rgba<u8>, _> = image.convert(); |
| 173 | *buf = image.into_raw(); |
| 174 | Img::new(buf.as_pixels(), width as usize, height as usize) |
| 175 | } |
| 176 | |
| 177 | // Cast the input slice using few buffer allocations if possible. |
| 178 | // In particular try not to allocate if the caller did the infallible reverse. |
| 179 | fn cast_buffer<Channel>(buf: &[u8]) -> ImageResult<Cow<[Channel]>> |
| 180 | where |
| 181 | Channel: Pod + Zero, |
| 182 | { |
| 183 | match try_cast_slice(buf) { |
| 184 | Ok(slice) => Ok(Cow::Borrowed(slice)), |
| 185 | Err(PodCastError::OutputSliceWouldHaveSlop) => Err(ImageError::Parameter( |
| 186 | ParameterError::from_kind(ParameterErrorKind::DimensionMismatch), |
| 187 | )), |
| 188 | Err(PodCastError::TargetAlignmentGreaterAndInputNotAligned) => { |
| 189 | // Sad, but let's allocate. |
| 190 | // bytemuck checks alignment _before_ slop but size mismatch before this.. |
| 191 | if buf.len() % size_of::<Channel>() != 0 { |
| 192 | Err(ImageError::Parameter(ParameterError::from_kind( |
| 193 | ParameterErrorKind::DimensionMismatch, |
| 194 | ))) |
| 195 | } else { |
| 196 | let len = buf.len() / size_of::<Channel>(); |
| 197 | let mut data = vec![Channel::zero(); len]; |
| 198 | let view = try_cast_slice_mut::<_, u8>(data.as_mut_slice()).unwrap(); |
| 199 | view.copy_from_slice(buf); |
| 200 | Ok(Cow::Owned(data)) |
| 201 | } |
| 202 | } |
| 203 | Err(err) => { |
| 204 | // Are you trying to encode a ZST?? |
| 205 | Err(ImageError::Parameter(ParameterError::from_kind( |
| 206 | ParameterErrorKind::Generic(format!(" {err:?}" )), |
| 207 | ))) |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | match color { |
| 213 | ExtendedColorType::Rgb8 => { |
| 214 | // ravif doesn't do any checks but has some asserts, so we do the checks. |
| 215 | let img = try_from_raw::<Rgb<u8>>(data, width, height)?; |
| 216 | // Now, internally ravif uses u32 but it takes usize. We could do some checked |
| 217 | // conversion but instead we use that a non-empty image must be addressable. |
| 218 | if img.pixels().len() == 0 { |
| 219 | return Err(ImageError::Parameter(ParameterError::from_kind( |
| 220 | ParameterErrorKind::DimensionMismatch, |
| 221 | ))); |
| 222 | } |
| 223 | |
| 224 | Ok(RgbColor::Rgb8(Img::new( |
| 225 | AsPixels::as_pixels(data), |
| 226 | width as usize, |
| 227 | height as usize, |
| 228 | ))) |
| 229 | } |
| 230 | ExtendedColorType::Rgba8 => { |
| 231 | // ravif doesn't do any checks but has some asserts, so we do the checks. |
| 232 | let img = try_from_raw::<Rgba<u8>>(data, width, height)?; |
| 233 | // Now, internally ravif uses u32 but it takes usize. We could do some checked |
| 234 | // conversion but instead we use that a non-empty image must be addressable. |
| 235 | if img.pixels().len() == 0 { |
| 236 | return Err(ImageError::Parameter(ParameterError::from_kind( |
| 237 | ParameterErrorKind::DimensionMismatch, |
| 238 | ))); |
| 239 | } |
| 240 | |
| 241 | Ok(RgbColor::Rgba8(Img::new( |
| 242 | AsPixels::as_pixels(data), |
| 243 | width as usize, |
| 244 | height as usize, |
| 245 | ))) |
| 246 | } |
| 247 | // we need a separate buffer.. |
| 248 | ExtendedColorType::L8 => { |
| 249 | let image = try_from_raw::<Luma<u8>>(data, width, height)?; |
| 250 | Ok(RgbColor::Rgba8(convert_into(fallback, image))) |
| 251 | } |
| 252 | ExtendedColorType::La8 => { |
| 253 | let image = try_from_raw::<LumaA<u8>>(data, width, height)?; |
| 254 | Ok(RgbColor::Rgba8(convert_into(fallback, image))) |
| 255 | } |
| 256 | // we need to really convert data.. |
| 257 | ExtendedColorType::L16 => { |
| 258 | let buffer = cast_buffer(data)?; |
| 259 | let image = try_from_raw::<Luma<u16>>(&buffer, width, height)?; |
| 260 | Ok(RgbColor::Rgba8(convert_into(fallback, image))) |
| 261 | } |
| 262 | ExtendedColorType::La16 => { |
| 263 | let buffer = cast_buffer(data)?; |
| 264 | let image = try_from_raw::<LumaA<u16>>(&buffer, width, height)?; |
| 265 | Ok(RgbColor::Rgba8(convert_into(fallback, image))) |
| 266 | } |
| 267 | ExtendedColorType::Rgb16 => { |
| 268 | let buffer = cast_buffer(data)?; |
| 269 | let image = try_from_raw::<Rgb<u16>>(&buffer, width, height)?; |
| 270 | Ok(RgbColor::Rgba8(convert_into(fallback, image))) |
| 271 | } |
| 272 | ExtendedColorType::Rgba16 => { |
| 273 | let buffer = cast_buffer(data)?; |
| 274 | let image = try_from_raw::<Rgba<u16>>(&buffer, width, height)?; |
| 275 | Ok(RgbColor::Rgba8(convert_into(fallback, image))) |
| 276 | } |
| 277 | // for cases we do not support at all? |
| 278 | _ => Err(ImageError::Unsupported( |
| 279 | UnsupportedError::from_format_and_kind( |
| 280 | ImageFormat::Avif.into(), |
| 281 | UnsupportedErrorKind::Color(color), |
| 282 | ), |
| 283 | )), |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |