Skip to main content

otf_pixels_codec_avif/
encoder.rs

1//! The AVIF encoder: pixels to YUV, YUV to an AV1 key frame, and the frame
2//! (with an alpha frame, when the image has transparency) into a HEIF
3//! container.
4//!
5//! What is written:
6//! - Colour as 8-bit 4:2:0 BT.601 full-range YUV with sRGB primaries and
7//!   transfer (`colr` nclx 1/13/6, libavif's default for RGB input); a grey
8//!   image as monochrome AV1.
9//! - Transparency as an auxiliary monochrome AV1 item (`auxC` alpha URN,
10//!   `auxl` reference to the colour item), coded at a finer quantizer than
11//!   the colour. An opaque alpha channel is dropped.
12//!
13//! Lossless AVIF needs 4:4:4 RGB coding at quantizer 0, which this encoder
14//! does not produce; asking for it is refused rather than silently lossy.
15
16use crate::av1::encode::frame::{CodedStill, Picture, encode_still, qindex_for_quality};
17use otf_pixels_core::{
18    EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
19};
20
21/// `colr` nclx: BT.709 primaries, sRGB transfer, BT.601 matrix.
22const CICP: (u8, u8, u8) = (1, 13, 6);
23/// The largest dimension an AV1 sequence header can declare.
24const MAX_DIMENSION: u32 = 65_536;
25
26/// Encodes an AVIF still.
27#[derive(Debug)]
28pub struct AvifEncoder {
29    state: Option<State>,
30    options: EncodeOptions,
31    /// The ICC profile for a `colr` of type `prof`, if any.
32    icc: Option<Vec<u8>>,
33}
34
35#[derive(Debug)]
36struct State {
37    descriptor: ImageDescriptor,
38    pixels: Vec<u8>,
39    rows_written: u32,
40}
41
42impl Default for AvifEncoder {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl AvifEncoder {
49    /// An encoder at the default quality.
50    #[must_use]
51    pub fn new() -> Self {
52        Self::from_options(&EncodeOptions::default())
53    }
54
55    /// An encoder at `options.quality`.
56    #[must_use]
57    pub const fn from_options(options: &EncodeOptions) -> Self {
58        Self {
59            state: None,
60            options: *options,
61            icc: None,
62        }
63    }
64}
65
66impl Encoder for AvifEncoder {
67    fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
68        if self.state.is_some() {
69            return Err(PixelsError::invalid_argument(
70                "profile",
71                "the ICC profile must be set before write_header",
72            ));
73        }
74        self.icc = profile.map(<[u8]>::to_vec);
75        Ok(())
76    }
77
78    fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
79        if self.state.is_some() {
80            return Err(PixelsError::invalid_argument(
81                "descriptor",
82                "write_header called more than once",
83            ));
84        }
85        if self.options.lossless {
86            return Err(PixelsError::unsupported(
87                "lossless AVIF encoding is not implemented; encode lossy or choose PNG or WebP",
88            ));
89        }
90        match desc.pixel {
91            PixelFormat::Gray8 | PixelFormat::GrayA8 | PixelFormat::Rgb8 | PixelFormat::Rgba8 => {}
92            other => {
93                return Err(PixelsError::unsupported(format!(
94                    "AVIF encoding needs an 8-bit format; got {other}. Convert first."
95                )));
96            }
97        }
98        if desc.width == 0
99            || desc.height == 0
100            || desc.width > MAX_DIMENSION
101            || desc.height > MAX_DIMENSION
102        {
103            return Err(PixelsError::unsupported(format!(
104                "AVIF dimensions must be 1 to {MAX_DIMENSION}; {}x{} is not",
105                desc.width, desc.height
106            )));
107        }
108        let capacity = desc
109            .byte_len()
110            .ok_or_else(|| PixelsError::malformed("avif", "image size overflows"))?;
111        self.state = Some(State {
112            descriptor: *desc,
113            pixels: Vec::with_capacity(capacity),
114            rows_written: 0,
115        });
116        Ok(())
117    }
118
119    fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
120        let Some(state) = self.state.as_mut() else {
121            return Err(PixelsError::invalid_argument(
122                "row",
123                "write_row called before write_header",
124            ));
125        };
126        let expected = state.descriptor.row_bytes();
127        if row.len() != expected {
128            return Err(PixelsError::invalid_argument(
129                "row",
130                format!("row is {} bytes, expected {expected}", row.len()),
131            ));
132        }
133        if state.rows_written >= state.descriptor.height {
134            return Err(PixelsError::invalid_argument(
135                "row",
136                format!("more than {} rows written", state.descriptor.height),
137            ));
138        }
139        state.pixels.extend_from_slice(row);
140        state.rows_written += 1;
141        Ok(())
142    }
143
144    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
145        let Some(state) = self.state.as_ref() else {
146            return Err(PixelsError::invalid_argument(
147                "sink",
148                "finish called before write_header",
149            ));
150        };
151        if state.rows_written < state.descriptor.height {
152            return Err(PixelsError::malformed(
153                "avif",
154                format!(
155                    "{} of {} rows were written",
156                    state.rows_written, state.descriptor.height
157                ),
158            ));
159        }
160        let bytes = encode(state, self.options.quality, self.icc.as_deref())?;
161        sink.write_all(&bytes)?;
162        sink.flush()
163    }
164}
165
166/// The planes of an 8-bit image: YUV 4:2:0 (or Y alone for grey) and the
167/// alpha channel, if any.
168fn to_planes(
169    pixels: &[u8],
170    format: PixelFormat,
171    width: usize,
172    height: usize,
173) -> (Vec<Vec<u16>>, Option<Vec<u16>>) {
174    let channels = format.channels();
175    let grey = matches!(format, PixelFormat::Gray8 | PixelFormat::GrayA8);
176    let has_alpha = matches!(format, PixelFormat::GrayA8 | PixelFormat::Rgba8);
177    let pixel = |i: usize| pixels.get(i * channels..(i + 1) * channels).unwrap_or(&[]);
178    let alpha = has_alpha.then(|| {
179        (0..width * height)
180            .map(|i| u16::from(pixel(i).get(channels - 1).copied().unwrap_or(255)))
181            .collect::<Vec<u16>>()
182    });
183    if grey {
184        let y = (0..width * height)
185            .map(|i| u16::from(pixel(i).first().copied().unwrap_or(0)))
186            .collect();
187        return (vec![y], alpha);
188    }
189    // BT.601 full range: Kr 0.299, Kb 0.114.
190    let mut y = Vec::with_capacity(width * height);
191    let mut u_full = Vec::with_capacity(width * height);
192    let mut v_full = Vec::with_capacity(width * height);
193    for i in 0..width * height {
194        let p = pixel(i);
195        let [r, g, b] = [0, 1, 2].map(|k| f64::from(p.get(k).copied().unwrap_or(0)));
196        let luma = 0.299 * r + 0.587 * g + 0.114 * b;
197        y.push(luma.round().clamp(0.0, 255.0) as u16);
198        u_full.push((b - luma) / 1.772);
199        v_full.push((r - luma) / 1.402);
200    }
201    let (cw, ch) = (width.div_ceil(2), height.div_ceil(2));
202    let average = |full: &[f64]| -> Vec<u16> {
203        let mut out = Vec::with_capacity(cw * ch);
204        for cy in 0..ch {
205            for cx in 0..cw {
206                let (mut sum, mut n) = (0.0, 0.0);
207                for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
208                    let (x, yy) = (cx * 2 + dx, cy * 2 + dy);
209                    if x < width && yy < height {
210                        sum += full.get(yy * width + x).copied().unwrap_or(0.0);
211                        n += 1.0;
212                    }
213                }
214                out.push((sum / n + 128.0).round().clamp(0.0, 255.0) as u16);
215            }
216        }
217        out
218    };
219    (vec![y, average(&u_full), average(&v_full)], alpha)
220}
221
222fn encode(state: &State, quality: u8, icc: Option<&[u8]>) -> Result<Vec<u8>> {
223    let (width, height) = (state.descriptor.width, state.descriptor.height);
224    let (planes, alpha) = to_planes(
225        &state.pixels,
226        state.descriptor.pixel,
227        width as usize,
228        height as usize,
229    );
230    let qindex = qindex_for_quality(quality);
231    let refs: Vec<&[u16]> = planes.iter().map(Vec::as_slice).collect();
232    let colour = encode_still(
233        &Picture {
234            width,
235            height,
236            planes: &refs,
237            cicp: CICP,
238            full_range: true,
239        },
240        qindex,
241    )?;
242    // Alpha edges show more than chroma errors: code it finer.
243    let alpha = match alpha.filter(|a| a.iter().any(|&v| v != 255)) {
244        Some(alpha) => Some(encode_still(
245            &Picture {
246                width,
247                height,
248                planes: &[&alpha],
249                cicp: (2, 2, 2),
250                full_range: true,
251            },
252            (qindex / 2).max(1),
253        )?),
254        None => None,
255    };
256    Ok(container(
257        width,
258        height,
259        planes.len() == 1,
260        &colour,
261        alpha.as_ref(),
262        icc,
263    ))
264}
265
266/// An ISOBMFF box.
267fn bx(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
268    let mut out = Vec::with_capacity(payload.len() + 8);
269    out.extend_from_slice(&(payload.len() as u32 + 8).to_be_bytes());
270    out.extend_from_slice(kind);
271    out.extend_from_slice(payload);
272    out
273}
274
275/// A full box: version and flags, then the payload.
276fn full(kind: &[u8; 4], version: u8, flags: u32, payload: &[u8]) -> Vec<u8> {
277    let mut body = vec![version];
278    body.extend_from_slice(&flags.to_be_bytes()[1..]);
279    body.extend_from_slice(payload);
280    bx(kind, &body)
281}
282
283/// `av1C` for a coded still (AV1-ISOBMFF ยง2.3.3).
284fn av1c(coded: &CodedStill, mono: bool) -> Vec<u8> {
285    let mut payload = vec![
286        0x81,               // marker, version 1
287        coded.level & 0x1F, // seq_profile 0, seq_level_idx_0
288        // tier 0, 8-bit, monochrome, 4:2:0 subsampling, sample position 0.
289        (u8::from(mono) << 4) | 0b1100,
290        0, // no initial_presentation_delay
291    ];
292    payload.extend_from_slice(&coded.sequence_header_obu);
293    bx(b"av1C", &payload)
294}
295
296/// The items' properties, in `ipco` order (1-based indices for `ipma`).
297fn container(
298    width: u32,
299    height: u32,
300    mono: bool,
301    colour: &CodedStill,
302    alpha: Option<&CodedStill>,
303    icc: Option<&[u8]>,
304) -> Vec<u8> {
305    let ftyp = bx(b"ftyp", b"avif\0\0\0\0avifmif1miaf");
306    let ispe = {
307        let mut p = width.to_be_bytes().to_vec();
308        p.extend_from_slice(&height.to_be_bytes());
309        full(b"ispe", 0, 0, &p)
310    };
311    let pixi = |channels: u8| {
312        let mut p = vec![channels];
313        p.extend(std::iter::repeat_n(8, usize::from(channels)));
314        full(b"pixi", 0, 0, &p)
315    };
316    let colr = {
317        let mut p = b"nclx".to_vec();
318        for v in [CICP.0, CICP.1, CICP.2] {
319            p.extend_from_slice(&u16::from(v).to_be_bytes());
320        }
321        p.push(0x80); // full_range_flag
322        bx(b"colr", &p)
323    };
324    // ipco: 1 ispe, 2 pixi, 3 av1C, 4 colr; alpha adds 5 pixi, 6 av1C, 7 auxC;
325    // a profile adds its colr last.
326    let mut ipco = [
327        ispe,
328        pixi(if mono { 1 } else { 3 }),
329        av1c(colour, mono),
330        colr,
331    ]
332    .concat();
333    let mut associations: Vec<(u16, Vec<u8>)> = vec![(1, vec![1, 2, 0x80 | 3, 4])];
334    if let Some(alpha) = alpha {
335        let mut urn = crate::meta::URN_ALPHA.as_bytes().to_vec();
336        urn.push(0);
337        ipco.extend_from_slice(&[pixi(1), av1c(alpha, true), full(b"auxC", 0, 0, &urn)].concat());
338        associations.push((2, vec![1, 5, 0x80 | 6, 7]));
339    }
340    // An ICC profile is a second `colr` on the colour item, next to the
341    // nclx, which still says how to turn the YUV into RGB.
342    if let Some(profile) = icc {
343        ipco.extend_from_slice(&bx(b"colr", &[b"prof".as_slice(), profile].concat()));
344        let index = if alpha.is_some() { 8 } else { 5 };
345        if let Some((_, props)) = associations.first_mut() {
346            props.push(index);
347        }
348    }
349    let ipma = {
350        let mut p = (associations.len() as u32).to_be_bytes().to_vec();
351        for (item, props) in &associations {
352            p.extend_from_slice(&item.to_be_bytes());
353            p.push(props.len() as u8);
354            p.extend_from_slice(props);
355        }
356        full(b"ipma", 0, 0, &p)
357    };
358    let iprp = bx(b"iprp", &[bx(b"ipco", &ipco), ipma].concat());
359
360    let hdlr = full(b"hdlr", 0, 0, b"\0\0\0\0pict\0\0\0\0\0\0\0\0\0\0\0\0\0");
361    let pitm = full(b"pitm", 0, 0, &1_u16.to_be_bytes());
362    let infe = |item: u16| {
363        let mut p = item.to_be_bytes().to_vec();
364        p.extend_from_slice(&[0, 0]);
365        p.extend_from_slice(b"av01");
366        p.push(0); // item_name
367        full(b"infe", 2, 0, &p)
368    };
369    let items: Vec<&CodedStill> = std::iter::once(colour).chain(alpha).collect();
370    let iinf = {
371        let mut p = (items.len() as u16).to_be_bytes().to_vec();
372        for i in 0..items.len() {
373            p.extend_from_slice(&infe(i as u16 + 1));
374        }
375        full(b"iinf", 0, 0, &p)
376    };
377    // auxl: the alpha item (2) is auxiliary to the colour item (1).
378    let iref = alpha.map(|_| full(b"iref", 0, 0, &bx(b"auxl", &[0, 2, 0, 1, 0, 1])));
379    // iloc v0 with 4-byte offsets and lengths, no base offset.
380    let iloc = |mdat_start: u32| {
381        let mut p = vec![0x44, 0x00];
382        p.extend_from_slice(&(items.len() as u16).to_be_bytes());
383        let mut offset = mdat_start;
384        for (i, item) in items.iter().enumerate() {
385            p.extend_from_slice(&(i as u16 + 1).to_be_bytes());
386            p.extend_from_slice(&[0, 0, 0, 1]); // data_reference_index, extent_count
387            p.extend_from_slice(&offset.to_be_bytes());
388            p.extend_from_slice(&(item.data.len() as u32).to_be_bytes());
389            offset += item.data.len() as u32;
390        }
391        full(b"iloc", 0, 0, &p)
392    };
393    let meta = |mdat_start: u32| {
394        let mut p = [hdlr.clone(), pitm.clone(), iloc(mdat_start), iinf.clone()].concat();
395        if let Some(iref) = &iref {
396            p.extend_from_slice(iref);
397        }
398        p.extend_from_slice(&iprp);
399        full(b"meta", 0, 0, &p)
400    };
401    // The meta box's size does not depend on the offsets it records.
402    let mdat_start = (ftyp.len() + meta(0).len() + 8) as u32;
403    let mdat_payload: Vec<u8> = items
404        .iter()
405        .flat_map(|item| item.data.iter().copied())
406        .collect();
407    [ftyp, meta(mdat_start), bx(b"mdat", &mdat_payload)].concat()
408}
409
410#[cfg(test)]
411#[allow(
412    clippy::unwrap_used,
413    clippy::indexing_slicing,
414    reason = "tests operate on known-good values"
415)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn rgb_to_yuv_follows_bt601_full_range() {
421        let pixels = [255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0, 255];
422        let (planes, alpha) = to_planes(&pixels, PixelFormat::Rgb8, 2, 2);
423        assert!(alpha.is_none());
424        assert_eq!(planes[0], vec![255, 0, 76, 29]);
425        // Chroma of the four averaged: red's V and blue's U pull up.
426        let u = (((-76.245_f64) + 255.0 * (1.0 - 0.114)) / 1.772 / 4.0 + 128.0).round();
427        assert!(
428            (i32::from(planes[1][0]) - u as i32).abs() <= 1,
429            "{} vs {u}",
430            planes[1][0]
431        );
432        assert_eq!(planes[1].len(), 1);
433    }
434
435    #[test]
436    fn grey_keeps_one_plane_and_alpha_is_split_out() {
437        let pixels = [10, 200, 20, 255, 30, 0];
438        let (planes, alpha) = to_planes(&pixels, PixelFormat::GrayA8, 3, 1);
439        assert_eq!(planes, vec![vec![10, 20, 30]]);
440        assert_eq!(alpha.unwrap(), vec![200, 255, 0]);
441    }
442
443    #[test]
444    fn odd_sizes_average_only_the_samples_present() {
445        let pixels = [0, 0, 255, 0, 0, 255, 0, 0, 255];
446        let (planes, _) = to_planes(&pixels, PixelFormat::Rgb8, 3, 1);
447        assert_eq!((planes[1].len(), planes[2].len()), (2, 2));
448        assert_eq!(planes[1][0], planes[1][1]);
449    }
450}