Coverage Report

Created: 2026-04-12 07:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/codecs/avif/encoder.rs
Line
Count
Source
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::{BitDepth, 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<'static>,
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
0
    fn to_ravif(self) -> ravif::ColorModel {
44
0
        match self {
45
0
            Self::Srgb => ravif::ColorModel::RGB,
46
0
            Self::Bt709 => ravif::ColorModel::YCbCr,
47
        }
48
0
    }
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
0
    pub fn new(w: W) -> Self {
59
0
        AvifEncoder::new_with_speed_quality(w, 4, 80) // `cavif` uses these defaults
60
0
    }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::new
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new
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
0
    pub fn new_with_speed_quality(w: W, speed: u8, quality: u8) -> Self {
67
        // Clamp quality and speed to range
68
0
        let quality = min(quality, 100);
69
0
        let speed = min(speed, 10);
70
71
0
        let encoder = Encoder::new()
72
0
            .with_quality(f32::from(quality))
73
0
            .with_alpha_quality(f32::from(quality))
74
0
            .with_speed(speed)
75
0
            .with_bit_depth(BitDepth::Eight);
76
77
0
        AvifEncoder { inner: w, encoder }
78
0
    }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::new_with_speed_quality
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new_with_speed_quality
79
80
    /// Encode with the specified `color_space`.
81
0
    pub fn with_colorspace(mut self, color_space: ColorSpace) -> Self {
82
0
        self.encoder = self
83
0
            .encoder
84
0
            .with_internal_color_model(color_space.to_ravif());
85
0
        self
86
0
    }
87
88
    /// Configures `rayon` thread pool size.
89
    /// The default `None` is to use all threads in the default `rayon` thread pool.
90
0
    pub fn with_num_threads(mut self, num_threads: Option<usize>) -> Self {
91
0
        self.encoder = self.encoder.with_num_threads(num_threads);
92
0
        self
93
0
    }
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
0
    fn write_image(
104
0
        mut self,
105
0
        data: &[u8],
106
0
        width: u32,
107
0
        height: u32,
108
0
        color: ExtendedColorType,
109
0
    ) -> ImageResult<()> {
110
0
        let expected_buffer_len = color.buffer_size(width, height);
111
0
        assert_eq!(
112
            expected_buffer_len,
113
0
            data.len() as u64,
114
0
            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
115
0
            data.len(),
116
        );
117
118
0
        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
0
        let mut fallback = vec![]; // This vector is used if we need to do a color conversion.
123
0
        let result = match Self::encode_as_img(&mut fallback, data, width, height, color)? {
124
0
            RgbColor::Rgb8(buffer) => self.encoder.encode_rgb(buffer),
125
0
            RgbColor::Rgba8(buffer) => self.encoder.encode_rgba(buffer),
126
        };
127
0
        let data = result.map_err(|err| {
128
0
            ImageError::Encoding(EncodingError::new(ImageFormat::Avif.into(), err))
129
0
        })?;
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_> as image::io::encoder::ImageEncoder>::write_image::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image::{closure#0}
130
0
        self.inner.write_all(&data.avif_file)?;
131
0
        Ok(())
132
0
    }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_> as image::io::encoder::ImageEncoder>::write_image
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image
133
134
0
    fn set_exif_metadata(&mut self, exif: Vec<u8>) -> Result<(), UnsupportedError> {
135
        // encoder.with_exif() accepts Self rather than &mut self, and Encoder doesn't impl Default,
136
        // so we can't even mem::take it and have to do this instead
137
0
        let encoder = std::mem::replace(&mut self.encoder, Encoder::new());
138
0
        self.encoder = encoder.with_exif(exif);
139
0
        Ok(())
140
0
    }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_> as image::io::encoder::ImageEncoder>::set_exif_metadata
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::set_exif_metadata
141
}
142
143
impl<W: Write> AvifEncoder<W> {
144
    // Does not currently do anything. Mirrors behaviour of old config function.
145
0
    fn set_color(&mut self, _color: ExtendedColorType) {
146
        // self.config.color_space = ColorSpace::RGB;
147
0
    }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::set_color
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::set_color
148
149
0
    fn encode_as_img<'buf>(
150
0
        fallback: &'buf mut Vec<u8>,
151
0
        data: &'buf [u8],
152
0
        width: u32,
153
0
        height: u32,
154
0
        color: ExtendedColorType,
155
0
    ) -> ImageResult<RgbColor<'buf>> {
156
        // Error wrapping utility for color dependent buffer dimensions.
157
0
        fn try_from_raw<P: Pixel>(
158
0
            data: &[P::Subpixel],
159
0
            width: u32,
160
0
            height: u32,
161
0
        ) -> ImageResult<ImageBuffer<P, &[P::Subpixel]>> {
162
0
            ImageBuffer::from_raw(width, height, data).ok_or_else(|| {
163
0
                ImageError::Parameter(ParameterError::from_kind(
164
0
                    ParameterErrorKind::DimensionMismatch,
165
0
                ))
166
0
            })
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<_>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgb<u8>>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgb<u16>>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Luma<u8>>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Luma<u16>>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgba<u8>>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgba<u16>>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::LumaA<u8>>::{closure#0}
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::LumaA<u16>>::{closure#0}
167
0
        }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<_>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgb<u8>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgb<u16>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Luma<u8>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Luma<u16>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgba<u8>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::Rgba<u16>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::LumaA<u8>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::try_from_raw::<image::color::LumaA<u16>>
168
169
        // Convert to target color type using few buffer allocations.
170
0
        fn convert_into<'buf, P>(
171
0
            buf: &'buf mut Vec<u8>,
172
0
            image: ImageBuffer<P, &[P::Subpixel]>,
173
0
        ) -> Img<&'buf [RGBA8]>
174
0
        where
175
0
            P: Pixel,
176
0
            Rgba<u8>: FromColor<P>,
177
        {
178
0
            let (width, height) = image.dimensions();
179
            // TODO: conversion re-using the target buffer?
180
0
            let image: ImageBuffer<Rgba<u8>, _> = image.convert();
181
0
            *buf = image.into_raw();
182
0
            Img::new(buf.as_pixels(), width as usize, height as usize)
183
0
        }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::convert_into::<_>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::convert_into::<image::color::Rgb<u16>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::convert_into::<image::color::Luma<u8>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::convert_into::<image::color::Luma<u16>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::convert_into::<image::color::Rgba<u16>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::convert_into::<image::color::LumaA<u8>>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::convert_into::<image::color::LumaA<u16>>
184
185
        // Cast the input slice using few buffer allocations if possible.
186
        // In particular try not to allocate if the caller did the infallible reverse.
187
0
        fn cast_buffer<Channel>(buf: &[u8]) -> ImageResult<Cow<'_, [Channel]>>
188
0
        where
189
0
            Channel: Pod + Zero,
190
        {
191
0
            match try_cast_slice(buf) {
192
0
                Ok(slice) => Ok(Cow::Borrowed(slice)),
193
0
                Err(PodCastError::OutputSliceWouldHaveSlop) => Err(ImageError::Parameter(
194
0
                    ParameterError::from_kind(ParameterErrorKind::DimensionMismatch),
195
0
                )),
196
                Err(PodCastError::TargetAlignmentGreaterAndInputNotAligned) => {
197
                    // Sad, but let's allocate.
198
                    // bytemuck checks alignment _before_ slop but size mismatch before this..
199
0
                    if !buf.len().is_multiple_of(size_of::<Channel>()) {
200
0
                        Err(ImageError::Parameter(ParameterError::from_kind(
201
0
                            ParameterErrorKind::DimensionMismatch,
202
0
                        )))
203
                    } else {
204
0
                        let len = buf.len() / size_of::<Channel>();
205
0
                        let mut data = vec![Channel::zero(); len];
206
0
                        let view = try_cast_slice_mut::<_, u8>(data.as_mut_slice()).unwrap();
207
0
                        view.copy_from_slice(buf);
208
0
                        Ok(Cow::Owned(data))
209
                    }
210
                }
211
0
                Err(err) => {
212
                    // Are you trying to encode a ZST??
213
0
                    Err(ImageError::Parameter(ParameterError::from_kind(
214
0
                        ParameterErrorKind::Generic(format!("{err:?}")),
215
0
                    )))
216
                }
217
            }
218
0
        }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::cast_buffer::<_>
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img::cast_buffer::<u16>
219
220
0
        match color {
221
            ExtendedColorType::Rgb8 => {
222
                // ravif doesn't do any checks but has some asserts, so we do the checks.
223
0
                let img = try_from_raw::<Rgb<u8>>(data, width, height)?;
224
                // Now, internally ravif uses u32 but it takes usize. We could do some checked
225
                // conversion but instead we use that a non-empty image must be addressable.
226
0
                if img.pixels().len() == 0 {
227
0
                    return Err(ImageError::Parameter(ParameterError::from_kind(
228
0
                        ParameterErrorKind::DimensionMismatch,
229
0
                    )));
230
0
                }
231
232
0
                Ok(RgbColor::Rgb8(Img::new(
233
0
                    AsPixels::as_pixels(data),
234
0
                    width as usize,
235
0
                    height as usize,
236
0
                )))
237
            }
238
            ExtendedColorType::Rgba8 => {
239
                // ravif doesn't do any checks but has some asserts, so we do the checks.
240
0
                let img = try_from_raw::<Rgba<u8>>(data, width, height)?;
241
                // Now, internally ravif uses u32 but it takes usize. We could do some checked
242
                // conversion but instead we use that a non-empty image must be addressable.
243
0
                if img.pixels().len() == 0 {
244
0
                    return Err(ImageError::Parameter(ParameterError::from_kind(
245
0
                        ParameterErrorKind::DimensionMismatch,
246
0
                    )));
247
0
                }
248
249
0
                Ok(RgbColor::Rgba8(Img::new(
250
0
                    AsPixels::as_pixels(data),
251
0
                    width as usize,
252
0
                    height as usize,
253
0
                )))
254
            }
255
            // we need a separate buffer..
256
            ExtendedColorType::L8 => {
257
0
                let image = try_from_raw::<Luma<u8>>(data, width, height)?;
258
0
                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
259
            }
260
            ExtendedColorType::La8 => {
261
0
                let image = try_from_raw::<LumaA<u8>>(data, width, height)?;
262
0
                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
263
            }
264
            // we need to really convert data..
265
            ExtendedColorType::L16 => {
266
0
                let buffer = cast_buffer(data)?;
267
0
                let image = try_from_raw::<Luma<u16>>(&buffer, width, height)?;
268
0
                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
269
            }
270
            ExtendedColorType::La16 => {
271
0
                let buffer = cast_buffer(data)?;
272
0
                let image = try_from_raw::<LumaA<u16>>(&buffer, width, height)?;
273
0
                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
274
            }
275
            ExtendedColorType::Rgb16 => {
276
0
                let buffer = cast_buffer(data)?;
277
0
                let image = try_from_raw::<Rgb<u16>>(&buffer, width, height)?;
278
0
                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
279
            }
280
            ExtendedColorType::Rgba16 => {
281
0
                let buffer = cast_buffer(data)?;
282
0
                let image = try_from_raw::<Rgba<u16>>(&buffer, width, height)?;
283
0
                Ok(RgbColor::Rgba8(convert_into(fallback, image)))
284
            }
285
            // for cases we do not support at all?
286
0
            _ => Err(ImageError::Unsupported(
287
0
                UnsupportedError::from_format_and_kind(
288
0
                    ImageFormat::Avif.into(),
289
0
                    UnsupportedErrorKind::Color(color),
290
0
                ),
291
0
            )),
292
        }
293
0
    }
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<_>>::encode_as_img
Unexecuted instantiation: <image::codecs::avif::encoder::AvifEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_as_img
294
}