Skip to main content

oxideav_avif/
api.rs

1//! The workspace **image-crate API** (`IMAGE_CRATE_API.md`): the small
2//! root vocabulary every `oxideav-<format>` picture crate shares —
3//! `probe` / `info` / `decode*` / `encode*`, the pixel type
4//! [`AvifImage`], the raw `RgbImage` / `RgbaImage` records and the
5//! option records.
6//!
7//! AVIF is a container whose pixels come from the AV1 codec crate,
8//! which is framework-only by rule. So the split is:
9//!
10//! * **standalone** (no `registry` feature, no `oxideav-core`):
11//!   [`probe`], [`info`], the container model ([`crate::parse`] /
12//!   [`crate::parse_header`] / [`crate::inspect`]), composition over
13//!   caller-supplied planes ([`crate::grid`], [`crate::alpha`],
14//!   [`crate::transform`]), and every type on this page — including
15//!   [`AvifImage::to_rgb8`] / [`AvifImage::to_rgba8`], which convert
16//!   planes a caller decoded with its own AV1 decoder;
17//! * **`registry`** (default on): [`crate::decode`], [`crate::decode_with`],
18//!   [`crate::decode_rgb8`], [`crate::decode_rgba8`], [`crate::decode_all`],
19//!   [`crate::decode_from`], [`crate::encode`], [`crate::encode_rgb8`],
20//!   [`crate::encode_rgba8`], [`crate::encode_to`] — the AV1 decode and
21//!   encode behind them is `oxideav-av1`.
22//!
23//! # Pixel layouts
24//!
25//! [`AvifImage::format`] is the storage layout ([`PixelFormat`] =
26//! [`AvifPixelFormat`], variant names mirroring
27//! `oxideav_core::PixelFormat`): planar Y′CbCr at 4:2:0 / 4:2:2 /
28//! 4:4:4, monochrome, each with or without a full-resolution alpha
29//! plane, at 8 bits (one byte per sample) or 10 / 12 bits (one
30//! little-endian 16-bit word per sample, value in the low bits), plus
31//! the packed monochrome + alpha layouts `Ya8` / `Ya16Le`. An
32//! **identity-matrix** item (`color.matrix == 0`, the way AVIF stores
33//! RGB) decodes to the 4:4:4 layout with the planes holding **G, B, R**
34//! (H.273 §8.3: Y = G, Cb = B, Cr = R); [`AvifImage::from_rgb8`] /
35//! [`AvifImage::from_rgba8`] build exactly that, so RGB goes in and out
36//! without arithmetic.
37//!
38//! # RGB conversion
39//!
40//! [`AvifImage::to_rgb8`] / [`AvifImage::to_rgba8`] are exact per
41//! sample: the H.273 matrix of [`AvifImage::color`] (identity copies,
42//! BT.601 / BT.709 / BT.2020 non-constant-luminance and the other
43//! `Kr` / `Kb` matrices invert the §8.3 equations, limited or full
44//! range as signalled, round-to-nearest, clipped), sub-sampled chroma
45//! replicated to its co-sited luma samples (no interpolation), and
46//! 10 / 12-bit samples brought to 8 bits as `round(v × 255 / (2^d − 1))`.
47//! Alpha rides through the same depth scaling; an image without alpha
48//! yields opaque `255`.
49
50use std::time::Duration;
51
52use crate::error::{AvifError as Error, Result};
53use crate::image::{AvifFrame, AvifPixelFormat, AvifPlane};
54use crate::meta::Colr;
55use oxideav_heif::props as hprops;
56
57/// The pixel-layout enum under the contract's name (variant names
58/// mirror `oxideav_core::PixelFormat`).
59pub type PixelFormat = AvifPixelFormat;
60
61/// One plane of an [`AvifImage`] (`stride` bytes per row, `data`
62/// row-major).
63pub type Plane = AvifPlane;
64
65// ---------------------------------------------------------------------
66// Colour + metadata
67// ---------------------------------------------------------------------
68
69/// Nominal sample range of a picture (H.273 `VideoFullRangeFlag`).
70#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
71pub enum ColorRange {
72    /// Not signalled. AVIF never produces this on decode (MIAF
73    /// §7.3.6.4 supplies a default); it reads as full range wherever a
74    /// flag must be written.
75    #[default]
76    Unspecified,
77    /// Limited (video / studio) range: `VideoFullRangeFlag == 0`.
78    Limited,
79    /// Full (PC) range: `VideoFullRangeFlag == 1`.
80    Full,
81}
82
83impl ColorRange {
84    /// `VideoFullRangeFlag` as a bool (`Unspecified` → full, the MIAF
85    /// default).
86    pub fn is_full(self) -> bool {
87        self != ColorRange::Limited
88    }
89
90    /// From an H.273 `VideoFullRangeFlag`.
91    pub fn from_flag(full_range: bool) -> Self {
92        if full_range {
93            ColorRange::Full
94        } else {
95            ColorRange::Limited
96        }
97    }
98}
99
100/// The H.273 colour description of a picture: nominal range plus the
101/// `ColourPrimaries` / `TransferCharacteristics` /
102/// `MatrixCoefficients` code points (one byte each on the wire).
103///
104/// Filled from the item's `colr` `nclx` on decode; when a file carries
105/// none (or only an ICC `colr`) the MIAF §7.3.6.4 default applies —
106/// BT.709 primaries (1), sRGB transfer (13), BT.601 matrix (6), full
107/// range — which is also [`Default`] here.
108#[non_exhaustive]
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub struct ColorInfo {
111    /// Nominal sample range.
112    pub range: ColorRange,
113    /// H.273 §8.1 `ColourPrimaries` (1 = BT.709, 9 = BT.2020, 12 =
114    /// Display P3, 2 = unspecified).
115    pub primaries: u8,
116    /// H.273 §8.2 `TransferCharacteristics` (13 = sRGB, 16 = PQ, 18 =
117    /// HLG, 2 = unspecified).
118    pub transfer: u8,
119    /// H.273 §8.3 `MatrixCoefficients` (0 = identity / RGB, 1 = BT.709,
120    /// 6 = BT.601, 9 = BT.2020 NCL, 2 = unspecified).
121    pub matrix: u8,
122}
123
124impl ColorInfo {
125    /// Every field as a positional argument, in declaration order.
126    pub const fn new(range: ColorRange, primaries: u8, transfer: u8, matrix: u8) -> Self {
127        Self {
128            range,
129            primaries,
130            transfer,
131            matrix,
132        }
133    }
134
135    /// The MIAF §7.3.6.4 default for an item without CICP: BT.709
136    /// primaries, sRGB transfer, BT.601 matrix, full range.
137    pub const fn miaf_default() -> Self {
138        Self::new(ColorRange::Full, 1, 13, 6)
139    }
140
141    /// The description the RGB constructors use: BT.709 primaries,
142    /// sRGB transfer, the identity matrix (planes are G, B, R), full
143    /// range.
144    pub const fn identity_full_range() -> Self {
145        Self::new(ColorRange::Full, 1, 13, 0)
146    }
147
148    /// The effective colour of an item from its `colr` (MIAF
149    /// §7.3.6.4): the `nclx` code points as signalled, else
150    /// [`Self::miaf_default`]. Code points past 255 read as unspecified
151    /// (2).
152    pub fn from_colr(colr: Option<&Colr>) -> Self {
153        match colr {
154            Some(Colr::Nclx {
155                colour_primaries,
156                transfer_characteristics,
157                matrix_coefficients,
158                full_range,
159            }) => {
160                let code = |v: u16| u8::try_from(v).unwrap_or(2);
161                Self::new(
162                    ColorRange::from_flag(*full_range),
163                    code(*colour_primaries),
164                    code(*transfer_characteristics),
165                    code(*matrix_coefficients),
166                )
167            }
168            _ => Self::miaf_default(),
169        }
170    }
171
172    /// The `colr` `nclx` property carrying this description.
173    pub fn to_colr(&self) -> Colr {
174        Colr::Nclx {
175            colour_primaries: u16::from(self.primaries),
176            transfer_characteristics: u16::from(self.transfer),
177            matrix_coefficients: u16::from(self.matrix),
178            full_range: self.range.is_full(),
179        }
180    }
181
182    /// `true` for `MatrixCoefficients == 0`: the planes are G, B, R.
183    pub fn is_identity_matrix(&self) -> bool {
184        self.matrix == 0
185    }
186
187    /// Setter: replace `range`.
188    pub fn with_range(mut self, range: ColorRange) -> Self {
189        self.range = range;
190        self
191    }
192    /// Setter: replace `primaries`.
193    pub fn with_primaries(mut self, primaries: u8) -> Self {
194        self.primaries = primaries;
195        self
196    }
197    /// Setter: replace `transfer`.
198    pub fn with_transfer(mut self, transfer: u8) -> Self {
199        self.transfer = transfer;
200        self
201    }
202    /// Setter: replace `matrix`.
203    pub fn with_matrix(mut self, matrix: u8) -> Self {
204        self.matrix = matrix;
205        self
206    }
207
208    fn to_heif_colr(self) -> hprops::Colr {
209        hprops::Colr::from(&self.to_colr())
210    }
211}
212
213impl Default for ColorInfo {
214    fn default() -> Self {
215        Self::miaf_default()
216    }
217}
218
219/// Embedded metadata of a picture, each as the raw bytes a consumer
220/// hands to its own parser.
221///
222/// * `icc` — the ICC profile of an ICC `colr` (`rICC` / `prof`).
223/// * `exif` — the Exif payload from the TIFF header on (the HEIF
224///   A.2.1 `exif_tiff_header_offset` word and any bytes before the
225///   TIFF header are stripped on decode and re-written on encode).
226/// * `xmp` — the XMP packet (`application/rdf+xml` `mime` item).
227/// * `gamma` — AVIF carries no gamma value; always `None` on decode,
228///   ignored on encode.
229#[non_exhaustive]
230#[derive(Clone, Debug, Default, PartialEq)]
231pub struct Metadata {
232    pub icc: Option<Vec<u8>>,
233    pub exif: Option<Vec<u8>>,
234    pub xmp: Option<Vec<u8>>,
235    pub gamma: Option<f32>,
236}
237
238impl Metadata {
239    /// Every field as a positional argument, in declaration order.
240    pub fn new(
241        icc: Option<Vec<u8>>,
242        exif: Option<Vec<u8>>,
243        xmp: Option<Vec<u8>>,
244        gamma: Option<f32>,
245    ) -> Self {
246        Self {
247            icc,
248            exif,
249            xmp,
250            gamma,
251        }
252    }
253    /// Setter: replace `icc`.
254    pub fn with_icc(mut self, icc: Option<Vec<u8>>) -> Self {
255        self.icc = icc;
256        self
257    }
258    /// Setter: replace `exif`.
259    pub fn with_exif(mut self, exif: Option<Vec<u8>>) -> Self {
260        self.exif = exif;
261        self
262    }
263    /// Setter: replace `xmp`.
264    pub fn with_xmp(mut self, xmp: Option<Vec<u8>>) -> Self {
265        self.xmp = xmp;
266        self
267    }
268    /// Setter: replace `gamma`.
269    pub fn with_gamma(mut self, gamma: Option<f32>) -> Self {
270        self.gamma = gamma;
271        self
272    }
273
274    /// `true` when nothing is embedded.
275    pub fn is_empty(&self) -> bool {
276        self.icc.is_none() && self.exif.is_none() && self.xmp.is_none() && self.gamma.is_none()
277    }
278}
279
280/// The Exif payload from the TIFF header on, out of a HEIF `Exif`
281/// item body (A.2.1: a 4-byte big-endian `exif_tiff_header_offset`
282/// then the Exif data; the TIFF header sits `offset` bytes into the
283/// data). `None` when the body is too short for its own offset.
284#[cfg_attr(not(feature = "registry"), allow(dead_code))]
285pub(crate) fn exif_tiff_bytes(body: &[u8]) -> Option<Vec<u8>> {
286    if body.len() < 4 {
287        return None;
288    }
289    let offset = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
290    let start = 4usize.checked_add(offset)?;
291    if start > body.len() {
292        return None;
293    }
294    Some(body[start..].to_vec())
295}
296
297/// The HEIF `Exif` item body for a TIFF-header-first Exif payload: a
298/// zero `exif_tiff_header_offset` word then the bytes.
299#[cfg_attr(not(feature = "registry"), allow(dead_code))]
300pub(crate) fn exif_item_body(tiff: &[u8]) -> Vec<u8> {
301    let mut out = Vec::with_capacity(4 + tiff.len());
302    out.extend_from_slice(&[0, 0, 0, 0]);
303    out.extend_from_slice(tiff);
304    out
305}
306
307// ---------------------------------------------------------------------
308// Raw RGB records
309// ---------------------------------------------------------------------
310
311/// A tightly packed 8-bit RGB picture: `3 × width` bytes per row,
312/// row-major, no padding.
313#[non_exhaustive]
314#[derive(Clone, Debug, PartialEq, Eq)]
315pub struct RgbImage {
316    pub width: u32,
317    pub height: u32,
318    /// `width × height × 3` bytes, R G B per pixel.
319    pub data: Vec<u8>,
320}
321
322impl RgbImage {
323    /// Every field as a positional argument, in declaration order.
324    pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
325        Self {
326            width,
327            height,
328            data,
329        }
330    }
331    /// The packed bytes.
332    pub fn as_bytes(&self) -> &[u8] {
333        &self.data
334    }
335    /// The packed bytes, owned.
336    pub fn into_raw(self) -> Vec<u8> {
337        self.data
338    }
339}
340
341/// A tightly packed 8-bit RGBA picture: `4 × width` bytes per row,
342/// row-major, straight (non-premultiplied) alpha.
343#[non_exhaustive]
344#[derive(Clone, Debug, PartialEq, Eq)]
345pub struct RgbaImage {
346    pub width: u32,
347    pub height: u32,
348    /// `width × height × 4` bytes, R G B A per pixel.
349    pub data: Vec<u8>,
350}
351
352impl RgbaImage {
353    /// Every field as a positional argument, in declaration order.
354    pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
355        Self {
356            width,
357            height,
358            data,
359        }
360    }
361    /// The packed bytes.
362    pub fn as_bytes(&self) -> &[u8] {
363        &self.data
364    }
365    /// The packed bytes, owned.
366    pub fn into_raw(self) -> Vec<u8> {
367        self.data
368    }
369}
370
371// ---------------------------------------------------------------------
372// The pixel type
373// ---------------------------------------------------------------------
374
375/// A decoded (or to-be-encoded) AVIF picture in its native planar
376/// layout — the contract's `XxxImage`.
377///
378/// `planes` holds `format.plane_count()` planes in Y, Cb, Cr, A order
379/// (one packed plane for `Ya8` / `Ya16Le`), each at its own extent
380/// (chroma planes at the sub-sampled size, ceiling division). Rows are
381/// `stride` bytes apart; a tightly packed plane has
382/// `stride == plane_width × bytes_per_sample`.
383///
384/// Build one with [`AvifImage::new`] (any layout), [`AvifImage::from_rgb8`]
385/// / [`AvifImage::from_rgba8`] (identity-matrix 4:4:4, exact), or
386/// through the `registry`-gated [`crate::decode`] family.
387#[non_exhaustive]
388#[derive(Clone, Debug, PartialEq)]
389pub struct AvifImage {
390    pub width: u32,
391    pub height: u32,
392    /// Storage layout.
393    pub format: PixelFormat,
394    /// `format.plane_count()` planes, Y Cb Cr A (or the packed YA plane).
395    pub planes: Vec<Plane>,
396    /// Range + H.273 primaries / transfer / matrix.
397    pub color: ColorInfo,
398    /// ICC / Exif / XMP, when the file carried them.
399    pub metadata: Metadata,
400    /// Coded sample depth — 8, 10 or 12. Equals `format.bit_depth()`
401    /// for every layout but `Ya16Le`, whose 16-bit words hold 10- or
402    /// 12-bit values.
403    pub bit_depth: u8,
404}
405
406impl AvifImage {
407    /// A picture from its planes; colour defaults to the MIAF default
408    /// ([`ColorInfo::miaf_default`]), metadata to none, `bit_depth` to
409    /// the layout's (set [`Self::with_bit_depth`] for `Ya16Le`, whose
410    /// words hold 10- or 12-bit values). The plane geometry is
411    /// validated — `format.plane_count()` planes, each at least its
412    /// extent at the layout's bytes per sample under its stride —
413    /// so an inconsistent picture cannot exist ([`Error::InvalidData`]).
414    pub fn new(width: u32, height: u32, format: PixelFormat, planes: Vec<Plane>) -> Result<Self> {
415        let img = Self {
416            width,
417            height,
418            format,
419            planes,
420            color: ColorInfo::default(),
421            metadata: Metadata::default(),
422            bit_depth: format.bit_depth(),
423        };
424        img.validate_geometry()?;
425        Ok(img)
426    }
427
428    /// Plane count and per-plane coverage of `width × height` at
429    /// `format` (chroma extents by ceiling division; the packed YA
430    /// layouts carry two samples per pixel in one plane).
431    pub(crate) fn validate_geometry(&self) -> Result<()> {
432        if self.width == 0 || self.height == 0 {
433            return Err(Error::invalid(
434                "avif: picture dimensions must be at least 1x1",
435            ));
436        }
437        let expect = self.format.plane_count();
438        if self.planes.len() != expect {
439            return Err(Error::invalid(format!(
440                "avif: {:?} needs {expect} planes, picture carries {}",
441                self.format,
442                self.planes.len()
443            )));
444        }
445        let bps = self.format.bytes_per_sample();
446        let (sx, sy) = self.format.chroma_subsampling();
447        let (w, h) = (self.width as usize, self.height as usize);
448        for (i, plane) in self.planes.iter().enumerate() {
449            let (cols, rows) = if self.format.is_packed_ya() {
450                (w * 2, h)
451            } else if i == 1 || i == 2 {
452                (w.div_ceil(1 << sx), h.div_ceil(1 << sy))
453            } else {
454                (w, h)
455            };
456            let row_bytes = cols * bps;
457            let need = plane
458                .stride
459                .checked_mul(rows - 1)
460                .and_then(|v| v.checked_add(row_bytes));
461            let short = match need {
462                Some(n) => plane.data.len() < n,
463                None => true,
464            };
465            if plane.stride < row_bytes || short {
466                return Err(Error::invalid(format!(
467                    "avif: plane {i} ({} bytes, stride {}) does not cover {rows} rows of \
468                     {row_bytes} bytes",
469                    plane.data.len(),
470                    plane.stride
471                )));
472            }
473        }
474        Ok(())
475    }
476
477    /// Packed 8-bit RGB (`3 × width × height` bytes) as an
478    /// identity-matrix 4:4:4 picture: planes G, B, R, full range,
479    /// BT.709 primaries / sRGB transfer. Exact in both directions.
480    /// [`Error::InvalidData`] when `data.len() != 3 × width × height`
481    /// or either extent is 0.
482    pub fn from_rgb8(width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
483        let n = width as usize * height as usize;
484        if width == 0 || height == 0 || data.len() != n * 3 {
485            return Err(Error::invalid(format!(
486                "avif: from_rgb8 {width}x{height} needs {} RGB bytes, got {}",
487                n * 3,
488                data.len()
489            )));
490        }
491        let mut g = Vec::with_capacity(n);
492        let mut b = Vec::with_capacity(n);
493        let mut r = Vec::with_capacity(n);
494        for px in data.chunks_exact(3) {
495            r.push(px[0]);
496            g.push(px[1]);
497            b.push(px[2]);
498        }
499        let w = width as usize;
500        let planes = vec![Plane::new(w, g), Plane::new(w, b), Plane::new(w, r)];
501        Ok(Self::new(width, height, PixelFormat::Yuv444P, planes)?
502            .with_color(ColorInfo::identity_full_range()))
503    }
504
505    /// Packed 8-bit RGBA (`4 × width × height` bytes) as an
506    /// identity-matrix 4:4:4 picture with a full-resolution alpha
507    /// plane (planes G, B, R, A). Exact in both directions.
508    /// [`Error::InvalidData`] when `data.len() != 4 × width × height`
509    /// or either extent is 0.
510    pub fn from_rgba8(width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
511        let n = width as usize * height as usize;
512        if width == 0 || height == 0 || data.len() != n * 4 {
513            return Err(Error::invalid(format!(
514                "avif: from_rgba8 {width}x{height} needs {} RGBA bytes, got {}",
515                n * 4,
516                data.len()
517            )));
518        }
519        let mut g = Vec::with_capacity(n);
520        let mut b = Vec::with_capacity(n);
521        let mut r = Vec::with_capacity(n);
522        let mut a = Vec::with_capacity(n);
523        for px in data.chunks_exact(4) {
524            r.push(px[0]);
525            g.push(px[1]);
526            b.push(px[2]);
527            a.push(px[3]);
528        }
529        let w = width as usize;
530        let planes = vec![
531            Plane::new(w, g),
532            Plane::new(w, b),
533            Plane::new(w, r),
534            Plane::new(w, a),
535        ];
536        Ok(Self::new(width, height, PixelFormat::Yuva444P, planes)?
537            .with_color(ColorInfo::identity_full_range()))
538    }
539
540    /// Setter: replace `color`.
541    pub fn with_color(mut self, color: ColorInfo) -> Self {
542        self.color = color;
543        self
544    }
545    /// Setter: replace `metadata`.
546    pub fn with_metadata(mut self, metadata: Metadata) -> Self {
547        self.metadata = metadata;
548        self
549    }
550    /// Setter: replace `bit_depth` (8, 10 or 12; meaningful for
551    /// `Ya16Le`, whose storage width is not its coded depth).
552    pub fn with_bit_depth(mut self, bit_depth: u8) -> Self {
553        self.bit_depth = bit_depth;
554        self
555    }
556
557    /// Luma width in pixels.
558    pub fn width(&self) -> u32 {
559        self.width
560    }
561    /// Luma height in pixels.
562    pub fn height(&self) -> u32 {
563        self.height
564    }
565    /// Storage layout.
566    pub fn format(&self) -> PixelFormat {
567        self.format
568    }
569    /// Coded sample depth (8 / 10 / 12).
570    pub fn bit_depth(&self) -> u8 {
571        self.bit_depth
572    }
573    /// `true` when the layout carries an alpha plane / channel.
574    pub fn has_alpha(&self) -> bool {
575        self.format.has_alpha()
576    }
577
578    /// The single plane's bytes for the one-plane layouts (`Gray8` /
579    /// `Gray10Le` / `Gray12Le`, packed `Ya8` / `Ya16Le`); `None` for
580    /// the planar Y′CbCr layouts — use [`Self::into_raw`].
581    pub fn as_bytes(&self) -> Option<&[u8]> {
582        if self.planes.len() == 1 {
583            self.planes.first().map(|p| p.data.as_slice())
584        } else {
585            None
586        }
587    }
588
589    /// Every plane's bytes concatenated in plane order, strides as
590    /// reported in `planes`.
591    pub fn into_raw(self) -> Vec<u8> {
592        let total: usize = self.planes.iter().map(|p| p.data.len()).sum();
593        let mut out = Vec::with_capacity(total);
594        for p in self.planes {
595            out.extend_from_slice(&p.data);
596        }
597        out
598    }
599
600    /// The picture as tightly packed 8-bit RGB (see the module docs for
601    /// the exact conversion). Alpha, if any, is dropped.
602    ///
603    /// A `MatrixCoefficients` the converter has no kernel for — the
604    /// reserved code points, unspecified (2), the constant-luminance
605    /// BT.2020 (10), chromaticity-derived (12 / 13) and ICtCp (14)
606    /// matrices, YCgCo (8) — is converted as the MIAF default BT.601 (6)
607    /// at the signalled range; [`Self::try_to_rgb8`] reports it as
608    /// [`Error::Unsupported`] instead for callers that must know.
609    ///
610    /// # Panics
611    ///
612    /// Never for a picture this crate decoded or [`Self::new`]
613    /// validated; only when the public fields were later assigned
614    /// planes that do not cover `width × height` at `format`
615    /// ([`Self::try_to_rgb8`] reports that as an error).
616    pub fn to_rgb8(&self) -> Vec<u8> {
617        self.rgb_bytes_or_bt601(3)
618            .expect("AvifImage::to_rgb8: planes inconsistent with width/height/format")
619    }
620
621    /// The picture as tightly packed 8-bit RGBA, alpha opaque (`255`)
622    /// when the layout has none. Matrix fallback as [`Self::to_rgb8`].
623    ///
624    /// # Panics
625    ///
626    /// As [`Self::to_rgb8`].
627    pub fn to_rgba8(&self) -> Vec<u8> {
628        self.rgb_bytes_or_bt601(4)
629            .expect("AvifImage::to_rgba8: planes inconsistent with width/height/format")
630    }
631
632    /// Fallible, exact [`Self::to_rgb8`]: [`Error::Unsupported`] for a
633    /// matrix without a kernel (no BT.601 fallback),
634    /// [`Error::InvalidData`] for planes inconsistent with the layout.
635    pub fn try_to_rgb8(&self) -> Result<Vec<u8>> {
636        self.rgb_bytes(3, self.color)
637    }
638
639    /// Fallible, exact [`Self::to_rgba8`] (see [`Self::try_to_rgb8`]).
640    pub fn try_to_rgba8(&self) -> Result<Vec<u8>> {
641        self.rgb_bytes(4, self.color)
642    }
643
644    /// The exact conversion, else the BT.601 reading of a matrix the
645    /// converter does not know.
646    fn rgb_bytes_or_bt601(&self, channels: usize) -> Result<Vec<u8>> {
647        match self.rgb_bytes(channels, self.color) {
648            Err(Error::Unsupported(_)) => self.rgb_bytes(channels, self.color.with_matrix(6)),
649            other => other,
650        }
651    }
652
653    /// The container crate's planar frame for this picture (the shape
654    /// its composition and colour layers work on).
655    pub(crate) fn to_heif_frame(&self) -> Result<oxideav_heif::HeifFrame> {
656        let frame = AvifFrame::new(None, self.planes.clone());
657        crate::frame_bridge::to_heif(&frame, self.format, self.bit_depth, self.width, self.height)
658    }
659
660    /// `channels` = 3 (RGB) or 4 (RGBA), 8 bits per channel, converted
661    /// with `color`.
662    fn rgb_bytes(&self, channels: usize, color: ColorInfo) -> Result<Vec<u8>> {
663        if !matches!(self.bit_depth, 8 | 10 | 12) {
664            return Err(Error::invalid(format!(
665                "avif: bit_depth {} is not 8, 10 or 12",
666                self.bit_depth
667            )));
668        }
669        let frame = self.to_heif_frame()?;
670        let colr = color.to_heif_colr();
671        let rgb = oxideav_heif::rgb::to_rgb(&frame, Some(&colr))?;
672        let n = self.width as usize * self.height as usize;
673        if rgb.channels < 3 || rgb.data.len() < n * rgb.channels {
674            return Err(Error::invalid(
675                "avif: RGB conversion returned fewer samples than the picture holds",
676            ));
677        }
678        let depth = rgb.bit_depth;
679        let max = (1u32 << depth) - 1;
680        let half = max / 2;
681        let to8 = |v: u16| -> u8 {
682            if depth == 8 {
683                v as u8
684            } else {
685                ((u32::from(v).min(max) * 255 + half) / max) as u8
686            }
687        };
688        let mut out = Vec::with_capacity(n * channels);
689        for px in rgb.data.chunks_exact(rgb.channels).take(n) {
690            out.push(to8(px[0]));
691            out.push(to8(px[1]));
692            out.push(to8(px[2]));
693            if channels == 4 {
694                out.push(if rgb.channels >= 4 { to8(px[3]) } else { 255 });
695            }
696        }
697        Ok(out)
698    }
699}
700
701// ---------------------------------------------------------------------
702// Frames, header info, options
703// ---------------------------------------------------------------------
704
705/// One picture of a multi-picture file ([`crate::decode_all`]): a
706/// sample of an `avis` image sequence (with its display `delay`) or
707/// one entity of an image-burst group (no delay).
708#[non_exhaustive]
709#[derive(Clone, Debug, PartialEq)]
710pub struct Frame {
711    pub image: AvifImage,
712    /// How long the frame is shown before the next one (sequence
713    /// samples); `None` for bursts and stills.
714    pub delay: Option<Duration>,
715}
716
717impl Frame {
718    /// Every field as a positional argument, in declaration order.
719    pub fn new(image: AvifImage, delay: Option<Duration>) -> Self {
720        Self { image, delay }
721    }
722}
723
724/// What [`probe`] + a header parse say about a file, without decoding
725/// pixels.
726#[non_exhaustive]
727#[derive(Clone, Debug, PartialEq, Eq)]
728pub struct ImageInfo {
729    /// Output extents of the primary item (`ispe` / derived canvas; a
730    /// sequence's `tkhd` display size, else its track's sample entry).
731    pub width: u32,
732    pub height: u32,
733    /// The layout a decode yields: depth and chroma from `av1C`, alpha
734    /// from the attached auxiliary.
735    pub format: PixelFormat,
736    /// Pictures a [`crate::decode_all`] yields: samples of a sequence,
737    /// entities of the primary's burst group, else 1.
738    pub frames: u32,
739    pub has_alpha: bool,
740    /// The primary's effective colour (MIAF default when unsignalled).
741    pub color: ColorInfo,
742    pub has_icc: bool,
743    pub has_exif: bool,
744    pub has_xmp: bool,
745    /// Coded sample depth from `av1C` (8 / 10 / 12).
746    pub bit_depth: u8,
747    /// `true` for an `avis` image sequence with a sample table.
748    pub is_sequence: bool,
749}
750
751impl ImageInfo {
752    /// Every field as a positional argument, in declaration order.
753    #[allow(clippy::too_many_arguments)]
754    pub fn new(
755        width: u32,
756        height: u32,
757        format: PixelFormat,
758        frames: u32,
759        has_alpha: bool,
760        color: ColorInfo,
761        has_icc: bool,
762        has_exif: bool,
763        has_xmp: bool,
764        bit_depth: u8,
765        is_sequence: bool,
766    ) -> Self {
767        Self {
768            width,
769            height,
770            format,
771            frames,
772            has_alpha,
773            color,
774            has_icc,
775            has_exif,
776            has_xmp,
777            bit_depth,
778            is_sequence,
779        }
780    }
781}
782
783/// Bounds and switches for [`crate::decode_with`]. The limits are
784/// checked against the header before any pixel allocation and reported
785/// as [`Error::LimitExceeded`]; `None` means unlimited.
786#[non_exhaustive]
787#[derive(Clone, Copy, Debug, PartialEq)]
788pub struct DecodeOptions {
789    /// Refuse a picture wider than this (default `Some(16384)`, the
790    /// largest AV1 coded extent this crate composes).
791    pub max_width: Option<u32>,
792    /// Refuse a picture taller than this (default `Some(16384)`).
793    pub max_height: Option<u32>,
794    /// Refuse a picture — or any derived canvas on the way to it —
795    /// with more pixels than this (default `Some(1 << 28)`).
796    pub max_pixels: Option<u64>,
797    /// Refuse an input longer than this many bytes (default
798    /// `Some(1 << 30)`, 1 GiB).
799    pub max_bytes: Option<u64>,
800    /// Strict conformance: the `ftyp` must declare an AVIF brand
801    /// (`avif` / `avis` / `avio`; a HEIF-structural-only brand set is
802    /// refused) and a file claiming `mif1` must carry every HEIF
803    /// §10.2.1.1 mandatory box. Default `false`: the lenient reading
804    /// this crate has always applied.
805    pub strict: bool,
806    /// Apply the gain map of a `tmap` alternative (HEIF Amd 1
807    /// §6.6.2.4, ISO 21496-1): the reconstructed HDR rendition in the
808    /// `tmap` item's own colour instead of the base image. Default
809    /// `false` — the base, what readers without tone-map support show.
810    pub tone_mapped: bool,
811    /// HDR reference white (cd/m²) for a PQ-coded reconstruction;
812    /// default 203.
813    pub reference_white_nits: f64,
814    /// Render this spatial layer of a layered primary item instead of
815    /// the file's `lsel` choice (av1-avif §2.3.2.2); `None` honours the
816    /// file.
817    pub layer: Option<u16>,
818    /// Worker budget for the AV1 decode of every coded item / sample
819    /// (the `ExecutionContext` handed to `oxideav-av1`, which walks a
820    /// multi-tile intra frame tile-parallel and runs its post-filters
821    /// by row band — byte-identical to the serial decode at any
822    /// budget). `None` or `Some(1)` = serial, the default.
823    pub threads: Option<usize>,
824}
825
826impl Default for DecodeOptions {
827    fn default() -> Self {
828        Self {
829            max_width: Some(16_384),
830            max_height: Some(16_384),
831            max_pixels: Some(1 << 28),
832            max_bytes: Some(1 << 30),
833            strict: false,
834            tone_mapped: false,
835            reference_white_nits: oxideav_heif::gainmap::DEFAULT_HDR_REFERENCE_WHITE_NITS,
836            layer: None,
837            threads: None,
838        }
839    }
840}
841
842impl DecodeOptions {
843    /// The defaults (see the field docs).
844    pub fn new() -> Self {
845        Self::default()
846    }
847    /// Setter: replace `max_width` (`None` = unlimited).
848    pub fn with_max_width(mut self, max_width: Option<u32>) -> Self {
849        self.max_width = max_width;
850        self
851    }
852    /// Setter: replace `max_height` (`None` = unlimited).
853    pub fn with_max_height(mut self, max_height: Option<u32>) -> Self {
854        self.max_height = max_height;
855        self
856    }
857    /// Setter: replace `max_pixels` (`None` = unlimited).
858    pub fn with_max_pixels(mut self, max_pixels: Option<u64>) -> Self {
859        self.max_pixels = max_pixels;
860        self
861    }
862    /// Setter: replace `max_bytes` (`None` = unlimited).
863    pub fn with_max_bytes(mut self, max_bytes: Option<u64>) -> Self {
864        self.max_bytes = max_bytes;
865        self
866    }
867    /// Setter: replace `strict`.
868    pub fn with_strict(mut self, strict: bool) -> Self {
869        self.strict = strict;
870        self
871    }
872    /// Setter: replace `tone_mapped`.
873    pub fn with_tone_mapped(mut self, tone_mapped: bool) -> Self {
874        self.tone_mapped = tone_mapped;
875        self
876    }
877    /// Setter: replace `reference_white_nits`.
878    pub fn with_reference_white_nits(mut self, reference_white_nits: f64) -> Self {
879        self.reference_white_nits = reference_white_nits;
880        self
881    }
882    /// Setter: replace `layer`.
883    pub fn with_layer(mut self, layer: Option<u16>) -> Self {
884        self.layer = layer;
885        self
886    }
887    /// Setter: replace `threads` (`None` / `Some(1)` = serial).
888    pub fn with_threads(mut self, threads: Option<usize>) -> Self {
889        self.threads = threads;
890        self
891    }
892
893    /// The worker budget as a count (`threads`, at least 1).
894    pub fn worker_count(&self) -> usize {
895        self.threads.unwrap_or(1).max(1)
896    }
897
898    /// Check an input length against `max_bytes`
899    /// ([`Error::LimitExceeded`] when over).
900    pub fn check_bytes(&self, len: usize) -> Result<()> {
901        if let Some(max) = self.max_bytes {
902            if len as u64 > max {
903                return Err(Error::limit(format!(
904                    "avif: input is {len} bytes, more than the {max} allowed"
905                )));
906            }
907        }
908        Ok(())
909    }
910
911    /// Check a picture's extents against `max_width` / `max_height` /
912    /// `max_pixels` ([`Error::LimitExceeded`] when over).
913    pub fn check_dims(&self, width: u32, height: u32) -> Result<()> {
914        if self.max_width.is_some_and(|m| width > m) || self.max_height.is_some_and(|m| height > m)
915        {
916            return Err(Error::limit(format!(
917                "avif: picture is {width}x{height}, more than the {:?}x{:?} allowed",
918                self.max_width, self.max_height
919            )));
920        }
921        let pixels = u64::from(width) * u64::from(height);
922        if let Some(max) = self.max_pixels {
923            if pixels > max {
924                return Err(Error::limit(format!(
925                    "avif: picture holds {pixels} pixels, more than the {max} allowed"
926                )));
927            }
928        }
929        Ok(())
930    }
931}
932
933/// Chroma layout of an encode ([`crate::still::StillImage`] and the
934/// RGB one-call paths). Mirrors the AV1 §6.4.2 subsampling pairings.
935#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
936pub enum StillChroma {
937    /// Chroma at half extent on both axes (`subsampling_x =
938    /// subsampling_y = 1`). Requires even luma extents.
939    #[default]
940    Yuv420,
941    /// Chroma at half horizontal extent (`subsampling_x = 1`,
942    /// `subsampling_y = 0`). Requires an even luma width. Codes as AV1
943    /// Professional profile.
944    Yuv422,
945    /// Chroma at full extent.
946    Yuv444,
947    /// Luma only (4:0:0).
948    Monochrome,
949}
950
951#[cfg_attr(not(feature = "registry"), allow(dead_code))]
952impl StillChroma {
953    /// `(subsampling_x, subsampling_y)` as shift amounts.
954    pub(crate) fn subsampling(self) -> (u32, u32) {
955        match self {
956            StillChroma::Yuv420 => (1, 1),
957            StillChroma::Yuv422 => (1, 0),
958            StillChroma::Yuv444 => (0, 0),
959            StillChroma::Monochrome => (0, 0), // no chroma planes
960        }
961    }
962
963    pub(crate) fn has_chroma(self) -> bool {
964        self != StillChroma::Monochrome
965    }
966}
967
968/// Search effort of the AV1 still encoder behind every coded item
969/// ([`EncodeOptions::speed`]).
970#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
971pub enum EncodeSpeed {
972    /// The production point (default): the reduced search ladders —
973    /// a 12 MP 8-bit still in seconds single-threaded, about a second
974    /// on 8 threads.
975    #[default]
976    Fast,
977    /// A fuller search (rect partitions, refined deblock, fuller CDEF
978    /// / restoration sweeps): roughly 2.5 × the wall clock for a small
979    /// rate gain.
980    Balanced,
981    /// The exhaustive search with every election: minutes per 12 MP
982    /// picture.
983    Thorough,
984}
985
986/// Tuning for the encoders ([`crate::encode`], [`crate::encode_rgb8`],
987/// [`crate::encode_rgba8`], [`crate::still::encode_still`] …). The
988/// default is a lossless colour encode (`base_q_idx = 0`), lossless
989/// alpha, straight (non-premultiplied) alpha signalling, 4:2:0 for the
990/// RGB one-call paths, every metadata block embedded.
991#[non_exhaustive]
992#[derive(Clone, Copy, Debug, PartialEq, Eq)]
993pub struct EncodeOptions {
994    /// AV1 `base_q_idx` for the colour planes. `0` = lossless
995    /// (default); higher = lossier / smaller (255 = coarsest). See
996    /// [`Self::with_quality`] for the 0–100 scale.
997    pub base_q_idx: u8,
998    /// AV1 `base_q_idx` for the alpha auxiliary. Defaults to `0`
999    /// (lossless) — alpha artefacts are far more visible than colour
1000    /// ones, and flat alpha planes are cheap.
1001    pub alpha_q_idx: u8,
1002    /// Emit the `prem` iref declaring the colour planes premultiplied
1003    /// by alpha (HEIF §6.10.1.1). Signalling only — the samples are
1004    /// stored as given.
1005    pub premultiplied_alpha: bool,
1006    /// Chroma layout of [`crate::encode_rgb8`] / [`crate::encode_rgba8`]
1007    /// (default 4:2:0; odd extents fall back to 4:4:4, see those
1008    /// functions). [`crate::encode`] keeps the image's own layout.
1009    pub chroma: StillChroma,
1010    /// Embed `metadata.exif` as an `Exif` item (default `true`).
1011    pub embed_exif: bool,
1012    /// Embed `metadata.xmp` as a `mime` item (default `true`).
1013    pub embed_xmp: bool,
1014    /// Embed `metadata.icc` as an ICC `colr` next to the `nclx` one
1015    /// (default `true`).
1016    pub embed_icc: bool,
1017    /// Search effort of the AV1 encode (default [`EncodeSpeed::Fast`]).
1018    pub speed: EncodeSpeed,
1019    /// Worker threads for the AV1 tile search of every coded item
1020    /// (default `1`). The tile layout is derived from the picture size
1021    /// alone, so the coded bytes never depend on this value.
1022    pub threads: usize,
1023}
1024
1025impl Default for EncodeOptions {
1026    fn default() -> Self {
1027        Self {
1028            base_q_idx: 0,
1029            alpha_q_idx: 0,
1030            premultiplied_alpha: false,
1031            chroma: StillChroma::Yuv420,
1032            embed_exif: true,
1033            embed_xmp: true,
1034            embed_icc: true,
1035            speed: EncodeSpeed::Fast,
1036            threads: 1,
1037        }
1038    }
1039}
1040
1041impl EncodeOptions {
1042    /// The historical three-field constructor (`base_q_idx`,
1043    /// `alpha_q_idx`, `premultiplied_alpha`); the other fields take
1044    /// their defaults.
1045    pub fn new(base_q_idx: u8, alpha_q_idx: u8, premultiplied_alpha: bool) -> Self {
1046        Self {
1047            base_q_idx,
1048            alpha_q_idx,
1049            premultiplied_alpha,
1050            ..Self::default()
1051        }
1052    }
1053    /// Setter: replace `base_q_idx`.
1054    pub fn with_base_q_idx(mut self, base_q_idx: u8) -> Self {
1055        self.base_q_idx = base_q_idx;
1056        self
1057    }
1058    /// Setter: replace `alpha_q_idx`.
1059    pub fn with_alpha_q_idx(mut self, alpha_q_idx: u8) -> Self {
1060        self.alpha_q_idx = alpha_q_idx;
1061        self
1062    }
1063    /// Setter: replace `premultiplied_alpha`.
1064    pub fn with_premultiplied_alpha(mut self, premultiplied_alpha: bool) -> Self {
1065        self.premultiplied_alpha = premultiplied_alpha;
1066        self
1067    }
1068    /// Setter: replace `chroma`.
1069    pub fn with_chroma(mut self, chroma: StillChroma) -> Self {
1070        self.chroma = chroma;
1071        self
1072    }
1073    /// Setter: replace `embed_exif`.
1074    pub fn with_embed_exif(mut self, embed_exif: bool) -> Self {
1075        self.embed_exif = embed_exif;
1076        self
1077    }
1078    /// Setter: replace `embed_xmp`.
1079    pub fn with_embed_xmp(mut self, embed_xmp: bool) -> Self {
1080        self.embed_xmp = embed_xmp;
1081        self
1082    }
1083    /// Setter: replace `embed_icc`.
1084    pub fn with_embed_icc(mut self, embed_icc: bool) -> Self {
1085        self.embed_icc = embed_icc;
1086        self
1087    }
1088    /// Setter: replace `speed`.
1089    pub fn with_speed(mut self, speed: EncodeSpeed) -> Self {
1090        self.speed = speed;
1091        self
1092    }
1093    /// Setter: replace `threads` (clamped to at least 1).
1094    pub fn with_threads(mut self, threads: usize) -> Self {
1095        self.threads = threads.max(1);
1096        self
1097    }
1098
1099    /// Colour quality on a 0–100 scale (100 = lossless): sets
1100    /// `base_q_idx = ((100 − quality) × 255 + 50) / 100`, so 100 → 0,
1101    /// 90 → 26, 75 → 64, 50 → 128, 0 → 255. Values above 100 clamp to
1102    /// 100. Alpha stays at its own `alpha_q_idx`.
1103    pub fn with_quality(mut self, quality: u8) -> Self {
1104        let q = u32::from(quality.min(100));
1105        self.base_q_idx = (((100 - q) * 255 + 50) / 100) as u8;
1106        self
1107    }
1108}
1109
1110// ---------------------------------------------------------------------
1111// probe / info (standalone)
1112// ---------------------------------------------------------------------
1113
1114const BRAND_AVIF: [u8; 4] = *b"avif";
1115const BRAND_AVIS: [u8; 4] = *b"avis";
1116const BRAND_AVIO: [u8; 4] = *b"avio";
1117
1118/// `true` when `bytes` start with an ISOBMFF `ftyp` box whose major or
1119/// compatible brands include an AVIF brand (`avif` / `avis` / `avio`,
1120/// av1-avif §6). No allocation, never panics, `false` on short input
1121/// or on a HEIF file that is not AVIF.
1122pub fn probe(bytes: &[u8]) -> bool {
1123    if bytes.len() < 16 || &bytes[4..8] != b"ftyp" {
1124        return false;
1125    }
1126    let size32 = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
1127    let (size, brands_start) = match size32 {
1128        0 => (bytes.len(), 16),
1129        1 => {
1130            if bytes.len() < 24 {
1131                return false;
1132            }
1133            let large = u64::from_be_bytes([
1134                bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
1135                bytes[15],
1136            ]);
1137            (usize::try_from(large).unwrap_or(usize::MAX), 24)
1138        }
1139        n => (n, 16),
1140    };
1141    let major_at = brands_start - 8;
1142    if size < brands_start {
1143        return false;
1144    }
1145    let end = size.min(bytes.len());
1146    let is_avif = |b: &[u8]| b == BRAND_AVIF || b == BRAND_AVIS || b == BRAND_AVIO;
1147    if is_avif(&bytes[major_at..major_at + 4]) {
1148        return true;
1149    }
1150    bytes[brands_start..end].chunks_exact(4).any(is_avif)
1151}
1152
1153/// Header-only description of an AVIF file: extents, the layout a
1154/// decode yields, the picture count, alpha, colour and metadata
1155/// presence. Parses `ftyp` + `meta` (and the sample table of an
1156/// `avis` sequence), never a coded payload.
1157pub fn info(bytes: &[u8]) -> Result<ImageInfo> {
1158    let (ftyp_payload, _) = crate::box_parser::find_box(bytes, b"ftyp")?
1159        .ok_or_else(|| Error::invalid("avif: missing ftyp"))?;
1160    let (major, _minor, compat) = crate::parser::parse_ftyp(ftyp_payload)?;
1161    let brands = crate::parser::classify_brands(&major, &compat)?;
1162    if (brands.is_sequence || brands.has_msf1) && has_moov(bytes) {
1163        return sequence_info(bytes);
1164    }
1165    still_info(bytes)
1166}
1167
1168pub(crate) fn has_moov(bytes: &[u8]) -> bool {
1169    crate::box_parser::find_box(bytes, b"moov")
1170        .ok()
1171        .flatten()
1172        .is_some()
1173}
1174
1175/// The `nclx` and ICC `colr` properties of an item, wherever they sit
1176/// in its association list (an item may carry one of each — MIAF
1177/// §7.3.6.4).
1178pub(crate) fn item_colrs(
1179    meta: &crate::meta::Meta,
1180    item_id: u32,
1181) -> (Option<Colr>, Option<Vec<u8>>) {
1182    let mut nclx = None;
1183    let mut icc = None;
1184    for prop in meta.properties_for(item_id) {
1185        if let crate::meta::Property::Colr(c) = prop {
1186            match c {
1187                Colr::Nclx { .. } if nclx.is_none() => nclx = Some(c.clone()),
1188                Colr::Icc(bytes) if icc.is_none() => icc = Some(bytes.clone()),
1189                _ => {}
1190            }
1191        }
1192    }
1193    (nclx, icc)
1194}
1195
1196/// The layout a decode of the primary yields, from the inspected
1197/// `av1C` flags plus alpha presence.
1198pub(crate) fn layout_of_info(
1199    bit_depth: u8,
1200    monochrome: bool,
1201    chroma: Option<(bool, bool)>,
1202    has_alpha: bool,
1203) -> Result<PixelFormat> {
1204    let (sx, sy) = match chroma {
1205        Some((x, y)) => (u8::from(x), u8::from(y)),
1206        None => (1, 1),
1207    };
1208    PixelFormat::from_layout(bit_depth, sx, sy, monochrome, has_alpha).ok_or_else(|| {
1209        Error::unsupported(format!(
1210            "avif: no pixel layout for {bit_depth}-bit subsampling ({sx}, {sy}) \
1211             monochrome={monochrome} alpha={has_alpha}"
1212        ))
1213    })
1214}
1215
1216/// The entities of a `brst` image-burst group (HEIF §6.8.9) that
1217/// contains `item_id`, in group order; empty when there is none.
1218pub(crate) fn burst_members(meta: &crate::meta::Meta, item_id: u32) -> Vec<u32> {
1219    meta.groups()
1220        .unwrap_or_default()
1221        .into_iter()
1222        .find(|g| g.is_burst() && g.entity_ids.contains(&item_id))
1223        .map(|g| g.entity_ids)
1224        .unwrap_or_default()
1225}
1226
1227fn still_info(bytes: &[u8]) -> Result<ImageInfo> {
1228    let hdr = crate::parser::parse_header(bytes)?;
1229    let primary_id = hdr
1230        .meta
1231        .primary_item_id
1232        .ok_or_else(|| Error::invalid("avif: missing pitm"))?;
1233    let info = crate::inspect::inspect(bytes)?;
1234    let bit_depth = info.bit_depth.unwrap_or(8);
1235    let format = layout_of_info(
1236        bit_depth,
1237        info.monochrome,
1238        info.chroma_subsampling,
1239        info.has_alpha,
1240    )?;
1241    let (nclx, icc) = item_colrs(&hdr.meta, primary_id);
1242    // A derived primary without its own `colr` inherits its first
1243    // input's (what `inspect` resolved).
1244    let colour = nclx.or_else(|| match &info.colour {
1245        Some(c @ Colr::Nclx { .. }) => Some(c.clone()),
1246        _ => None,
1247    });
1248    let has_icc = icc.is_some() || matches!(info.colour, Some(Colr::Icc(_)));
1249    let burst = burst_members(&hdr.meta, primary_id);
1250    let frames = if burst.is_empty() {
1251        1
1252    } else {
1253        u32::try_from(burst.len()).unwrap_or(u32::MAX)
1254    };
1255    Ok(ImageInfo::new(
1256        info.width,
1257        info.height,
1258        format,
1259        frames,
1260        info.has_alpha,
1261        ColorInfo::from_colr(colour.as_ref()),
1262        has_icc,
1263        info.exif_item_id.is_some(),
1264        info.xmp_item_id.is_some(),
1265        bit_depth,
1266        false,
1267    ))
1268}
1269
1270fn sequence_info(bytes: &[u8]) -> Result<ImageInfo> {
1271    let meta = crate::avis::parse_avis(bytes)?;
1272    let av1c = meta.av1_codec_config.as_deref().ok_or_else(|| {
1273        Error::invalid("avis: track stsd → av01 → av1C is missing (av1-avif §2.2.1)")
1274    })?;
1275    let cfg = oxideav_heif::Av1Config::parse(av1c)?;
1276    let bit_depth = cfg.bit_depth();
1277    let format = layout_of_info(
1278        bit_depth,
1279        cfg.monochrome,
1280        Some((cfg.chroma_subsampling_x, cfg.chroma_subsampling_y)),
1281        false,
1282    )?;
1283    let (width, height) = meta.display_dims.unwrap_or((0, 0));
1284    let (has_exif, has_xmp, has_icc) = match crate::parser::parse_header(bytes) {
1285        Ok(hdr) => {
1286            // The cover still of the sequence (av1-avif §6.3) may carry
1287            // the metadata items.
1288            let primary = hdr.meta.primary_item_id;
1289            match primary.and_then(|_| crate::inspect::inspect(bytes).ok()) {
1290                Some(i) => (
1291                    i.exif_item_id.is_some(),
1292                    i.xmp_item_id.is_some(),
1293                    matches!(i.colour, Some(Colr::Icc(_)))
1294                        || primary
1295                            .map(|p| item_colrs(&hdr.meta, p).1.is_some())
1296                            .unwrap_or(false),
1297                ),
1298                None => (false, false, false),
1299            }
1300        }
1301        Err(_) => (false, false, false),
1302    };
1303    Ok(ImageInfo::new(
1304        width,
1305        height,
1306        format,
1307        u32::try_from(meta.samples.len()).unwrap_or(u32::MAX),
1308        false,
1309        ColorInfo::from_colr(meta.colr.as_ref()),
1310        has_icc,
1311        has_exif,
1312        has_xmp,
1313        bit_depth,
1314        true,
1315    ))
1316}
1317
1318#[cfg(test)]
1319mod tests {
1320    use super::*;
1321
1322    fn ftyp(major: &[u8; 4], compat: &[&[u8; 4]]) -> Vec<u8> {
1323        let mut v = Vec::new();
1324        let size = 16 + 4 * compat.len();
1325        v.extend_from_slice(&(size as u32).to_be_bytes());
1326        v.extend_from_slice(b"ftyp");
1327        v.extend_from_slice(major);
1328        v.extend_from_slice(&[0, 0, 0, 0]);
1329        for c in compat {
1330            v.extend_from_slice(*c);
1331        }
1332        v
1333    }
1334
1335    #[test]
1336    fn probe_accepts_avif_brands_only() {
1337        assert!(probe(&ftyp(b"avif", &[b"mif1", b"miaf"])));
1338        assert!(probe(&ftyp(b"mif1", &[b"avif"])));
1339        assert!(probe(&ftyp(b"avis", &[b"msf1"])));
1340        assert!(probe(&ftyp(b"avio", &[])));
1341        assert!(!probe(&ftyp(b"heic", &[b"mif1", b"miaf"])));
1342        assert!(!probe(&ftyp(b"mif1", &[b"heic"])));
1343        assert!(!probe(b""));
1344        assert!(!probe(b"\0\0\0\x10ftypavif"));
1345        assert!(!probe(b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0"));
1346        // A truncated compatible-brand list is scanned as far as it goes.
1347        let mut t = ftyp(b"mif1", &[b"miaf", b"avif"]);
1348        t.truncate(22);
1349        assert!(!probe(&t));
1350        // Largesize header.
1351        let mut large = Vec::new();
1352        large.extend_from_slice(&1u32.to_be_bytes());
1353        large.extend_from_slice(b"ftyp");
1354        large.extend_from_slice(&28u64.to_be_bytes());
1355        large.extend_from_slice(b"avif");
1356        large.extend_from_slice(&[0, 0, 0, 0]);
1357        large.extend_from_slice(b"mif1");
1358        assert!(probe(&large));
1359    }
1360
1361    #[test]
1362    fn rgb_round_trips_through_identity_planes() {
1363        let rgb: Vec<u8> = (0..3 * 4 * 2).map(|i| (i * 7 % 256) as u8).collect();
1364        let img = AvifImage::from_rgb8(4, 2, rgb.clone()).unwrap();
1365        assert_eq!(img.format(), PixelFormat::Yuv444P);
1366        assert!(img.color.is_identity_matrix());
1367        assert_eq!(img.to_rgb8(), rgb);
1368        let rgba = img.to_rgba8();
1369        assert_eq!(rgba.len(), 4 * 2 * 4);
1370        assert!(rgba.chunks_exact(4).all(|p| p[3] == 255));
1371        let with_alpha: Vec<u8> = (0..4 * 4 * 2).map(|i| (i * 13 % 256) as u8).collect();
1372        let img = AvifImage::from_rgba8(4, 2, with_alpha.clone()).unwrap();
1373        assert_eq!(img.format(), PixelFormat::Yuva444P);
1374        assert_eq!(img.to_rgba8(), with_alpha);
1375        assert_eq!(
1376            img.to_rgb8(),
1377            with_alpha
1378                .chunks_exact(4)
1379                .flat_map(|p| p[..3].to_vec())
1380                .collect::<Vec<_>>()
1381        );
1382    }
1383
1384    #[test]
1385    fn limited_range_luma_maps_to_rgb_extremes() {
1386        // BT.709 limited: Y = 16 → 0, Y = 235 → 255 with neutral chroma.
1387        let planes = vec![
1388            Plane::new(2, vec![16, 235, 128, 128]),
1389            Plane::new(1, vec![128]),
1390            Plane::new(1, vec![128]),
1391        ];
1392        let img = AvifImage::new(2, 2, PixelFormat::Yuv420P, planes)
1393            .unwrap()
1394            .with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 1));
1395        let rgb = img.to_rgb8();
1396        assert_eq!(&rgb[0..3], &[0, 0, 0]);
1397        assert_eq!(&rgb[3..6], &[255, 255, 255]);
1398        // Y = 128 limited → (128 − 16) × 255 / 219 = 130.4 → 130.
1399        assert_eq!(&rgb[6..9], &[130, 130, 130]);
1400        // The same samples full range: 16 and 235 stay.
1401        let full = img
1402            .clone()
1403            .with_color(ColorInfo::new(ColorRange::Full, 1, 1, 1));
1404        let rgb = full.to_rgb8();
1405        assert_eq!(&rgb[0..3], &[16, 16, 16]);
1406        assert_eq!(&rgb[3..6], &[235, 235, 235]);
1407    }
1408
1409    #[test]
1410    fn deep_samples_scale_to_eight_bits_by_rounding() {
1411        // 10-bit gray: 0 → 0, 1023 → 255, 512 → round(512*255/1023) = 128.
1412        let words: Vec<u8> = [0u16, 1023, 512, 2]
1413            .iter()
1414            .flat_map(|v| v.to_le_bytes())
1415            .collect();
1416        let img = AvifImage::new(4, 1, PixelFormat::Gray10Le, vec![Plane::new(8, words)]).unwrap();
1417        let rgb = img.to_rgb8();
1418        assert_eq!(rgb[0], 0);
1419        assert_eq!(rgb[3], 255);
1420        assert_eq!(rgb[6], 128);
1421        assert_eq!(rgb[9], 0); // 2 * 255 / 1023 = 0.498 → 0
1422        assert_eq!(img.as_bytes().map(|b| b.len()), Some(8));
1423    }
1424
1425    #[test]
1426    fn unknown_matrix_falls_back_to_bt601_in_the_infallible_view() {
1427        let planes = vec![
1428            Plane::new(2, vec![16, 235, 128, 128]),
1429            Plane::new(1, vec![128]),
1430            Plane::new(1, vec![128]),
1431        ];
1432        let img = AvifImage::new(2, 2, PixelFormat::Yuv420P, planes)
1433            .unwrap()
1434            .with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 131));
1435        assert!(matches!(img.try_to_rgb8(), Err(Error::Unsupported(_))));
1436        let bt601 = img
1437            .clone()
1438            .with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 6));
1439        assert_eq!(img.to_rgb8(), bt601.to_rgb8());
1440        assert_eq!(img.to_rgba8(), bt601.to_rgba8());
1441    }
1442
1443    #[test]
1444    fn packed_ya_carries_alpha() {
1445        let img = AvifImage::new(
1446            2,
1447            1,
1448            PixelFormat::Ya8,
1449            vec![Plane::new(4, vec![200, 10, 50, 255])],
1450        )
1451        .unwrap();
1452        let rgba = img.to_rgba8();
1453        assert_eq!(rgba, vec![200, 200, 200, 10, 50, 50, 50, 255]);
1454        assert!(img.has_alpha());
1455    }
1456
1457    #[test]
1458    fn inconsistent_planes_cannot_be_constructed() {
1459        // Plane count.
1460        assert!(matches!(
1461            AvifImage::new(4, 4, PixelFormat::Yuv420P, vec![Plane::new(4, vec![0; 4])]),
1462            Err(Error::InvalidData(_))
1463        ));
1464        // Short chroma plane (2x2 needs 4 bytes).
1465        let short = vec![
1466            Plane::new(4, vec![0; 16]),
1467            Plane::new(2, vec![0; 3]),
1468            Plane::new(2, vec![0; 4]),
1469        ];
1470        assert!(AvifImage::new(4, 4, PixelFormat::Yuv420P, short).is_err());
1471        // Zero extent.
1472        assert!(AvifImage::new(0, 4, PixelFormat::Gray8, vec![Plane::new(0, vec![])]).is_err());
1473        // Odd 4:2:0 extents use ceiling chroma; a wider stride is fine.
1474        let odd = vec![
1475            Plane::new(3, vec![0; 9]),
1476            Plane::new(4, vec![0; 8]),
1477            Plane::new(2, vec![0; 4]),
1478        ];
1479        let img = AvifImage::new(3, 3, PixelFormat::Yuv420P, odd).unwrap();
1480        assert!(img.as_bytes().is_none());
1481        assert_eq!(img.to_rgb8().len(), 27);
1482        // A picture whose public fields were assigned inconsistent
1483        // planes afterwards is reported, not a panic, by the try_ view.
1484        let mut broken =
1485            AvifImage::new(2, 2, PixelFormat::Gray8, vec![Plane::new(2, vec![0; 4])]).unwrap();
1486        broken.planes.clear();
1487        assert!(broken.try_to_rgb8().is_err());
1488        assert_eq!(broken.into_raw().len(), 0);
1489    }
1490
1491    #[test]
1492    fn quality_scale_maps_to_q_idx() {
1493        assert_eq!(EncodeOptions::default().with_quality(100).base_q_idx, 0);
1494        assert_eq!(EncodeOptions::default().with_quality(90).base_q_idx, 26);
1495        assert_eq!(EncodeOptions::default().with_quality(50).base_q_idx, 128);
1496        assert_eq!(EncodeOptions::default().with_quality(0).base_q_idx, 255);
1497        assert_eq!(EncodeOptions::default().with_quality(200).base_q_idx, 0);
1498        let o = EncodeOptions::new(3, 4, true);
1499        assert_eq!(
1500            (o.base_q_idx, o.alpha_q_idx, o.premultiplied_alpha),
1501            (3, 4, true)
1502        );
1503        assert_eq!(o.chroma, StillChroma::Yuv420);
1504        assert!(o.embed_exif && o.embed_xmp && o.embed_icc);
1505    }
1506
1507    #[test]
1508    fn exif_offset_word_is_stripped_and_rewritten() {
1509        let body = [0, 0, 0, 2, 0xAA, 0xBB, b'M', b'M', 0, 42];
1510        assert_eq!(exif_tiff_bytes(&body), Some(vec![b'M', b'M', 0, 42]));
1511        assert_eq!(exif_tiff_bytes(&[0, 0, 0, 9, 1]), None);
1512        assert_eq!(exif_tiff_bytes(&[0, 0]), None);
1513        assert_eq!(exif_item_body(b"II"), vec![0, 0, 0, 0, b'I', b'I']);
1514    }
1515
1516    #[test]
1517    fn decode_limits_are_reported_as_limit_exceeded() {
1518        let o = DecodeOptions::default()
1519            .with_max_width(Some(8))
1520            .with_max_pixels(Some(10));
1521        assert!(matches!(o.check_dims(9, 1), Err(Error::LimitExceeded(_))));
1522        assert!(matches!(o.check_dims(4, 4), Err(Error::LimitExceeded(_))));
1523        assert!(o.check_dims(5, 2).is_ok());
1524        assert!(matches!(
1525            DecodeOptions::default()
1526                .with_max_bytes(Some(3))
1527                .check_bytes(4),
1528            Err(Error::LimitExceeded(_))
1529        ));
1530        let unlimited = DecodeOptions::default()
1531            .with_max_width(None)
1532            .with_max_height(None)
1533            .with_max_pixels(None)
1534            .with_max_bytes(None);
1535        assert!(unlimited.check_dims(u32::MAX, u32::MAX).is_ok());
1536        assert!(unlimited.check_bytes(usize::MAX).is_ok());
1537    }
1538
1539    #[test]
1540    fn info_describes_fixture_headers() {
1541        let bytes = std::fs::read(concat!(
1542            env!("CARGO_MANIFEST_DIR"),
1543            "/tests/fixtures/yuv420_10bit_full.avif"
1544        ))
1545        .unwrap();
1546        assert!(probe(&bytes));
1547        let i = info(&bytes).unwrap();
1548        assert_eq!(i.format, PixelFormat::Yuv420P10Le);
1549        assert_eq!(i.bit_depth, 10);
1550        assert_eq!(i.frames, 1);
1551        assert!(!i.is_sequence);
1552        assert_eq!(i.color.range, ColorRange::Full);
1553        assert!(i.width > 0 && i.height > 0);
1554
1555        let bytes = std::fs::read(concat!(
1556            env!("CARGO_MANIFEST_DIR"),
1557            "/tests/fixtures/alpha_video.avif"
1558        ))
1559        .unwrap();
1560        let i = info(&bytes).unwrap();
1561        assert!(i.is_sequence);
1562        assert!(i.frames > 1);
1563
1564        let bytes = std::fs::read(concat!(
1565            env!("CARGO_MANIFEST_DIR"),
1566            "/tests/fixtures/monochrome.avif"
1567        ))
1568        .unwrap();
1569        let i = info(&bytes).unwrap();
1570        assert_eq!(i.format, PixelFormat::Gray8);
1571        assert!(!i.has_alpha);
1572        assert!(info(b"not an avif file at all, just bytes").is_err());
1573    }
1574}