Skip to main content

otf_pixels_codec_avif/
decoder.rs

1//! The AVIF decoder: container parsing and the [`Decoder`] implementation.
2//!
3//! [`AvifDecoder::new`] parses only boxes, so `metadata()` stays a no-decode
4//! operation (SPEC §Guarantees 3): it answers dimensions, bit depth, chroma
5//! format, and alpha without touching the bitstream. [`AvifDecoder::read_row`]
6//! reconstructs the whole primary frame on the first call and serves rows from
7//! the cache — an AVIF still is one AV1 key frame with no prefix that yields a
8//! partial raster, so the decode is inherently whole-image (SPEC §Memory).
9//!
10//! The AV1 reconstruction covers the intra still, in any tiling, in 4:4:4, 4:2:2
11//! and 4:2:0 at 8, 10 and 12 bits, and the raster conversion handles the
12//! identity matrix, the BT.601/709/2020 YUV matrices and YCgCo at full and studio
13//! range (`yuv.rs`), to `Rgb8` or — for 10/12-bit — full-range `Rgb16`.
14//! Monochrome pictures decode to grey, and an alpha auxiliary item — a second
15//! AV1 image — becomes the alpha channel, un-premultiplying the colour when a
16//! `prem` reference says it was premultiplied.
17//! Every AV1 intra coding tool an encoder uses for stills is decoded —
18//! segmentation, delta-q/delta-lf, quantizer matrices, any tiling. Anything
19//! outside that (other matrices, intra block copy, grids,
20//! film grain) is reported as [`PixelsError::Unsupported`] rather than decoded
21//! wrong.
22
23use crate::boxes::{FourCc, Reader};
24use crate::meta::Meta;
25use crate::props::{Av1Config, Colour, Subsampling};
26use crate::yuv::{Depths, Layout, YuvMatrix, identity_to_rgb, plane_to_grey, yuv_to_rgb};
27use otf_pixels_core::{
28    Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation, PixelFormat,
29    PixelsError, Result, Source,
30};
31
32/// The most container bytes read before a file is called hostile.
33///
34/// The container addresses its payload by absolute file offset, so the whole
35/// file must be resident before any of it can be interpreted and there is no
36/// bound derivable from the image dimensions: a small `meta` can be followed
37/// by an unbounded `mdat`. `max_pixels` bounds the output; this bounds the
38/// input, exactly as the WebP decoder does for the same reason.
39const MAX_CONTAINER: usize = 256 * 1024 * 1024;
40
41/// What the container says about the primary image.
42///
43/// Everything here comes from boxes, so it is available without touching the
44/// AV1 bitstream.
45#[derive(Debug, Clone)]
46pub struct AvifInfo {
47    /// The primary item's dimensions, from its `ispe`.
48    pub width: u32,
49    /// See [`AvifInfo::width`].
50    pub height: u32,
51    /// The AV1 configuration of the primary item, or of its first tile when
52    /// the primary item is a grid.
53    pub config: Av1Config,
54    /// Whether the file carries an alpha plane as an auxiliary item.
55    pub has_alpha: bool,
56    /// Whether the primary item is a derived grid rather than a single coded
57    /// image.
58    pub is_grid: bool,
59    /// The primary item's `colr` colour information, if it has any. Its `nclx`
60    /// matrix, when present, overrides the one in the AV1 sequence header.
61    pub colour: Option<Colour>,
62    /// The primary item's ICC profile, from an ICC `colr`.
63    pub icc: Option<Vec<u8>>,
64    /// The primary item's `irot`/`imir` transform. Reported through
65    /// [`Decoder::orientation`], never applied to the decoded rows.
66    pub orientation: Orientation,
67}
68
69/// An alpha plane's coded image: a second AV1 still, usually monochrome.
70#[derive(Debug)]
71struct AlphaItem {
72    /// Its coded AV1 bytes.
73    frame_data: Vec<u8>,
74    /// Its `av1C` configuration OBUs.
75    config_obus: Vec<u8>,
76    /// Whether the colour is premultiplied by it (a `prem` reference from the
77    /// colour item to this one).
78    premultiplied: bool,
79}
80
81/// Decodes an AVIF stream.
82#[derive(Debug)]
83pub struct AvifDecoder {
84    descriptor: ImageDescriptor,
85    info: AvifInfo,
86    /// The primary item's coded AV1 bytes, retained for the lazy pixel decode.
87    /// `None` for a grid, whose per-tile decode is not implemented.
88    frame_data: Option<Vec<u8>>,
89    /// The alpha auxiliary item, when there is one and it can be decoded.
90    alpha: Option<AlphaItem>,
91    /// The reconstructed interleaved raster, produced on the first row read.
92    raster: Option<Vec<u8>>,
93    /// Rows already served.
94    row: u32,
95}
96
97impl AvifDecoder {
98    /// Read the container and describe the primary image.
99    ///
100    /// Decodes no pixels: this parses boxes only, which is what makes
101    /// `metadata()` free.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`PixelsError::Malformed`] for a stream that is not a
106    /// structurally valid AVIF, [`PixelsError::Unsupported`] for a valid file
107    /// using a feature this decoder does not implement, or
108    /// [`PixelsError::LimitExceeded`] if the image exceeds `limits`.
109    pub fn new<S: Source>(source: S, limits: Limits) -> Result<Self> {
110        let bytes = read_all(source)?;
111        let info = parse_container(&bytes)?;
112        let (frame_data, alpha) = if info.is_grid {
113            (None, None)
114        } else {
115            (Some(locate_primary_frame(&bytes)?), locate_alpha(&bytes)?)
116        };
117        let pixel = pixel_format(&info);
118        let descriptor = ImageDescriptor::with_limits(info.width, info.height, pixel, &limits)?;
119        Ok(Self {
120            descriptor,
121            info,
122            frame_data,
123            alpha,
124            raster: None,
125            row: 0,
126        })
127    }
128
129    /// What the container said about this image.
130    #[must_use]
131    pub const fn info(&self) -> &AvifInfo {
132        &self.info
133    }
134}
135
136/// Drain `source` into a bounded buffer.
137fn read_all<S: Source>(mut source: S) -> Result<Vec<u8>> {
138    let mut bytes = Vec::new();
139    let mut chunk = [0_u8; 64 * 1024];
140    loop {
141        if bytes.len() > MAX_CONTAINER {
142            return Err(PixelsError::malformed(
143                "avif",
144                format!("stream exceeds {MAX_CONTAINER} bytes"),
145            ));
146        }
147        match source.read(&mut chunk)? {
148            0 => break,
149            read => {
150                let Some(filled) = chunk.get(..read) else {
151                    break;
152                };
153                bytes.extend_from_slice(filled);
154            }
155        }
156    }
157    Ok(bytes)
158}
159
160/// Parse the container down to the facts about the primary image.
161fn parse_container(bytes: &[u8]) -> Result<AvifInfo> {
162    let mut reader = Reader::new(bytes);
163
164    // The first box must be `ftyp`. Checking it here rather than trusting
165    // sniffing means a decoder handed bytes directly still validates them.
166    let first = reader
167        .next_box()
168        .ok_or_else(|| PixelsError::malformed("avif", "the file holds no boxes"))??;
169    if first.kind != FourCc::new(b"ftyp") {
170        return Err(PixelsError::malformed(
171            "avif",
172            format!("the file begins with '{}' rather than 'ftyp'", first.kind),
173        ));
174    }
175    if !probe(bytes) {
176        return Err(PixelsError::malformed(
177            "avif",
178            "the ftyp box declares no brand this decoder recognises",
179        ));
180    }
181
182    let meta_reader = reader
183        .find(b"meta")?
184        .ok_or_else(|| PixelsError::malformed("avif", "the file has no meta box"))?;
185    let meta = Meta::parse(meta_reader)?;
186
187    let primary = meta
188        .primary_item()
189        .ok_or_else(|| PixelsError::malformed("avif", "the file names no primary image item"))?;
190
191    // An essential property we cannot interpret was declared to change how the
192    // pixels are to be read, so decoding anyway would produce a confidently
193    // wrong image.
194    if let Some(kind) = meta.properties.essential_unknown(primary.id) {
195        return Err(PixelsError::unsupported(format!(
196            "avif: the primary item requires the '{kind}' property, which this decoder does not implement"
197        )));
198    }
199
200    if !primary.is_coded_image() && !primary.is_grid() {
201        return Err(PixelsError::unsupported(format!(
202            "avif: the primary item has type '{}', which is not an image this decoder produces",
203            primary.kind
204        )));
205    }
206
207    let extents = meta.properties.extents(primary.id).ok_or_else(|| {
208        PixelsError::malformed(
209            "avif",
210            format!(
211                "item {} has no ispe property, so its size is unknown",
212                primary.id
213            ),
214        )
215    })?;
216
217    // A grid's own configuration lives on its tiles: the grid item has no
218    // coded data of its own, only a dimg reference to the items that do.
219    let config_item = if primary.is_grid() {
220        *meta
221            .referenced(primary.id, b"dimg")
222            .first()
223            .ok_or_else(|| {
224                PixelsError::malformed(
225                    "avif",
226                    format!("grid item {} references no tiles", primary.id),
227                )
228            })?
229    } else {
230        primary.id
231    };
232
233    let config = meta
234        .properties
235        .av1_config(config_item)
236        .ok_or_else(|| {
237            PixelsError::malformed(
238                "avif",
239                format!("item {config_item} has no av1C property, so it is not a coded AV1 image"),
240            )
241        })?
242        .clone();
243
244    Ok(AvifInfo {
245        width: extents.width,
246        height: extents.height,
247        config,
248        has_alpha: meta.alpha_item(primary.id).is_some(),
249        is_grid: primary.is_grid(),
250        colour: meta.properties.colour(primary.id).cloned(),
251        icc: meta.properties.icc_profile(primary.id).map(<[u8]>::to_vec),
252        orientation: meta.properties.orientation(primary.id),
253    })
254}
255
256/// Locate and copy out the primary coded image item's AV1 bytes.
257fn locate_primary_frame(bytes: &[u8]) -> Result<Vec<u8>> {
258    let mut reader = Reader::new(bytes);
259    reader.next_box();
260    let meta_reader = reader
261        .find(b"meta")?
262        .ok_or_else(|| PixelsError::malformed("avif", "the file has no meta box"))?;
263    let meta = Meta::parse(meta_reader)?;
264    let primary = meta
265        .primary_item()
266        .ok_or_else(|| PixelsError::malformed("avif", "the file names no primary image item"))?;
267    Ok(meta.item_data(bytes, primary)?.into_owned())
268}
269
270/// Locate the primary item's alpha auxiliary item, if it has one.
271fn locate_alpha(bytes: &[u8]) -> Result<Option<AlphaItem>> {
272    let mut reader = Reader::new(bytes);
273    reader.next_box();
274    let meta_reader = reader
275        .find(b"meta")?
276        .ok_or_else(|| PixelsError::malformed("avif", "the file has no meta box"))?;
277    let meta = Meta::parse(meta_reader)?;
278    let primary = meta
279        .primary_item()
280        .ok_or_else(|| PixelsError::malformed("avif", "the file names no primary image item"))?;
281    let Some(alpha) = meta.alpha_item(primary.id) else {
282        return Ok(None);
283    };
284    let config = meta.properties.av1_config(alpha.id).ok_or_else(|| {
285        PixelsError::malformed(
286            "avif",
287            format!("alpha item {} has no av1C property", alpha.id),
288        )
289    })?;
290    Ok(Some(AlphaItem {
291        frame_data: meta.item_data(bytes, alpha)?.into_owned(),
292        config_obus: config.config_obus.clone(),
293        // `prem` runs from the colour (master) item to its alpha item.
294        premultiplied: meta.referenced(primary.id, b"prem").contains(&alpha.id),
295    }))
296}
297
298/// Decode one coded AV1 still to its sample planes and sequence header.
299fn decode_frame(
300    config_obus: &[u8],
301    frame_data: &[u8],
302) -> Result<(crate::av1::DecodedFrame, crate::av1::SequenceHeader)> {
303    use crate::av1::{StillPicture, decode_still};
304
305    let still = StillPicture::parse(config_obus, frame_data)?;
306    let groups = still.tile_group_data(frame_data)?;
307    let frame = decode_still(&still.sequence, &still.frame, &groups)?;
308    Ok((frame, still.sequence))
309}
310
311/// Decode the primary AV1 still and convert it to the interleaved raster the
312/// descriptor promises.
313fn decode_raster(
314    info: &AvifInfo,
315    pixel: PixelFormat,
316    frame_data: &[u8],
317    alpha: Option<&AlphaItem>,
318) -> Result<Vec<u8>> {
319    let (frame, sequence) = decode_frame(&info.config.config_obus, frame_data)?;
320    let color = &sequence.color;
321    let layout = Layout {
322        width: info.width as usize,
323        height: info.height as usize,
324        subsampling_x: usize::from(color.subsampling_x),
325        subsampling_y: usize::from(color.subsampling_y),
326    };
327    let depths = Depths::for_input(color.bit_depth);
328    let (colour, channels) = if color.mono_chrome {
329        let luma = frame
330            .planes
331            .first()
332            .ok_or_else(|| PixelsError::malformed("avif", "the luma plane is missing"))?;
333        (plane_to_grey(luma, layout, depths, color.color_range), 1)
334    } else {
335        (
336            colour_to_rgb(info, &frame.planes, color, layout, depths)?,
337            3,
338        )
339    };
340    let mut colour = colour;
341    let alpha = match (info.has_alpha, alpha) {
342        (false, _) => None,
343        (true, Some(item)) => {
344            let samples = decode_alpha(item, layout, depths)?;
345            if item.premultiplied {
346                unpremultiply(&mut colour, channels, &samples, depths.output_max());
347            }
348            Some(samples)
349        }
350        // The container declares alpha but it could not be located (a grid's
351        // alpha, say); the descriptor promises it, so this cannot continue.
352        (true, None) => {
353            return Err(PixelsError::unsupported(
354                "avif: this file's alpha plane is not decodable yet",
355            ));
356        }
357    };
358    Ok(pack(pixel, &colour, channels, alpha.as_deref()))
359}
360
361/// Convert a colour (non-monochrome) frame's planes to RGB samples.
362fn colour_to_rgb(
363    info: &AvifInfo,
364    planes: &[crate::av1::Plane],
365    color: &crate::av1::ColorConfig,
366    layout: Layout,
367    depths: Depths,
368) -> Result<Vec<u16>> {
369    // The container's nclx, when present, is what the file declares; the
370    // sequence header is the fallback. The range is the sequence header's:
371    // it is what governs the decoded samples.
372    let matrix = match &info.colour {
373        Some(Colour::Nclx { matrix, .. }) => *matrix,
374        _ => u16::from(color.matrix_coefficients),
375    };
376    if matrix != 0 {
377        let yuv = YuvMatrix::new(matrix, color.color_range, depths)?;
378        return yuv_to_rgb(planes, layout, &yuv);
379    }
380    if (layout.subsampling_x, layout.subsampling_y) != (0, 0) {
381        return Err(PixelsError::malformed(
382            "avif",
383            "the identity colour matrix requires 4:4:4, but the chroma is subsampled",
384        ));
385    }
386    identity_to_rgb(planes, layout, depths)
387}
388
389/// Decode the alpha item to samples at the colour's output depth. Alpha is
390/// its image's luma — any chroma it codes is ignored — expanded to full range
391/// if the item was coded at studio range.
392fn decode_alpha(item: &AlphaItem, layout: Layout, colour_depths: Depths) -> Result<Vec<u16>> {
393    let (frame, sequence) = decode_frame(&item.config_obus, &item.frame_data)?;
394    let luma = frame
395        .planes
396        .first()
397        .ok_or_else(|| PixelsError::malformed("avif", "the alpha plane is missing"))?;
398    let depths = Depths {
399        input: sequence.color.bit_depth,
400        output: colour_depths.output,
401    };
402    let alpha_layout = Layout {
403        subsampling_x: 0,
404        subsampling_y: 0,
405        ..layout
406    };
407    Ok(plane_to_grey(
408        luma,
409        alpha_layout,
410        depths,
411        sequence.color.color_range,
412    ))
413}
414
415/// Turn colour premultiplied by `alpha` (a `prem` reference) back into the
416/// straight colour the API carries (SPEC §Pixel formats), as libavif does:
417/// each sample scaled by `max / alpha`, rounded and capped at `max`, and
418/// fully transparent pixels black. Precision lost to premultiplication at
419/// low alpha stays lost.
420fn unpremultiply(colour: &mut [u16], channels: usize, alpha: &[u16], max: i64) {
421    for (px, &a) in colour.chunks_exact_mut(channels.max(1)).zip(alpha) {
422        let a = i64::from(a);
423        if a >= max {
424            continue;
425        }
426        for sample in px {
427            *sample = if a == 0 {
428                0
429            } else {
430                ((i64::from(*sample) * max * 2 + a) / (2 * a)).min(max) as u16
431            };
432        }
433    }
434}
435
436/// Interleave colour samples (`channels` per pixel: 1 grey or 3 RGB) and
437/// optional alpha into the raster `pixel` describes — one byte per sample for
438/// the 8-bit formats, two native-endian bytes for the 16-bit ones. Grey with
439/// alpha at 16 bits has no format of its own (SPEC §Pixel formats), so it is
440/// widened to RGBA.
441fn pack(pixel: PixelFormat, colour: &[u16], channels: usize, alpha: Option<&[u16]>) -> Vec<u8> {
442    let wide = matches!(
443        pixel,
444        PixelFormat::Rgb16 | PixelFormat::Rgba16 | PixelFormat::Gray16
445    );
446    let out_colour = match pixel {
447        PixelFormat::Gray8 | PixelFormat::Gray16 | PixelFormat::GrayA8 => 1,
448        _ => 3,
449    };
450    let pixels = colour.len() / channels.max(1);
451    let mut raster = Vec::with_capacity(pixels * (out_colour + 1) * if wide { 2 } else { 1 });
452    let mut put = |v: u16| {
453        if wide {
454            raster.extend_from_slice(&v.to_ne_bytes());
455        } else {
456            raster.push(v as u8);
457        }
458    };
459    for (i, px) in colour.chunks_exact(channels.max(1)).enumerate() {
460        for c in 0..out_colour {
461            // Grey widened to RGB repeats its one sample.
462            put(px.get(c).or_else(|| px.first()).copied().unwrap_or(0));
463        }
464        if let Some(alpha) = alpha {
465            put(alpha.get(i).copied().unwrap_or(0));
466        }
467    }
468    raster
469}
470
471/// The pixel format this image decodes to.
472///
473/// AV1 codes YUV; the engine's formats are RGB and greyscale, so the mapping
474/// happens here and the colour conversion happens at decode. Ten- and
475/// twelve-bit samples widen to sixteen because SPEC §Pixel formats has no
476/// narrower wide type, and they are rescaled to its full 0..=65535 range —
477/// the conversion computes RGB at 16 bits directly (`yuv.rs`).
478fn pixel_format(info: &AvifInfo) -> PixelFormat {
479    let wide = info.config.bit_depth > 8;
480    let monochrome = info.config.subsampling == Subsampling::Monochrome;
481    match (monochrome, info.has_alpha, wide) {
482        (true, false, false) => PixelFormat::Gray8,
483        (true, false, true) => PixelFormat::Gray16,
484        (true, true, false) => PixelFormat::GrayA8,
485        // SPEC §Pixel formats has no GrayA16, so wide greyscale with alpha
486        // widens to RGBA rather than silently dropping either the alpha or
487        // the low bits.
488        (true, true, true) => PixelFormat::Rgba16,
489        (false, false, false) => PixelFormat::Rgb8,
490        (false, false, true) => PixelFormat::Rgb16,
491        (false, true, false) => PixelFormat::Rgba8,
492        (false, true, true) => PixelFormat::Rgba16,
493    }
494}
495
496impl Decoder for AvifDecoder {
497    fn descriptor(&self) -> ImageDescriptor {
498        self.descriptor
499    }
500
501    fn orientation(&self) -> Orientation {
502        self.info.orientation
503    }
504
505    fn icc_profile(&self) -> Option<&[u8]> {
506        self.info.icc.as_deref()
507    }
508
509    fn capability(&self) -> DecodeCapability {
510        // A grid AVIF stores independently coded tiles and could serve
511        // regions, but this decoder does not yet implement `read_region`.
512        // Claiming `Regions` before it does would be a lie the scheduler acts
513        // on, which is the mistake `codec.rs` records having made once for
514        // JPEG's scaled decode.
515        DecodeCapability::Sequential
516    }
517
518    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
519        // Validate the call even though it cannot yet be served, so that the
520        // contract is already enforced when the bitstream decoder lands.
521        if self.row >= self.descriptor.height {
522            return Err(PixelsError::invalid_argument(
523                "out",
524                format!("all {} rows have already been read", self.descriptor.height),
525            ));
526        }
527        let row_bytes = self.descriptor.row_bytes();
528        if out.len() != row_bytes {
529            return Err(PixelsError::invalid_argument(
530                "out",
531                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
532            ));
533        }
534
535        // Reconstruct the whole frame on the first row: an AVIF still is coded
536        // as one AV1 frame with no prefix that yields a partial raster, so the
537        // decode is inherently whole-image (SPEC §Memory). Later rows are served
538        // from the cached raster.
539        if self.raster.is_none() {
540            let frame_data = self
541                .frame_data
542                .as_deref()
543                .ok_or_else(|| PixelsError::unsupported("avif: grid images are not decoded yet"))?;
544            self.raster = Some(decode_raster(
545                &self.info,
546                self.descriptor.pixel,
547                frame_data,
548                self.alpha.as_ref(),
549            )?);
550        }
551        let raster = self.raster.as_deref().unwrap_or(&[]);
552        let start = self.row as usize * row_bytes;
553        let src = raster
554            .get(start..start + row_bytes)
555            .ok_or_else(|| PixelsError::malformed("avif", "the reconstructed raster is short"))?;
556        out.copy_from_slice(src);
557        self.row += 1;
558        Ok(())
559    }
560}
561
562/// Whether `prefix` starts with an ISOBMFF file declaring a brand this
563/// decoder recognises.
564///
565/// Detection is by magic bytes only (SPEC §Formats). `ftyp` at offset 4 marks
566/// the whole ISOBMFF family, which also holds MP4 and HEIC, so the brands are
567/// what actually identify an AVIF. The major brand alone is not enough: many
568/// encoders write `mif1` as the major brand and put `avif` in the compatible
569/// list, so both are scanned.
570#[must_use]
571pub fn probe(prefix: &[u8]) -> bool {
572    if prefix.get(4..8) != Some(&crate::SIGNATURE_FTYP[..]) {
573        return false;
574    }
575    let Some(brands) = prefix.get(8..) else {
576        return false;
577    };
578    // Words from offset 8: index 0 is the major brand, index 1 is the minor
579    // version — a number, not a brand — and the rest are compatible brands.
580    brands
581        .chunks_exact(4)
582        .enumerate()
583        .filter(|(index, _)| *index != 1)
584        .any(|(_, brand)| is_known_brand(brand))
585}
586
587/// Whether these four bytes name a brand this decoder claims.
588fn is_known_brand(brand: &[u8]) -> bool {
589    crate::BRANDS_STILL
590        .iter()
591        .chain(core::iter::once(&crate::BRAND_SEQUENCE))
592        .any(|known| known == brand)
593}
594
595/// The AVIF entry in a sniffing registry.
596#[derive(Debug, Clone, Copy, Default)]
597pub struct AvifCodec;
598
599impl Codec for AvifCodec {
600    fn format(&self) -> Format {
601        Format::Avif
602    }
603
604    fn magic_len(&self) -> usize {
605        // Enough for `ftyp`, the major brand, the minor version and four
606        // compatible brands, which covers every file this has been tried on.
607        // `probe` reads whatever it is given, so a shorter prefix still works
608        // when the brand appears early.
609        32
610    }
611
612    fn probe(&self, prefix: &[u8]) -> bool {
613        probe(prefix)
614    }
615}
616
617#[cfg(test)]
618#[allow(
619    clippy::unwrap_used,
620    clippy::indexing_slicing,
621    reason = "tests operate on known-good values and assert shapes directly"
622)]
623mod tests {
624    use super::*;
625
626    #[test]
627    fn unpremultiplying_restores_straight_colour() {
628        // RGB per pixel: opaque (untouched), half (doubled, rounded, capped),
629        // transparent (black), and a 16-bit sample.
630        let mut colour = vec![10, 20, 30, 50, 64, 200, 9, 9, 9];
631        unpremultiply(&mut colour, 3, &[255, 128, 0], 255);
632        assert_eq!(colour, vec![10, 20, 30, 100, 128, 255, 0, 0, 0]);
633        let mut wide = vec![1000, 30000, 65535];
634        unpremultiply(&mut wide, 3, &[32768], 65535);
635        // 30000 * 65535 / 32768 = 59999.08.
636        assert_eq!(wide, vec![2000, 59999, 65535]);
637        // Grey with alpha: one colour channel.
638        let mut grey = vec![60, 255];
639        unpremultiply(&mut grey, 1, &[120, 255], 255);
640        assert_eq!(grey, vec![128, 255]);
641    }
642    use otf_pixels_core::ErrorCode;
643
644    fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
645        let mut out = Vec::new();
646        let total = u32::try_from(8 + payload.len()).unwrap();
647        out.extend_from_slice(&total.to_be_bytes());
648        out.extend_from_slice(kind);
649        out.extend_from_slice(payload);
650        out
651    }
652
653    fn ftyp(major: &[u8; 4], compatible: &[&[u8; 4]]) -> Vec<u8> {
654        let mut payload = Vec::from(*major);
655        payload.extend_from_slice(&0_u32.to_be_bytes());
656        for brand in compatible {
657            payload.extend_from_slice(*brand);
658        }
659        boxed(b"ftyp", &payload)
660    }
661
662    /// An `av1C` for 8-bit 4:2:0, profile 0.
663    fn av1c() -> Vec<u8> {
664        boxed(b"av1C", &[0x81, 0x00, 0x0C, 0x00])
665    }
666
667    fn ispe(width: u32, height: u32) -> Vec<u8> {
668        let mut payload = vec![0, 0, 0, 0];
669        payload.extend_from_slice(&width.to_be_bytes());
670        payload.extend_from_slice(&height.to_be_bytes());
671        boxed(b"ispe", &payload)
672    }
673
674    fn infe(id: u16, kind: &[u8; 4], hidden: bool) -> Vec<u8> {
675        let mut payload = vec![2, 0, 0, u8::from(hidden)];
676        payload.extend_from_slice(&id.to_be_bytes());
677        payload.extend_from_slice(&[0, 0]);
678        payload.extend_from_slice(kind);
679        payload.push(0);
680        boxed(b"infe", &payload)
681    }
682
683    fn iinf(entries: &[Vec<u8>]) -> Vec<u8> {
684        let mut payload = vec![0, 0, 0, 0];
685        payload.extend_from_slice(&u16::try_from(entries.len()).unwrap().to_be_bytes());
686        for entry in entries {
687            payload.extend_from_slice(entry);
688        }
689        boxed(b"iinf", &payload)
690    }
691
692    fn iloc(rows: &[(u16, u32, u32)]) -> Vec<u8> {
693        let mut payload = vec![0, 0, 0, 0, 0x44, 0x00];
694        payload.extend_from_slice(&u16::try_from(rows.len()).unwrap().to_be_bytes());
695        for (id, offset, length) in rows {
696            payload.extend_from_slice(&id.to_be_bytes());
697            payload.extend_from_slice(&[0, 0]);
698            payload.extend_from_slice(&1_u16.to_be_bytes());
699            payload.extend_from_slice(&offset.to_be_bytes());
700            payload.extend_from_slice(&length.to_be_bytes());
701        }
702        boxed(b"iloc", &payload)
703    }
704
705    fn pitm(id: u16) -> Vec<u8> {
706        let mut payload = vec![0, 0, 0, 0];
707        payload.extend_from_slice(&id.to_be_bytes());
708        boxed(b"pitm", &payload)
709    }
710
711    /// An `ipma` associating each `(item, [properties])` pair, all
712    /// non-essential.
713    fn ipma(rows: &[(u16, &[u8])]) -> Vec<u8> {
714        let mut payload = vec![0, 0, 0, 0];
715        payload.extend_from_slice(&u32::try_from(rows.len()).unwrap().to_be_bytes());
716        for (item, properties) in rows {
717            payload.extend_from_slice(&item.to_be_bytes());
718            payload.push(u8::try_from(properties.len()).unwrap());
719            payload.extend_from_slice(properties);
720        }
721        boxed(b"ipma", &payload)
722    }
723
724    fn iprp(properties: &[Vec<u8>], associations: &[(u16, &[u8])]) -> Vec<u8> {
725        let mut ipco = Vec::new();
726        for property in properties {
727            ipco.extend_from_slice(property);
728        }
729        let mut payload = boxed(b"ipco", &ipco);
730        payload.extend_from_slice(&ipma(associations));
731        boxed(b"iprp", &payload)
732    }
733
734    fn meta_box(children: &[Vec<u8>]) -> Vec<u8> {
735        let mut payload = vec![0, 0, 0, 0];
736        for child in children {
737            payload.extend_from_slice(child);
738        }
739        boxed(b"meta", &payload)
740    }
741
742    /// A minimal single-item AVIF: one `av01` item, `ispe` then `av1C`.
743    fn minimal_avif(width: u32, height: u32) -> Vec<u8> {
744        let mut file = ftyp(b"avif", &[b"mif1"]);
745        file.extend_from_slice(&meta_box(&[
746            pitm(1),
747            iinf(&[infe(1, b"av01", false)]),
748            iloc(&[(1, 0, 1)]),
749            iprp(&[ispe(width, height), av1c()], &[(1, &[1, 2])]),
750        ]));
751        file.extend_from_slice(&boxed(b"mdat", &[0]));
752        file
753    }
754
755    #[test]
756    fn reads_dimensions_without_decoding_pixels() {
757        let file = minimal_avif(320, 240);
758        let decoder = AvifDecoder::new(&file[..], Limits::default()).unwrap();
759        let descriptor = decoder.descriptor();
760        assert_eq!((descriptor.width, descriptor.height), (320, 240));
761        assert_eq!(descriptor.pixel, PixelFormat::Rgb8);
762        assert_eq!(decoder.info().config.bit_depth, 8);
763        assert_eq!(decoder.info().config.subsampling, Subsampling::Yuv420);
764        assert!(!decoder.info().has_alpha);
765        assert!(!decoder.info().is_grid);
766    }
767
768    #[test]
769    fn an_alpha_auxiliary_item_widens_the_pixel_format() {
770        let mut auxl = Vec::new();
771        auxl.extend_from_slice(&2_u16.to_be_bytes()); // from the alpha item
772        auxl.extend_from_slice(&1_u16.to_be_bytes());
773        auxl.extend_from_slice(&1_u16.to_be_bytes()); // to the colour item
774        let mut iref_payload = vec![0, 0, 0, 0];
775        iref_payload.extend_from_slice(&boxed(b"auxl", &auxl));
776
777        let mut auxc_payload = vec![0, 0, 0, 0];
778        auxc_payload.extend_from_slice(crate::meta::URN_ALPHA.as_bytes());
779        auxc_payload.push(0);
780
781        let mut file = ftyp(b"avif", &[]);
782        file.extend_from_slice(&meta_box(&[
783            pitm(1),
784            iinf(&[infe(1, b"av01", false), infe(2, b"av01", true)]),
785            iloc(&[(1, 0, 1), (2, 0, 1)]),
786            boxed(b"iref", &iref_payload),
787            iprp(
788                &[ispe(8, 8), av1c(), boxed(b"auxC", &auxc_payload)],
789                &[(1, &[1, 2]), (2, &[1, 2, 3])],
790            ),
791        ]));
792
793        let decoder = AvifDecoder::new(&file[..], Limits::default()).unwrap();
794        assert!(decoder.info().has_alpha);
795        assert_eq!(decoder.descriptor().pixel, PixelFormat::Rgba8);
796    }
797
798    #[test]
799    fn a_grid_takes_its_configuration_from_its_first_tile() {
800        let mut dimg = Vec::new();
801        dimg.extend_from_slice(&1_u16.to_be_bytes()); // from the grid
802        dimg.extend_from_slice(&2_u16.to_be_bytes()); // two tiles
803        dimg.extend_from_slice(&2_u16.to_be_bytes());
804        dimg.extend_from_slice(&3_u16.to_be_bytes());
805        let mut iref_payload = vec![0, 0, 0, 0];
806        iref_payload.extend_from_slice(&boxed(b"dimg", &dimg));
807
808        let mut file = ftyp(b"avif", &[]);
809        file.extend_from_slice(&meta_box(&[
810            pitm(1),
811            iinf(&[
812                infe(1, b"grid", false),
813                infe(2, b"av01", true),
814                infe(3, b"av01", true),
815            ]),
816            iloc(&[(1, 0, 1), (2, 0, 1), (3, 0, 1)]),
817            boxed(b"iref", &iref_payload),
818            // The grid carries the full size; the tiles carry the av1C.
819            iprp(
820                &[ispe(128, 64), av1c(), ispe(64, 64)],
821                &[(1, &[1]), (2, &[3, 2]), (3, &[3, 2])],
822            ),
823        ]));
824
825        let decoder = AvifDecoder::new(&file[..], Limits::default()).unwrap();
826        assert!(decoder.info().is_grid);
827        assert_eq!(
828            (decoder.descriptor().width, decoder.descriptor().height),
829            (128, 64)
830        );
831        assert_eq!(decoder.info().config.bit_depth, 8);
832    }
833
834    #[test]
835    fn pack_interleaves_colour_and_alpha_in_the_formats_layout() {
836        // Grey + alpha at 8 bits: one byte each, grey first.
837        assert_eq!(
838            pack(PixelFormat::GrayA8, &[10, 20], 1, Some(&[200, 100])),
839            [10, 200, 20, 100]
840        );
841        // RGB + alpha at 8 bits.
842        assert_eq!(
843            pack(PixelFormat::Rgba8, &[1, 2, 3], 3, Some(&[4])),
844            [1, 2, 3, 4]
845        );
846        // Wide grey + alpha has no format of its own and widens to RGBA16:
847        // the grey sample repeats, native-endian.
848        let wide = pack(PixelFormat::Rgba16, &[0x1234], 1, Some(&[0xFFFF]));
849        let samples: Vec<u16> = wide
850            .chunks_exact(2)
851            .map(|b| u16::from_ne_bytes([b[0], b[1]]))
852            .collect();
853        assert_eq!(samples, [0x1234, 0x1234, 0x1234, 0xFFFF]);
854        // No alpha: grey stays one sample per pixel.
855        assert_eq!(pack(PixelFormat::Gray8, &[7, 8], 1, None), [7, 8]);
856    }
857
858    #[test]
859    fn monochrome_and_wide_samples_choose_the_right_pixel_format() {
860        fn format_for(av1c_bytes: [u8; 4], has_alpha: bool) -> PixelFormat {
861            let info = AvifInfo {
862                width: 1,
863                height: 1,
864                config: Av1Config {
865                    seq_profile: av1c_bytes[1] >> 5,
866                    seq_level_idx0: 0,
867                    seq_tier0: 0,
868                    bit_depth: 8,
869                    subsampling: Subsampling::Yuv420,
870                    chroma_sample_position: 0,
871                    config_obus: Vec::new(),
872                },
873                has_alpha,
874                is_grid: false,
875                colour: None,
876                icc: None,
877                orientation: Orientation::Normal,
878            };
879            pixel_format(&info)
880        }
881        assert_eq!(format_for([0x81, 0, 0x0C, 0], false), PixelFormat::Rgb8);
882        assert_eq!(format_for([0x81, 0, 0x0C, 0], true), PixelFormat::Rgba8);
883
884        let mono = |depth: u8, alpha: bool| {
885            pixel_format(&AvifInfo {
886                width: 1,
887                height: 1,
888                config: Av1Config {
889                    seq_profile: 0,
890                    seq_level_idx0: 0,
891                    seq_tier0: 0,
892                    bit_depth: depth,
893                    subsampling: Subsampling::Monochrome,
894                    chroma_sample_position: 0,
895                    config_obus: Vec::new(),
896                },
897                has_alpha: alpha,
898                is_grid: false,
899                colour: None,
900                icc: None,
901                orientation: Orientation::Normal,
902            })
903        };
904        assert_eq!(mono(8, false), PixelFormat::Gray8);
905        assert_eq!(mono(8, true), PixelFormat::GrayA8);
906        assert_eq!(mono(10, false), PixelFormat::Gray16);
907        // No GrayA16 exists, so this widens rather than dropping a channel.
908        assert_eq!(mono(12, true), PixelFormat::Rgba16);
909    }
910
911    #[test]
912    fn the_row_buffer_size_contract_is_enforced_before_decoding() {
913        let file = minimal_avif(4, 4);
914        let mut decoder = AvifDecoder::new(&file[..], Limits::default()).unwrap();
915
916        // A wrong-sized buffer is an argument error, checked before the decode
917        // is reached.
918        let mut wrong = [0_u8; 3];
919        assert_eq!(
920            decoder.read_row(&mut wrong).unwrap_err().code(),
921            ErrorCode::InvalidArgument
922        );
923
924        // The synthetic fixture carries no real coded frame, so the decode
925        // itself fails cleanly rather than panicking. (Real decodes are proven
926        // bit-exact against libavif in tests/reference.rs.)
927        let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
928        assert!(decoder.read_row(&mut row).is_err());
929    }
930
931    #[test]
932    fn dimensions_are_limited_before_anything_is_allocated() {
933        let file = minimal_avif(100_000, 100_000);
934        let error = AvifDecoder::new(&file[..], Limits::default()).unwrap_err();
935        assert_eq!(error.code(), ErrorCode::LimitExceeded);
936    }
937
938    #[test]
939    fn an_essential_property_we_do_not_understand_refuses_the_file() {
940        let mut file = ftyp(b"avif", &[]);
941        file.extend_from_slice(&meta_box(&[
942            pitm(1),
943            iinf(&[infe(1, b"av01", false)]),
944            iloc(&[(1, 0, 1)]),
945            // Property 3 is unknown and marked essential (high bit set).
946            iprp(
947                &[ispe(8, 8), av1c(), boxed(b"zzzz", &[0; 4])],
948                &[(1, &[1, 2, 0x83])],
949            ),
950        ]));
951
952        let error = AvifDecoder::new(&file[..], Limits::default()).unwrap_err();
953        assert_eq!(error.code(), ErrorCode::Unsupported);
954        assert!(error.to_string().contains("zzzz"), "{error}");
955    }
956
957    #[test]
958    fn a_file_without_the_boxes_an_image_needs_is_rejected() {
959        // No meta at all.
960        let mut file = ftyp(b"avif", &[]);
961        file.extend_from_slice(&boxed(b"mdat", &[0; 4]));
962        let error = AvifDecoder::new(&file[..], Limits::default()).unwrap_err();
963        assert_eq!(error.code(), ErrorCode::Malformed);
964        assert!(error.to_string().contains("no meta box"), "{error}");
965
966        // A meta with no items.
967        let mut file = ftyp(b"avif", &[]);
968        file.extend_from_slice(&meta_box(&[pitm(1)]));
969        let error = AvifDecoder::new(&file[..], Limits::default()).unwrap_err();
970        assert_eq!(error.code(), ErrorCode::Malformed);
971        assert!(
972            error.to_string().contains("no primary image item"),
973            "{error}"
974        );
975
976        // An item with no ispe, so no dimensions.
977        let mut file = ftyp(b"avif", &[]);
978        file.extend_from_slice(&meta_box(&[
979            pitm(1),
980            iinf(&[infe(1, b"av01", false)]),
981            iloc(&[(1, 0, 1)]),
982            iprp(&[av1c()], &[(1, &[1])]),
983        ]));
984        let error = AvifDecoder::new(&file[..], Limits::default()).unwrap_err();
985        assert_eq!(error.code(), ErrorCode::Malformed);
986        assert!(error.to_string().contains("no ispe"), "{error}");
987
988        // An item with no av1C, so it is not a coded AV1 image.
989        let mut file = ftyp(b"avif", &[]);
990        file.extend_from_slice(&meta_box(&[
991            pitm(1),
992            iinf(&[infe(1, b"av01", false)]),
993            iloc(&[(1, 0, 1)]),
994            iprp(&[ispe(8, 8)], &[(1, &[1])]),
995        ]));
996        let error = AvifDecoder::new(&file[..], Limits::default()).unwrap_err();
997        assert_eq!(error.code(), ErrorCode::Malformed);
998        assert!(error.to_string().contains("no av1C"), "{error}");
999    }
1000
1001    #[test]
1002    fn a_stream_that_is_not_an_avif_is_rejected() {
1003        for bytes in [
1004            &b""[..],
1005            &b"not an avif at all"[..],
1006            &b"\x89PNG\r\n\x1a\n"[..],
1007        ] {
1008            let error = AvifDecoder::new(bytes, Limits::default()).unwrap_err();
1009            assert_eq!(error.code(), ErrorCode::Malformed, "for {bytes:?}");
1010        }
1011    }
1012
1013    #[test]
1014    fn probe_needs_a_brand_not_just_an_isobmff_header() {
1015        assert!(probe(&ftyp(b"avif", &[])));
1016        // The common real-world shape: mif1 major, avif compatible.
1017        assert!(probe(&ftyp(b"mif1", &[b"avif", b"miaf"])));
1018        assert!(probe(&ftyp(b"MA1B", &[])));
1019        assert!(probe(&ftyp(b"avis", &[b"avif"])));
1020
1021        // Other ISOBMFF families must not be claimed.
1022        assert!(!probe(&ftyp(b"isom", &[b"mp42"])));
1023        assert!(!probe(&ftyp(b"heic", &[b"heix"])));
1024        assert!(!probe(&ftyp(b"qt  ", &[])));
1025
1026        // Short prefixes are declined, never indexed past.
1027        assert!(!probe(b""));
1028        assert!(!probe(b"\0\0\0\x20ftyp"));
1029        assert!(!probe(b"\x89PNG\r\n\x1a\n"));
1030    }
1031
1032    /// The minor version sits between the major brand and the compatible
1033    /// brands, and is a number. A file whose minor version happened to spell
1034    /// a brand must not be claimed on that basis.
1035    #[test]
1036    fn the_minor_version_is_not_read_as_a_brand() {
1037        let mut payload = Vec::from(*b"isom");
1038        payload.extend_from_slice(b"avif"); // minor version, not a brand
1039        payload.extend_from_slice(b"mp42");
1040        assert!(!probe(&boxed(b"ftyp", &payload)));
1041    }
1042
1043    #[test]
1044    fn the_codec_entry_reports_the_format() {
1045        assert_eq!(AvifCodec.format(), Format::Avif);
1046        assert!(AvifCodec.probe(&ftyp(b"avif", &[])));
1047        assert!(!AvifCodec.probe(b"short"));
1048    }
1049}