Skip to main content

ai_image/codecs/avif/
encoder.rs

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/
6use alloc::{borrow::Cow, format, vec, vec::Vec};
7use core::cmp::min;
8use core::mem::size_of;
9use no_std_io::io::Write;
10
11use crate::buffer::ConvertBuffer;
12use crate::color::{FromColor, Luma, LumaA, Rgb, Rgba};
13use crate::error::{
14    EncodingError, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind,
15};
16use crate::{ExtendedColorType, ImageBuffer, ImageEncoder, ImageFormat, Pixel};
17use crate::{ImageError, ImageResult};
18
19use bytemuck::{try_cast_slice, try_cast_slice_mut, Pod, PodCastError};
20use num_traits::Zero;
21use ravif::{BitDepth, Encoder, Img, RGB8, RGBA8};
22use rgb::AsPixels;
23
24/// AVIF Encoder.
25///
26/// Writes one image into the chosen output.
27pub struct AvifEncoder<W> {
28    inner: W,
29    encoder: Encoder<'static>,
30}
31
32/// An enumeration over supported AVIF color spaces
33#[derive(Debug, Copy, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum ColorSpace {
36    /// sRGB colorspace
37    Srgb,
38    /// BT.709 colorspace
39    Bt709,
40}
41
42impl ColorSpace {
43    fn to_ravif(self) -> ravif::ColorModel {
44        match self {
45            Self::Srgb => ravif::ColorModel::RGB,
46            Self::Bt709 => ravif::ColorModel::YCbCr,
47        }
48    }
49}
50
51enum RgbColor<'buf> {
52    Rgb8(Img<&'buf [RGB8]>),
53    Rgba8(Img<&'buf [RGBA8]>),
54}
55
56impl<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_bit_depth(BitDepth::Eight);
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_model(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
96impl<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    fn set_exif_metadata(&mut self, exif: Vec<u8>) -> Result<(), UnsupportedError> {
135        let encoder = core::mem::replace(&mut self.encoder, Encoder::new());
136        self.encoder = encoder.with_exif(exif);
137        Ok(())
138    }
139}
140
141impl<W: Write> AvifEncoder<W> {
142    // Does not currently do anything. Mirrors behaviour of old config function.
143    fn set_color(&mut self, _color: ExtendedColorType) {
144        // self.config.color_space = ColorSpace::RGB;
145    }
146
147    fn encode_as_img<'buf>(
148        fallback: &'buf mut Vec<u8>,
149        data: &'buf [u8],
150        width: u32,
151        height: u32,
152        color: ExtendedColorType,
153    ) -> ImageResult<RgbColor<'buf>> {
154        // Error wrapping utility for color dependent buffer dimensions.
155        fn try_from_raw<P: Pixel + 'static>(
156            data: &[P::Subpixel],
157            width: u32,
158            height: u32,
159        ) -> ImageResult<ImageBuffer<P, &[P::Subpixel]>> {
160            ImageBuffer::from_raw(width, height, data).ok_or_else(|| {
161                ImageError::Parameter(ParameterError::from_kind(
162                    ParameterErrorKind::DimensionMismatch,
163                ))
164            })
165        }
166
167        // Convert to target color type using few buffer allocations.
168        fn convert_into<'buf, P>(
169            buf: &'buf mut Vec<u8>,
170            image: ImageBuffer<P, &[P::Subpixel]>,
171        ) -> Img<&'buf [RGBA8]>
172        where
173            P: Pixel + 'static,
174            Rgba<u8>: FromColor<P>,
175        {
176            let (width, height) = image.dimensions();
177            // TODO: conversion re-using the target buffer?
178            let image: ImageBuffer<Rgba<u8>, _> = image.convert();
179            *buf = image.into_raw();
180            Img::new(buf.as_pixels(), width as usize, height as usize)
181        }
182
183        // Cast the input slice using few buffer allocations if possible.
184        // In particular try not to allocate if the caller did the infallible reverse.
185        fn cast_buffer<Channel>(buf: &[u8]) -> ImageResult<Cow<'_, [Channel]>>
186        where
187            Channel: Pod + Zero,
188        {
189            match try_cast_slice(buf) {
190                Ok(slice) => Ok(Cow::Borrowed(slice)),
191                Err(PodCastError::OutputSliceWouldHaveSlop) => Err(ImageError::Parameter(
192                    ParameterError::from_kind(ParameterErrorKind::DimensionMismatch),
193                )),
194                Err(PodCastError::TargetAlignmentGreaterAndInputNotAligned) => {
195                    // Sad, but let's allocate.
196                    // bytemuck checks alignment _before_ slop but size mismatch before this..
197                    if !buf.len().is_multiple_of(size_of::<Channel>()) {
198                        Err(ImageError::Parameter(ParameterError::from_kind(
199                            ParameterErrorKind::DimensionMismatch,
200                        )))
201                    } else {
202                        let len = buf.len() / size_of::<Channel>();
203                        let mut data = vec![Channel::zero(); len];
204                        let view = try_cast_slice_mut::<_, u8>(data.as_mut_slice()).unwrap();
205                        view.copy_from_slice(buf);
206                        Ok(Cow::Owned(data))
207                    }
208                }
209                Err(err) => {
210                    // Are you trying to encode a ZST??
211                    Err(ImageError::Parameter(ParameterError::from_kind(
212                        ParameterErrorKind::Generic(format!("{err:?}")),
213                    )))
214                }
215            }
216        }
217
218        match color {
219            ExtendedColorType::Rgb8 => {
220                // ravif doesn't do any checks but has some asserts, so we do the checks.
221                let img = try_from_raw::<Rgb<u8>>(data, width, height)?;
222                // Now, internally ravif uses u32 but it takes usize. We could do some checked
223                // conversion but instead we use that a non-empty image must be addressable.
224                if img.pixels().len() == 0 {
225                    return Err(ImageError::Parameter(ParameterError::from_kind(
226                        ParameterErrorKind::DimensionMismatch,
227                    )));
228                }
229
230                Ok(RgbColor::Rgb8(Img::new(
231                    AsPixels::as_pixels(data),
232                    width as usize,
233                    height as usize,
234                )))
235            }
236            ExtendedColorType::Rgba8 => {
237                // ravif doesn't do any checks but has some asserts, so we do the checks.
238                let img = try_from_raw::<Rgba<u8>>(data, width, height)?;
239                // Now, internally ravif uses u32 but it takes usize. We could do some checked
240                // conversion but instead we use that a non-empty image must be addressable.
241                if img.pixels().len() == 0 {
242                    return Err(ImageError::Parameter(ParameterError::from_kind(
243                        ParameterErrorKind::DimensionMismatch,
244                    )));
245                }
246
247                Ok(RgbColor::Rgba8(Img::new(
248                    AsPixels::as_pixels(data),
249                    width as usize,
250                    height as usize,
251                )))
252            }
253            // we need a separate buffer..
254            ExtendedColorType::L8 => {
255                let image = try_from_raw::<Luma<u8>>(data, width, height)?;
256                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
257            }
258            ExtendedColorType::La8 => {
259                let image = try_from_raw::<LumaA<u8>>(data, width, height)?;
260                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
261            }
262            // we need to really convert data..
263            ExtendedColorType::L16 => {
264                let buffer = cast_buffer(data)?;
265                let image = try_from_raw::<Luma<u16>>(&buffer, width, height)?;
266                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
267            }
268            ExtendedColorType::La16 => {
269                let buffer = cast_buffer(data)?;
270                let image = try_from_raw::<LumaA<u16>>(&buffer, width, height)?;
271                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
272            }
273            ExtendedColorType::Rgb16 => {
274                let buffer = cast_buffer(data)?;
275                let image = try_from_raw::<Rgb<u16>>(&buffer, width, height)?;
276                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
277            }
278            ExtendedColorType::Rgba16 => {
279                let buffer = cast_buffer(data)?;
280                let image = try_from_raw::<Rgba<u16>>(&buffer, width, height)?;
281                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
282            }
283            // for cases we do not support at all?
284            _ => Err(ImageError::Unsupported(
285                UnsupportedError::from_format_and_kind(
286                    ImageFormat::Avif.into(),
287                    UnsupportedErrorKind::Color(color),
288                ),
289            )),
290        }
291    }
292}