Skip to main content

oxideav_avif/
api_codec.rs

1//! The `registry`-gated half of the image-crate API: `decode*` /
2//! `encode*` over [`crate::api::AvifImage`], the AV1 work done by
3//! `oxideav-av1`. The framework `Decoder` / `Encoder`
4//! ([`crate::AvifDecoder`] / [`crate::AvifEncoder`]) are thin adapters
5//! over the functions here.
6
7use std::io::{Read, Write};
8use std::time::Duration;
9
10use oxideav_core::frame::{VideoFrame, VideoPlane};
11use oxideav_core::{CodecId, CodecParameters, ColorSignal, Packet, TimeBase};
12
13use crate::api::{
14    burst_members, exif_item_body, exif_tiff_bytes, has_moov, item_colrs, AvifImage, ColorInfo,
15    ColorRange, DecodeOptions, EncodeOptions, Frame, Metadata, RgbImage, RgbaImage, StillChroma,
16};
17use crate::av1_config::{parse_av1c, Av1CodecConfig};
18use crate::avis::{parse_avis, sample_bytes};
19use crate::decoder::{
20    core_to_avif_frame, decode_item_output, infer_av1_pixmap, tone_map_of, validate_av1_config,
21    DecodeCtx, MAX_AV1_ITEM_BYTES,
22};
23use crate::error::{AvifError as Error, Result};
24use crate::frame_bridge::from_heif;
25use crate::image::{AvifPixelFormat, AvifPlane};
26use crate::inspect::{build_info, build_info_derived, build_info_grid, AvifInfo};
27use crate::meta::{Colr, ITEM_TYPE_IDEN, ITEM_TYPE_IOVL, ITEM_TYPE_TMAP};
28use crate::parser::{
29    audit_mif1, classify_brands, parse, parse_header, AvifHeader, BrandClass, ITEM_TYPE_AV01,
30    ITEM_TYPE_GRID,
31};
32use crate::signal::{color_signal_for, layout_of_record};
33use crate::still::{encode_still_auto, StillImage, StillProperties};
34use oxideav_heif::{Chroma, HeifFrame};
35
36/// Framework errors (the AV1 decoder's, the composition layer's) as
37/// this crate's.
38pub(crate) fn from_core_err(e: oxideav_core::Error) -> Error {
39    match e {
40        oxideav_core::Error::InvalidData(s) => Error::InvalidData(s),
41        oxideav_core::Error::Unsupported(s) => Error::Unsupported(s),
42        oxideav_core::Error::ResourceExhausted(s) => Error::LimitExceeded(s),
43        oxideav_core::Error::Io(e) => Error::Io(e),
44        other => Error::InvalidData(other.to_string()),
45    }
46}
47
48// ---------------------------------------------------------------------
49// Framework frame conversions
50// ---------------------------------------------------------------------
51
52/// The picture as a framework frame: the planes as given, the colour
53/// as the frame's `ColorSignal` side channel, and — for `Ya16Le`,
54/// whose words are wider than the coded depth — the depth on the
55/// significant-bits side channel.
56impl From<AvifImage> for VideoFrame {
57    fn from(img: AvifImage) -> Self {
58        let signal = color_signal_for(Some(&img.color.to_colr()));
59        let mut vf = VideoFrame {
60            pts: None,
61            planes: img
62                .planes
63                .into_iter()
64                .map(|p| VideoPlane {
65                    stride: p.stride,
66                    data: p.data,
67                })
68                .collect(),
69        };
70        vf.set_color_signal(signal);
71        if img.format == AvifPixelFormat::Ya16Le && img.bit_depth < 16 {
72            vf.set_significant_bits(vec![img.bit_depth]);
73        }
74        vf
75    }
76}
77
78impl From<ColorSignal> for ColorInfo {
79    fn from(s: ColorSignal) -> Self {
80        let range = match s.range {
81            oxideav_core::ColorRange::Full => ColorRange::Full,
82            oxideav_core::ColorRange::Limited => ColorRange::Limited,
83            _ => ColorRange::Unspecified,
84        };
85        ColorInfo::new(range, s.primaries.0, s.transfer.0, s.matrix.0)
86    }
87}
88
89/// The storage layout behind a framework pixel-format label, plus what
90/// the label itself says about the colour: the `YuvJ*` family is full
91/// range, the `Gbrp*` / `Gbrap*` family the identity matrix (planes
92/// G B R [A], exactly this crate's 4:4:4 layout).
93fn storage_layout_of(
94    fmt: oxideav_core::PixelFormat,
95) -> Result<(AvifPixelFormat, Option<ColorRange>, bool)> {
96    use oxideav_core::PixelFormat as P;
97    Ok(match fmt {
98        P::YuvJ420P => (AvifPixelFormat::Yuv420P, Some(ColorRange::Full), false),
99        P::YuvJ422P => (AvifPixelFormat::Yuv422P, Some(ColorRange::Full), false),
100        P::YuvJ444P => (AvifPixelFormat::Yuv444P, Some(ColorRange::Full), false),
101        P::Gbrp8 => (AvifPixelFormat::Yuv444P, None, true),
102        P::Gbrap8 => (AvifPixelFormat::Yuva444P, None, true),
103        P::Gbrp10Le => (AvifPixelFormat::Yuv444P10Le, None, true),
104        P::Gbrap10Le => (AvifPixelFormat::Yuva444P10Le, None, true),
105        P::Gbrp12Le => (AvifPixelFormat::Yuv444P12Le, None, true),
106        P::Gbrap12Le => (AvifPixelFormat::Yuva444P12Le, None, true),
107        other => (AvifPixelFormat::try_from(other)?, None, false),
108    })
109}
110
111impl AvifImage {
112    /// A picture from a framework frame plus the parameters that carry
113    /// what the frame does not: `width`, `height` and `pixel_format`
114    /// (the storage layouts by name, plus the `YuvJ*` full-range and
115    /// `Gbrp*` / `Gbrap*` identity-matrix labels this crate's decoder
116    /// emits). Only the image planes are copied (a side-channel record
117    /// is not a plane); `color` is the frame's `ColorSignal`, else the
118    /// parameters' stream-level one, else (fully unspecified) the MIAF
119    /// default, with the label's own range / matrix applied; the frame's
120    /// significant-bits channel sets the depth of a `Ya16Le` picture.
121    pub fn from_video_frame(frame: &VideoFrame, params: &CodecParameters) -> Result<Self> {
122        let width = params
123            .width
124            .ok_or_else(|| Error::invalid("avif: CodecParameters.width required"))?;
125        let height = params
126            .height
127            .ok_or_else(|| Error::invalid("avif: CodecParameters.height required"))?;
128        let label = params
129            .pixel_format
130            .ok_or_else(|| Error::invalid("avif: CodecParameters.pixel_format required"))?;
131        let (format, label_range, identity) = storage_layout_of(label)?;
132        // The frame's own record refines the stream-level signal; an
133        // entirely unspecified signal means the MIAF default.
134        let signal = frame.color_signal().unwrap_or(params.color_signal);
135        let mut color = if signal.is_unspecified() {
136            ColorInfo::default()
137        } else {
138            ColorInfo::from(signal)
139        };
140        if let Some(range) = label_range {
141            color.range = range;
142        }
143        if identity {
144            color.matrix = 0;
145        }
146        if color.range == ColorRange::Unspecified {
147            color.range = ColorRange::Full;
148        }
149        let bit_depth = match frame.significant_bits() {
150            Some(bits) if format == AvifPixelFormat::Ya16Le && !bits.is_empty() => bits[0],
151            _ => format.bit_depth(),
152        };
153        let planes = core_to_avif_frame(frame.clone()).planes;
154        Ok(Self::new(width, height, format, planes)?
155            .with_color(color)
156            .with_bit_depth(bit_depth))
157    }
158}
159
160impl TryFrom<(&VideoFrame, &CodecParameters)> for AvifImage {
161    type Error = Error;
162
163    fn try_from((frame, params): (&VideoFrame, &CodecParameters)) -> Result<Self> {
164        AvifImage::from_video_frame(frame, params)
165    }
166}
167
168// ---------------------------------------------------------------------
169// Decode
170// ---------------------------------------------------------------------
171
172/// A decoded primary picture with the container-side description the
173/// framework decoder also reports.
174pub(crate) struct Decoded {
175    pub image: AvifImage,
176    pub info: AvifInfo,
177}
178
179/// `strict` conformance refusals (see [`DecodeOptions::strict`]).
180fn strict_checks(brands: &BrandClass, mif1: &crate::derived::Mif1Compliance) -> Result<()> {
181    if !(brands.is_image || brands.is_sequence || brands.is_intra_only) {
182        return Err(Error::invalid(
183            "avif (strict): ftyp declares no AVIF brand (avif / avis / avio, av1-avif §6)",
184        ));
185    }
186    if mif1.claims_mif1
187        && !(mif1.has_hdlr && mif1.has_pitm && mif1.has_iinf && mif1.has_iloc && mif1.has_iprp)
188    {
189        return Err(Error::invalid(
190            "avif (strict): file claims mif1 but lacks a HEIF §10.2.1.1 mandatory box",
191        ));
192    }
193    Ok(())
194}
195
196/// Decode the primary item's output image with its colour and
197/// metadata — the one implementation behind [`decode_with`] and
198/// [`crate::AvifDecoder::decode_file`].
199pub(crate) fn decode_primary(file: &[u8], opts: &DecodeOptions) -> Result<Decoded> {
200    opts.check_bytes(file.len())?;
201    let hdr = parse_header(file)?;
202    let primary_id = hdr
203        .meta
204        .primary_item_id
205        .ok_or_else(|| Error::invalid("avif: missing pitm"))?;
206    let primary_info = hdr
207        .meta
208        .item_by_id(primary_id)
209        .ok_or_else(|| Error::invalid("avif: pitm references unknown item"))?
210        .clone();
211    let brands = classify_brands(&hdr.major_brand, &hdr.compatible_brands)?;
212    let mif1 = audit_mif1(file)?;
213    if opts.strict {
214        strict_checks(&brands, &mif1)?;
215    }
216    let info = if primary_info.item_type == ITEM_TYPE_GRID {
217        build_info_grid(&hdr, primary_id, brands, mif1)?
218    } else if primary_info.item_type == ITEM_TYPE_AV01 {
219        let img = parse(file)?;
220        let has_alpha = crate::alpha::find_alpha_item_id(&hdr.meta, primary_id).is_some();
221        build_info(&img, has_alpha, brands, mif1, file)?
222    } else if primary_info.item_type == ITEM_TYPE_IOVL
223        || primary_info.item_type == ITEM_TYPE_IDEN
224        || primary_info.item_type == ITEM_TYPE_TMAP
225    {
226        build_info_derived(&hdr, primary_id, brands, mif1)?
227    } else {
228        return Err(Error::unsupported(format!(
229            "avif: primary item type '{}' not supported",
230            String::from_utf8_lossy(&primary_info.item_type)
231        )));
232    };
233    // Limits before any pixel allocation.
234    opts.check_dims(info.width, info.height)?;
235    let image = picture_of(
236        &hdr,
237        primary_id,
238        opts,
239        info.colour.as_ref(),
240        (info.exif_item_id, info.xmp_item_id),
241    )?;
242    Ok(Decoded { image, info })
243}
244
245/// The output image of `item_id` (HEIF §6.3) as an [`AvifImage`]:
246/// derived-item composition, alpha, transforms, then colour and
247/// metadata. `fallback_colour` is the container-resolved `colr` of a
248/// derived item without its own; `meta_items` the Exif / XMP item ids
249/// attached to it.
250fn picture_of(
251    hdr: &AvifHeader<'_>,
252    item_id: u32,
253    opts: &DecodeOptions,
254    fallback_colour: Option<&Colr>,
255    meta_items: (Option<u32>, Option<u32>),
256) -> Result<AvifImage> {
257    let is_tmap = hdr
258        .meta
259        .item_by_id(item_id)
260        .map(|i| i.item_type == ITEM_TYPE_TMAP)
261        .unwrap_or(false);
262    let mut ctx = DecodeCtx::new(opts);
263    // Gain-map application: the `tmap` alternative of the item (the
264    // item itself when it is one) replaces it.
265    let target = if opts.tone_mapped {
266        tone_map_of(hdr, item_id).unwrap_or(item_id)
267    } else {
268        item_id
269    };
270    let composed: HeifFrame =
271        decode_item_output(hdr, target, 0, &mut ctx, true).map_err(from_core_err)?;
272    opts.check_dims(composed.width, composed.height)?;
273    let bit_depth = composed.format.bit_depth;
274    let (frame, format) = from_heif(&composed)?;
275    // The output image's colour: the item's own `nclx` `colr` (a
276    // reconstructed `tmap` carries the `tmap` item's — the alternate
277    // colorimetry, HEIF Amd 1 §6.6.2.4.1), else what the container
278    // resolved for a derived item (its first input's), else the MIAF
279    // §7.3.6.4 default.
280    let (nclx, icc) = item_colrs(&hdr.meta, target);
281    let colour = if target != item_id || is_tmap {
282        nclx.or_else(|| nclx_of(fallback_colour))
283    } else {
284        nclx.or_else(|| nclx_of(fallback_colour))
285    };
286    let icc = icc.or_else(|| match fallback_colour {
287        Some(Colr::Icc(bytes)) => Some(bytes.clone()),
288        _ => None,
289    });
290    let exif = meta_items
291        .0
292        .and_then(|id| hdr.item_data(id).ok())
293        .and_then(|body| exif_tiff_bytes(&body));
294    let xmp = meta_items
295        .1
296        .and_then(|id| hdr.item_data(id).ok())
297        .map(|b| b.into_owned());
298    Ok(AvifImage {
299        width: composed.width,
300        height: composed.height,
301        format,
302        planes: frame.planes,
303        color: ColorInfo::from_colr(colour.as_ref()),
304        metadata: Metadata::new(icc, exif, xmp, None),
305        bit_depth,
306    })
307}
308
309fn nclx_of(colr: Option<&Colr>) -> Option<Colr> {
310    match colr {
311        Some(c @ Colr::Nclx { .. }) => Some(c.clone()),
312        _ => None,
313    }
314}
315
316/// `true` when the file is an `avis` image sequence with a sample
317/// table (the brand says so and a `moov` is present).
318pub(crate) fn is_sequence_file(bytes: &[u8]) -> bool {
319    let Ok(Some((ftyp_payload, _))) = crate::box_parser::find_box(bytes, b"ftyp") else {
320        return false;
321    };
322    let Ok((major, _minor, compat)) = crate::parser::parse_ftyp(ftyp_payload) else {
323        return false;
324    };
325    let Ok(brands) = classify_brands(&major, &compat) else {
326        return false;
327    };
328    (brands.is_sequence || brands.has_msf1) && has_moov(bytes)
329}
330
331/// What a sequence decode established before its first frame.
332pub(crate) struct SequenceDecode {
333    /// The `av01` sample entry's `colr` as a signal (MIAF default
334    /// when absent).
335    pub signal: ColorSignal,
336    /// The storage layout the samples decode to, from the `av1C`.
337    pub layout: Option<AvifPixelFormat>,
338    /// The parsed `av1C` record.
339    pub config: Av1CodecConfig,
340}
341
342/// Decode every sample of an `avis` image sequence (av1-avif §6.3 +
343/// ISO/IEC 14496-12 §8) through one shared AV1 decoder so inter-frame
344/// state is preserved, handing each decoded frame to `sink` with the
345/// duration of the sample that produced it. `limit` stops after that
346/// many frames. The one implementation behind [`decode_all`] and
347/// [`crate::AvifDecoder::decode_avis_file`].
348pub(crate) fn decode_sequence_with<F>(
349    file: &[u8],
350    opts: &DecodeOptions,
351    limit: Option<usize>,
352    mut sink: F,
353) -> Result<SequenceDecode>
354where
355    F: FnMut(oxideav_core::Frame, &SequenceDecode, Duration) -> Result<()>,
356{
357    opts.check_bytes(file.len())?;
358    let meta = parse_avis(file)?;
359    if meta.samples.is_empty() {
360        return Err(Error::invalid("avis: track has zero samples"));
361    }
362    let av1c = meta.av1_codec_config.clone().ok_or_else(|| {
363        Error::invalid(
364            "avis: track stsd → av01 → av1C is missing — cannot seed AV1 decoder \
365             (av1-avif §2.2.1)",
366        )
367    })?;
368    // Eagerly validate the codec config — same shape as the
369    // still-image path uses for the av1C item property. 10/12-bit
370    // tracks pass straight through: AVIS sample frames are handed
371    // out exactly as the AV1 decoder emits them (little-endian
372    // 16-bit words for a `high_bitdepth` track), no composition
373    // step involved.
374    let cfg = parse_av1c(&av1c).map_err(from_core_err)?;
375    validate_av1_config(&cfg).map_err(from_core_err)?;
376    if let Some((w, h)) = meta.display_dims {
377        opts.check_dims(w, h)?;
378    }
379    let timescale = if meta.timescale == 0 {
380        1
381    } else {
382        meta.timescale
383    };
384    let media_timescale = meta.media_timescale.filter(|&t| t > 0).unwrap_or(timescale);
385    let seq = SequenceDecode {
386        signal: color_signal_for(meta.colr.as_ref()),
387        layout: layout_of_record(&cfg),
388        config: cfg,
389    };
390    let mut params = CodecParameters::video(CodecId::new("av1"));
391    if let Some((w, h)) = meta.display_dims {
392        params.width = Some(w);
393        params.height = Some(h);
394    }
395    params.extradata = av1c;
396
397    let mut av1 = oxideav_av1::registry::make_decoder(&params).map_err(from_core_err)?;
398    if opts.worker_count() > 1 {
399        av1.set_execution_context(&oxideav_core::ExecutionContext::with_threads(
400            opts.worker_count(),
401        ));
402    }
403    let mut delivered = 0usize;
404    let done = |delivered: usize| limit.is_some_and(|n| delivered >= n);
405    let mut cumulative_pts: u64 = 0;
406    let mut last_duration = Duration::ZERO;
407    for (i, s) in meta.samples.iter().enumerate() {
408        if done(delivered) {
409            break;
410        }
411        let bytes = sample_bytes(file, s)?;
412        if bytes.len() > MAX_AV1_ITEM_BYTES {
413            return Err(Error::invalid(format!(
414                "avis: sample {i} payload {} bytes exceeds soft cap {} bytes",
415                bytes.len(),
416                MAX_AV1_ITEM_BYTES
417            )));
418        }
419        // Build a packet on stream 0 with the movie timescale so
420        // the framework consumer recovers presentation order
421        // without an extra remapping step.
422        let pkt = Packet::new(0, TimeBase::new(1, timescale as i64), bytes.to_vec())
423            .with_pts(cumulative_pts as i64);
424        cumulative_pts = cumulative_pts.saturating_add(s.duration as u64);
425        last_duration = Duration::from_secs_f64(f64::from(s.duration) / f64::from(media_timescale));
426        av1.send_packet(&pkt).map_err(|e| {
427            Error::invalid(format!(
428                "avis: av1 decoder rejected sample {i} (offset={}, size={}, sync={}): {e}",
429                s.offset, s.size, s.is_sync
430            ))
431        })?;
432        // Drain frames after every packet — most AV1 packets emit a
433        // single decoded frame, but show-existing-frame OBUs can
434        // produce zero, and a single packet can occasionally yield
435        // more than one display frame.
436        loop {
437            match av1.receive_frame() {
438                Ok(frame) => {
439                    sink(frame, &seq, last_duration)?;
440                    delivered += 1;
441                    if done(delivered) {
442                        break;
443                    }
444                }
445                Err(oxideav_core::Error::NeedMore) => break,
446                Err(e) => return Err(from_core_err(e)),
447            }
448        }
449    }
450    if !done(delivered) {
451        // Flush any frames the decoder buffered past the last packet
452        // (re-ordering in standard AV1 is rare for AVIS, but the trait
453        // contract requires the call).
454        let _ = av1.flush();
455        while let Ok(frame) = av1.receive_frame() {
456            sink(frame, &seq, last_duration)?;
457            delivered += 1;
458            if done(delivered) {
459                break;
460            }
461        }
462    }
463    Ok(seq)
464}
465
466/// A sequence sample's decoded frame as a picture: geometry inferred
467/// from the plane layout at the record's depth, the sample entry's
468/// colour.
469fn sequence_picture(
470    frame: oxideav_core::Frame,
471    seq: &SequenceDecode,
472    opts: &DecodeOptions,
473) -> Result<AvifImage> {
474    let vf = match frame {
475        oxideav_core::Frame::Video(v) => v,
476        other => {
477            return Err(Error::unsupported(format!(
478                "avis: AV1 decoder returned a non-video frame: {other:?}"
479            )))
480        }
481    };
482    let (fmt_core, w, h) = infer_av1_pixmap(&vf, &seq.config).map_err(from_core_err)?;
483    opts.check_dims(w, h)?;
484    let format = AvifPixelFormat::try_from(fmt_core)?;
485    let planes = core_to_avif_frame(vf).planes;
486    Ok(AvifImage::new(w, h, format, planes)?.with_color(ColorInfo::from(seq.signal)))
487}
488
489/// Decode the primary picture with [`DecodeOptions::default`] — a
490/// still's primary item (coded or derived, alpha composited,
491/// transforms applied) or the first sample of an `avis` sequence.
492pub fn decode(bytes: &[u8]) -> Result<AvifImage> {
493    decode_with(bytes, &DecodeOptions::default())
494}
495
496/// [`decode`] with explicit limits / strictness / gain-map and layer
497/// choices.
498pub fn decode_with(bytes: &[u8], opts: &DecodeOptions) -> Result<AvifImage> {
499    opts.check_bytes(bytes.len())?;
500    if is_sequence_file(bytes) {
501        let mut first = None;
502        decode_sequence_with(bytes, opts, Some(1), |frame, seq, _| {
503            first = Some(sequence_picture(frame, seq, opts)?);
504            Ok(())
505        })?;
506        return first.ok_or_else(|| Error::invalid("avis: the sequence decoded to no frame"));
507    }
508    decode_primary(bytes, opts).map(|d| d.image)
509}
510
511/// The primary picture as tightly packed 8-bit RGB (alpha dropped).
512pub fn decode_rgb8(bytes: &[u8]) -> Result<RgbImage> {
513    let img = decode(bytes)?;
514    let data = img.try_to_rgb8()?;
515    Ok(RgbImage::new(img.width, img.height, data))
516}
517
518/// The primary picture as tightly packed 8-bit RGBA (alpha opaque
519/// when the file has none).
520pub fn decode_rgba8(bytes: &[u8]) -> Result<RgbaImage> {
521    let img = decode(bytes)?;
522    let data = img.try_to_rgba8()?;
523    Ok(RgbaImage::new(img.width, img.height, data))
524}
525
526/// Every picture of the file: the samples of an `avis` image sequence
527/// (each with its display `delay`), the entities of the primary's
528/// `brst` image-burst group (HEIF §6.8.9), else the primary alone.
529pub fn decode_all(bytes: &[u8]) -> Result<Vec<Frame>> {
530    decode_all_with(bytes, &DecodeOptions::default())
531}
532
533/// [`decode_all`] with explicit options.
534pub fn decode_all_with(bytes: &[u8], opts: &DecodeOptions) -> Result<Vec<Frame>> {
535    opts.check_bytes(bytes.len())?;
536    if is_sequence_file(bytes) {
537        let mut frames = Vec::new();
538        decode_sequence_with(bytes, opts, None, |frame, seq, delay| {
539            frames.push(Frame::new(sequence_picture(frame, seq, opts)?, Some(delay)));
540            Ok(())
541        })?;
542        return Ok(frames);
543    }
544    let decoded = decode_primary(bytes, opts)?;
545    let hdr = parse_header(bytes)?;
546    let primary_id = hdr
547        .meta
548        .primary_item_id
549        .ok_or_else(|| Error::invalid("avif: missing pitm"))?;
550    let burst = burst_members(&hdr.meta, primary_id);
551    if burst.len() < 2 {
552        return Ok(vec![Frame::new(decoded.image, None)]);
553    }
554    let mut frames = Vec::with_capacity(burst.len());
555    for id in burst {
556        let image = if id == primary_id {
557            decoded.image.clone()
558        } else {
559            picture_of(&hdr, id, opts, None, (None, None))?
560        };
561        frames.push(Frame::new(image, None));
562    }
563    Ok(frames)
564}
565
566/// [`decode`] from a reader (read to end first — AVIF item offsets are
567/// absolute, so the whole file is needed).
568pub fn decode_from<R: Read>(mut r: R) -> Result<AvifImage> {
569    let mut bytes = Vec::new();
570    r.read_to_end(&mut bytes)?;
571    decode(&bytes)
572}
573
574// ---------------------------------------------------------------------
575// Encode
576// ---------------------------------------------------------------------
577
578/// Samples of one plane as `u16` values: `rows` rows of `cols` samples
579/// at `bps` bytes each (little-endian words for 2), honouring the
580/// plane's stride.
581fn plane_samples(
582    plane: &AvifPlane,
583    cols: usize,
584    rows: usize,
585    bps: usize,
586    label: &str,
587) -> Result<Vec<u16>> {
588    let row_bytes = cols * bps;
589    if rows > 0
590        && (plane.stride < row_bytes || plane.data.len() < plane.stride * (rows - 1) + row_bytes)
591    {
592        return Err(Error::invalid(format!(
593            "avif encode: {label} plane too short ({} bytes, stride {}, need {rows} rows of {row_bytes})",
594            plane.data.len(),
595            plane.stride
596        )));
597    }
598    let mut out = Vec::with_capacity(cols * rows);
599    for r in 0..rows {
600        let row = &plane.data[r * plane.stride..r * plane.stride + row_bytes];
601        if bps == 1 {
602            out.extend(row.iter().map(|&b| u16::from(b)));
603        } else {
604            out.extend(
605                row.chunks_exact(2)
606                    .map(|w| u16::from_le_bytes([w[0], w[1]])),
607            );
608        }
609    }
610    Ok(out)
611}
612
613/// The still-encoder input for a picture: planes widened to `u16`,
614/// the `nclx` of its colour, metadata per the options.
615fn still_of(image: &AvifImage, opts: &EncodeOptions) -> Result<StillImage> {
616    if image.width == 0 || image.height == 0 {
617        return Err(Error::invalid(
618            "avif encode: dimensions must be at least 1x1",
619        ));
620    }
621    let format = image.format;
622    if image.planes.len() != format.plane_count() {
623        return Err(Error::invalid(format!(
624            "avif encode: {format:?} needs {} planes, image carries {}",
625            format.plane_count(),
626            image.planes.len()
627        )));
628    }
629    let bit_depth = image.bit_depth;
630    if !matches!(bit_depth, 8 | 10 | 12) {
631        return Err(Error::unsupported(format!(
632            "avif encode: {bit_depth}-bit samples — AV1 codes 8, 10 or 12"
633        )));
634    }
635    if format != AvifPixelFormat::Ya16Le && format.bit_depth() != bit_depth {
636        return Err(Error::invalid(format!(
637            "avif encode: bit_depth {bit_depth} does not match the {format:?} layout"
638        )));
639    }
640    let (w, h) = (image.width as usize, image.height as usize);
641    let bps = format.bytes_per_sample();
642    let mono = format.plane_count() == 1 || format.is_packed_ya();
643    let (sx, sy) = format.chroma_subsampling();
644    let chroma = if mono {
645        StillChroma::Monochrome
646    } else {
647        match (sx, sy) {
648            (1, 1) => StillChroma::Yuv420,
649            (1, 0) => StillChroma::Yuv422,
650            _ => StillChroma::Yuv444,
651        }
652    };
653    if (sx == 1 && image.width % 2 != 0) || (sy == 1 && image.height % 2 != 0) {
654        return Err(Error::unsupported(format!(
655            "avif encode: {format:?} at {}x{} — this encoder codes sub-sampled chroma only at \
656             even extents; use 4:4:4 (or `encode_rgb8`, which falls back to it)",
657            image.width, image.height
658        )));
659    }
660    let (y, u, v, alpha) = if format.is_packed_ya() {
661        let packed = plane_samples(&image.planes[0], w * 2, h, bps, "packed YA")?;
662        let mut y = Vec::with_capacity(w * h);
663        let mut a = Vec::with_capacity(w * h);
664        for px in packed.chunks_exact(2) {
665            y.push(px[0]);
666            a.push(px[1]);
667        }
668        (y, Vec::new(), Vec::new(), Some(a))
669    } else {
670        let y = plane_samples(&image.planes[0], w, h, bps, "Y")?;
671        let (u, v) = if mono {
672            (Vec::new(), Vec::new())
673        } else {
674            let cw = w.div_ceil(1 << sx);
675            let ch = h.div_ceil(1 << sy);
676            (
677                plane_samples(&image.planes[1], cw, ch, bps, "Cb")?,
678                plane_samples(&image.planes[2], cw, ch, bps, "Cr")?,
679            )
680        };
681        let alpha = if format.has_alpha() {
682            let idx = image.planes.len() - 1;
683            Some(plane_samples(&image.planes[idx], w, h, bps, "alpha")?)
684        } else {
685            None
686        };
687        (y, u, v, alpha)
688    };
689    let mut still = StillImage::yuv(image.width, image.height, bit_depth, chroma, y, u, v)?;
690    if let Some(a) = alpha {
691        still = still.with_alpha(a)?;
692    }
693    still = still.with_colr(image.color.to_colr());
694    let mut props = StillProperties::default();
695    if opts.embed_exif {
696        props.exif = image.metadata.exif.as_deref().map(exif_item_body);
697    }
698    if opts.embed_xmp {
699        props.xmp = image.metadata.xmp.clone();
700    }
701    if opts.embed_icc {
702        props.icc = image.metadata.icc.clone();
703    }
704    Ok(still.with_props(props))
705}
706
707/// Encode `frames` as one file — the mirror of [`decode_all`]. Frames
708/// without a `delay` are coded as image items: one is exactly
709/// [`encode`], several become the primary plus a `brst` image-burst
710/// group ([`crate::encode_still_burst`]; each member with its own
711/// colour, ICC, alpha auxiliary and `clap`, Exif / XMP on the primary).
712/// Frames with a `delay` become the samples of an `avis` image
713/// sequence ([`crate::encode_sequence`] at timescale 1000 with each
714/// frame's delay in milliseconds, `opts.base_q_idx` as the quality);
715/// every frame must then carry a delay, since an `avis` file holds
716/// samples only, and the sequence encoder carries no alpha
717/// (`Error::Unsupported`). All frames share frame 0's geometry, depth
718/// and chroma layout.
719///
720/// `decode_all(encode_all(frames)) == frames` holds for planes and
721/// colour at the lossless default (`base_q_idx = 0`); burst members
722/// read back without the primary's Exif / XMP, sequence samples with
723/// their delays.
724pub fn encode_all(frames: &[Frame], opts: &EncodeOptions) -> Result<Vec<u8>> {
725    if frames.is_empty() {
726        return Err(Error::invalid("avif encode_all: no frames"));
727    }
728    let timed = frames.iter().filter(|f| f.delay.is_some()).count();
729    if timed == 0 {
730        if let [single] = frames {
731            return encode(&single.image, opts);
732        }
733        let stills: Vec<StillImage> = frames
734            .iter()
735            .map(|f| still_of(&f.image, opts))
736            .collect::<Result<_>>()?;
737        return crate::still::encode_still_burst(&stills, opts);
738    }
739    if timed != frames.len() {
740        return Err(Error::invalid(
741            "avif encode_all: an avis image sequence holds timed samples only — give every \
742             frame a delay, or none for an image burst",
743        ));
744    }
745    let stills: Vec<StillImage> = frames
746        .iter()
747        .map(|f| still_of(&f.image, opts))
748        .collect::<Result<_>>()?;
749    let durations: Vec<u32> = frames
750        .iter()
751        .map(|f| {
752            f.delay
753                .map(|d| u32::try_from(d.as_millis()).unwrap_or(u32::MAX))
754                .unwrap_or(0)
755        })
756        .collect();
757    let seq_opts = crate::sequence::SequenceEncodeOptions::default()
758        .with_timescale(1000)
759        .with_base_q_idx(opts.base_q_idx);
760    crate::sequence::encode_sequence_timed(&stills, &seq_opts, Some(&durations))
761}
762
763/// Encode a picture as it is: its layout and depth (8 / 10 / 12-bit
764/// 4:2:0 / 4:2:2 / 4:4:4 / monochrome, alpha as the AV1-coded alpha
765/// auxiliary item, an identity-matrix picture as the RGB it holds),
766/// its colour as the `colr` `nclx`, its metadata as `Exif` / XMP
767/// items and an ICC `colr` (per [`EncodeOptions`]). A canvas beyond
768/// 4096 coded pixels per axis is tiled as a `grid`. Lossless at the
769/// default `base_q_idx = 0`. [`Error::Unsupported`] for a layout this
770/// encoder does not carry (sub-sampled chroma at odd extents), never a
771/// silent conversion.
772pub fn encode(image: &AvifImage, opts: &EncodeOptions) -> Result<Vec<u8>> {
773    let still = still_of(image, opts)?;
774    encode_still_auto(&still, opts)
775}
776
777/// RGB(A) bytes as the still-encoder input at the MIAF default colour
778/// (BT.709 primaries, sRGB transfer, BT.601 matrix, full range):
779/// converted to 4:4:4 Y′CbCr exactly (H.273 §8.3, round-to-nearest),
780/// then the chroma planes box-filtered to the requested layout
781/// (`(a + b + c + d + 2) >> 2` per 2×2 for 4:2:0, `(a + b + 1) >> 1`
782/// per pair for 4:2:2). Odd extents fall back to 4:4:4, which AV1
783/// sub-sampling cannot represent at this encoder's even-extent rule.
784fn still_of_rgb(
785    width: u32,
786    height: u32,
787    data: &[u8],
788    channels: usize,
789    opts: &EncodeOptions,
790) -> Result<StillImage> {
791    still_of_rgb_in(
792        width,
793        height,
794        data,
795        channels,
796        ColorInfo::miaf_default(),
797        opts.chroma,
798    )
799}
800
801/// [`still_of_rgb`] converting with `color` (a `nclx` description whose
802/// matrix the converter has a kernel for; the result carries it as its
803/// `colr`) into `chroma` (odd extents fall back to 4:4:4) — the
804/// registry encoder's lossy packed-RGB path.
805pub(crate) fn still_of_rgb_in(
806    width: u32,
807    height: u32,
808    data: &[u8],
809    channels: usize,
810    color: ColorInfo,
811    chroma: StillChroma,
812) -> Result<StillImage> {
813    let n = width as usize * height as usize;
814    if width == 0 || height == 0 {
815        return Err(Error::invalid(
816            "avif encode: dimensions must be at least 1x1",
817        ));
818    }
819    if data.len() != n * channels {
820        return Err(Error::invalid(format!(
821            "avif encode: {width}x{height} with {channels} channels needs {} bytes, got {}",
822            n * channels,
823            data.len()
824        )));
825    }
826    let colr = oxideav_heif::props::Colr::from(&color.to_colr());
827    let rgb = oxideav_heif::rgb::RgbImage16::new(
828        width,
829        height,
830        channels,
831        8,
832        data.iter().map(|&b| u16::from(b)).collect(),
833    );
834    let mut chroma = chroma;
835    let (sx, sy) = chroma.subsampling();
836    if (sx == 1 && width % 2 != 0) || (sy == 1 && height % 2 != 0) {
837        chroma = StillChroma::Yuv444;
838    }
839    let target = if chroma == StillChroma::Monochrome {
840        Chroma::Mono
841    } else {
842        Chroma::Yuv444
843    };
844    let frame = oxideav_heif::rgb::from_rgb(&rgb, Some(&colr), target)?;
845    let (w, h) = (width as usize, height as usize);
846    let heif_plane = |idx: usize| -> Result<Vec<u16>> {
847        let p = frame
848            .planes
849            .get(idx)
850            .ok_or_else(|| Error::invalid("avif encode: RGB conversion returned too few planes"))?;
851        plane_samples(
852            &AvifPlane::new(p.stride, p.data.clone()),
853            w,
854            h,
855            1,
856            "converted",
857        )
858    };
859    let y = heif_plane(0)?;
860    let (u, v) = match chroma {
861        StillChroma::Monochrome => (Vec::new(), Vec::new()),
862        StillChroma::Yuv444 => (heif_plane(1)?, heif_plane(2)?),
863        StillChroma::Yuv420 => (
864            box_filter(&heif_plane(1)?, w, h, 1, 1),
865            box_filter(&heif_plane(2)?, w, h, 1, 1),
866        ),
867        StillChroma::Yuv422 => (
868            box_filter(&heif_plane(1)?, w, h, 1, 0),
869            box_filter(&heif_plane(2)?, w, h, 1, 0),
870        ),
871    };
872    let mut still = StillImage::yuv(width, height, 8, chroma, y, u, v)?;
873    if channels == 4 {
874        let a: Vec<u16> = data.chunks_exact(4).map(|px| u16::from(px[3])).collect();
875        still = still.with_alpha(a)?;
876    }
877    Ok(still.with_colr(color.to_colr()))
878}
879
880/// Average a full-resolution plane down by `2^sx × 2^sy` with
881/// round-half-up; extents must be even on each sub-sampled axis.
882fn box_filter(plane: &[u16], w: usize, h: usize, sx: u32, sy: u32) -> Vec<u16> {
883    let (cw, ch) = (w >> sx, h >> sy);
884    let count = 1u32 << (sx + sy);
885    let mut out = Vec::with_capacity(cw * ch);
886    for cy in 0..ch {
887        for cx in 0..cw {
888            let mut sum = 0u32;
889            for dy in 0..(1usize << sy) {
890                for dx in 0..(1usize << sx) {
891                    sum += u32::from(plane[(cy * (1 << sy) + dy) * w + cx * (1 << sx) + dx]);
892                }
893            }
894            out.push(((sum + count / 2) / count) as u16);
895        }
896    }
897    out
898}
899
900/// Encode tightly packed 8-bit RGB as an AVIF at the MIAF default
901/// colour signalling (BT.709 / sRGB / BT.601, full range) in
902/// `opts.chroma` — 4:2:0 by default, 4:4:4 at odd extents (see
903/// [`EncodeOptions::chroma`]); `opts.base_q_idx` / [`EncodeOptions::with_quality`]
904/// set the quality. For byte-exact RGB use
905/// `encode(&AvifImage::from_rgb8(..).unwrap(), ..)`, which codes the identity
906/// matrix in 4:4:4.
907pub fn encode_rgb8(width: u32, height: u32, rgb: &[u8], opts: &EncodeOptions) -> Result<Vec<u8>> {
908    let still = still_of_rgb(width, height, rgb, 3, opts)?;
909    encode_still_auto(&still, opts)
910}
911
912/// [`encode_rgb8`] for tightly packed 8-bit RGBA: the alpha channel
913/// becomes the AV1-coded alpha auxiliary item (av1-avif §4.1, coded
914/// at `opts.alpha_q_idx`, `prem` signalled per
915/// `opts.premultiplied_alpha`).
916pub fn encode_rgba8(width: u32, height: u32, rgba: &[u8], opts: &EncodeOptions) -> Result<Vec<u8>> {
917    let still = still_of_rgb(width, height, rgba, 4, opts)?;
918    encode_still_auto(&still, opts)
919}
920
921/// [`encode`] into a writer.
922pub fn encode_to<W: Write>(image: &AvifImage, opts: &EncodeOptions, mut w: W) -> Result<()> {
923    let bytes = encode(image, opts)?;
924    w.write_all(&bytes)?;
925    Ok(())
926}
927
928#[cfg(test)]
929mod tests {
930    use super::*;
931    use crate::api::{PixelFormat, Plane};
932    use crate::info;
933
934    fn fixture(name: &str) -> Vec<u8> {
935        std::fs::read(format!(
936            "{}/tests/fixtures/{name}",
937            env!("CARGO_MANIFEST_DIR")
938        ))
939        .unwrap()
940    }
941
942    #[test]
943    fn encode_all_mirrors_decode_all() {
944        use std::time::Duration;
945        let pic = |seed: u8, alpha: bool| -> AvifImage {
946            let n = 16 * 8;
947            if alpha {
948                let rgba: Vec<u8> = (0..n * 4)
949                    .map(|i| ((i * 31 + seed as usize * 17) % 251) as u8)
950                    .collect();
951                AvifImage::from_rgba8(16, 8, rgba).unwrap()
952            } else {
953                let rgb: Vec<u8> = (0..n * 3)
954                    .map(|i| ((i * 29 + seed as usize * 13) % 251) as u8)
955                    .collect();
956                AvifImage::from_rgb8(16, 8, rgb).unwrap()
957            }
958        };
959        let opts = EncodeOptions::default();
960
961        // One delay-less frame is exactly `encode`.
962        let one = pic(1, false);
963        assert_eq!(
964            encode_all(&[Frame::new(one.clone(), None)], &opts).unwrap(),
965            encode(&one, &opts).unwrap()
966        );
967
968        // Several delay-less frames: a `brst` burst, lossless, with
969        // each member's alpha; the primary is frame 0.
970        let burst = vec![
971            Frame::new(pic(1, true), None),
972            Frame::new(pic(2, true), None),
973            Frame::new(pic(3, true), None),
974        ];
975        let bytes = encode_all(&burst, &opts).unwrap();
976        assert_eq!(info(&bytes).unwrap().frames, 3);
977        assert_eq!(decode(&bytes).unwrap(), burst[0].image);
978        assert_eq!(decode_all(&bytes).unwrap(), burst);
979
980        // Timed frames: an `avis` sequence with the delays in ms.
981        let seq = vec![
982            Frame::new(pic(4, false), Some(Duration::from_millis(40))),
983            Frame::new(pic(5, false), Some(Duration::from_millis(1500))),
984            Frame::new(pic(6, false), Some(Duration::from_millis(1))),
985        ];
986        let bytes = encode_all(&seq, &opts).unwrap();
987        assert!(is_sequence_file(&bytes));
988        assert_eq!(info(&bytes).unwrap().frames, 3);
989        let back = decode_all(&bytes).unwrap();
990        assert_eq!(back.len(), 3);
991        for (i, (got, want)) in back.iter().zip(&seq).enumerate() {
992            assert_eq!(got.delay, want.delay, "sample {i} delay");
993            assert_eq!(got.image.planes, want.image.planes, "sample {i} planes");
994            assert_eq!(got.image.format, want.image.format, "sample {i} layout");
995            assert_eq!(got.image.color, want.image.color, "sample {i} colour");
996        }
997
998        // Rejections: nothing; mixed timed / untimed; alpha in a sequence.
999        assert!(matches!(encode_all(&[], &opts), Err(Error::InvalidData(_))));
1000        let mixed = [seq[0].clone(), burst[0].clone()];
1001        assert!(matches!(
1002            encode_all(&mixed, &opts),
1003            Err(Error::InvalidData(_))
1004        ));
1005        let timed_alpha = [
1006            Frame::new(pic(1, true), Some(Duration::from_millis(10))),
1007            Frame::new(pic(2, true), Some(Duration::from_millis(10))),
1008        ];
1009        assert!(matches!(
1010            encode_all(&timed_alpha, &opts),
1011            Err(Error::Unsupported(_))
1012        ));
1013        // Fallible constructors.
1014        assert!(matches!(
1015            AvifImage::from_rgb8(2, 2, vec![0; 11]),
1016            Err(Error::InvalidData(_))
1017        ));
1018        assert!(matches!(
1019            AvifImage::from_rgba8(0, 2, vec![]),
1020            Err(Error::InvalidData(_))
1021        ));
1022    }
1023
1024    #[test]
1025    fn identity_rgb_fixture_decodes_to_the_pinned_bytes() {
1026        let bytes = fixture("identity_rgb_lossless.avif");
1027        let expect = fixture("identity_rgb_lossless.rgb");
1028        let img = decode(&bytes).unwrap();
1029        assert_eq!(img.format(), PixelFormat::Yuv444P);
1030        assert!(img.color.is_identity_matrix());
1031        assert_eq!(img.to_rgb8(), expect);
1032        let rgb = decode_rgb8(&bytes).unwrap();
1033        assert_eq!((rgb.width, rgb.height), (img.width, img.height));
1034        assert_eq!(rgb.data, expect);
1035        let rgba = decode_rgba8(&bytes).unwrap();
1036        assert_eq!(rgba.data.len(), expect.len() / 3 * 4);
1037        assert!(rgba.data.chunks_exact(4).all(|p| p[3] == 255));
1038        let from_reader = decode_from(std::io::Cursor::new(&bytes)).unwrap();
1039        assert_eq!(from_reader, img);
1040    }
1041
1042    #[test]
1043    fn lossless_round_trip_is_exact_for_planes_colour_and_metadata() {
1044        let rgba: Vec<u8> = (0..16 * 8 * 4).map(|i| (i * 31 % 251) as u8).collect();
1045        let img = AvifImage::from_rgba8(16, 8, rgba.clone())
1046            .unwrap()
1047            .with_metadata(Metadata::new(
1048                Some(vec![0x11; 8]),
1049                Some(b"MM\0\x2a\0\0\0\x08".to_vec()),
1050                Some(b"<x:xmpmeta/>".to_vec()),
1051                None,
1052            ));
1053        let bytes = encode(&img, &EncodeOptions::default()).unwrap();
1054        assert!(crate::probe(&bytes));
1055        let info = crate::info(&bytes).unwrap();
1056        assert!(info.has_alpha && info.has_exif && info.has_xmp && info.has_icc);
1057        assert_eq!(info.format, PixelFormat::Yuva444P);
1058        assert!(info.color.is_identity_matrix());
1059        let back = decode(&bytes).unwrap();
1060        assert_eq!(back.width, 16);
1061        assert_eq!(back.height, 8);
1062        assert_eq!(back.format, img.format);
1063        assert_eq!(back.planes, img.planes);
1064        assert_eq!(back.color, img.color);
1065        assert_eq!(back.metadata, img.metadata);
1066        assert_eq!(back.to_rgba8(), rgba);
1067        // Metadata embedding can be switched off per block.
1068        let bare = encode(
1069            &img,
1070            &EncodeOptions::default()
1071                .with_embed_exif(false)
1072                .with_embed_xmp(false)
1073                .with_embed_icc(false),
1074        )
1075        .unwrap();
1076        let info = crate::info(&bare).unwrap();
1077        assert!(!info.has_exif && !info.has_xmp && !info.has_icc);
1078        assert!(decode(&bare).unwrap().metadata.is_empty());
1079        let mut sink = Vec::new();
1080        encode_to(&img, &EncodeOptions::default(), &mut sink).unwrap();
1081        assert_eq!(sink, bytes);
1082    }
1083
1084    #[test]
1085    fn rgb_one_call_paths_code_420_by_default() {
1086        let rgb: Vec<u8> = (0..16 * 8 * 3).map(|i| (i * 7 % 256) as u8).collect();
1087        let bytes = encode_rgb8(16, 8, &rgb, &EncodeOptions::default()).unwrap();
1088        let info = crate::info(&bytes).unwrap();
1089        assert_eq!(info.format, PixelFormat::Yuv420P);
1090        assert_eq!(info.color, ColorInfo::miaf_default());
1091        let back = decode_rgb8(&bytes).unwrap();
1092        assert_eq!((back.width, back.height), (16, 8));
1093        // Lossless luma, box-filtered chroma: close, not exact.
1094        let err: u32 = back
1095            .data
1096            .iter()
1097            .zip(&rgb)
1098            .map(|(a, b)| u32::from(a.abs_diff(*b)))
1099            .sum();
1100        assert!(err / (16 * 8 * 3) < 24, "mean error {}", err / (16 * 8 * 3));
1101        // Odd extents fall back to 4:4:4, and 4:4:4 at q 0 is lossless
1102        // up to the forward/inverse matrix rounding.
1103        let odd: Vec<u8> = (0..5 * 3 * 3).map(|i| (i * 11 % 256) as u8).collect();
1104        let bytes = encode_rgb8(5, 3, &odd, &EncodeOptions::default()).unwrap();
1105        assert_eq!(crate::info(&bytes).unwrap().format, PixelFormat::Yuv444P);
1106        let back = decode_rgb8(&bytes).unwrap();
1107        assert!(back.data.iter().zip(&odd).all(|(a, b)| a.abs_diff(*b) <= 2));
1108        // RGBA: alpha as the auxiliary, exact.
1109        let rgba: Vec<u8> = (0..8 * 8 * 4).map(|i| (i * 13 % 256) as u8).collect();
1110        let bytes = encode_rgba8(8, 8, &rgba, &EncodeOptions::default()).unwrap();
1111        let info = crate::info(&bytes).unwrap();
1112        assert!(info.has_alpha);
1113        assert_eq!(info.format, PixelFormat::Yuva420P);
1114        let back = decode_rgba8(&bytes).unwrap();
1115        assert!(back
1116            .data
1117            .chunks_exact(4)
1118            .zip(rgba.chunks_exact(4))
1119            .all(|(a, b)| a[3] == b[3]));
1120        // Quality from the options changes the output.
1121        let lossy = encode_rgb8(16, 8, &rgb, &EncodeOptions::default().with_quality(50)).unwrap();
1122        assert_ne!(
1123            lossy,
1124            encode_rgb8(16, 8, &rgb, &EncodeOptions::default()).unwrap()
1125        );
1126        assert!(decode(&lossy).is_ok());
1127        // Wrong byte count is refused.
1128        assert!(matches!(
1129            encode_rgb8(4, 4, &[0; 10], &EncodeOptions::default()),
1130            Err(Error::InvalidData(_))
1131        ));
1132    }
1133
1134    #[test]
1135    fn encode_refuses_unsupported_layouts_without_converting() {
1136        // Odd 4:2:0 extents: AV1 could, this encoder does not.
1137        let img = AvifImage::new(
1138            3,
1139            3,
1140            PixelFormat::Yuv420P,
1141            vec![
1142                Plane::new(3, vec![0; 9]),
1143                Plane::new(2, vec![0; 4]),
1144                Plane::new(2, vec![0; 4]),
1145            ],
1146        )
1147        .unwrap();
1148        assert!(matches!(
1149            encode(&img, &EncodeOptions::default()),
1150            Err(Error::Unsupported(_))
1151        ));
1152        // Plane count mismatch (fields assigned after construction).
1153        let mut img =
1154            AvifImage::new(4, 4, PixelFormat::Gray8, vec![Plane::new(4, vec![0; 16])]).unwrap();
1155        img.format = PixelFormat::Yuv420P;
1156        assert!(matches!(
1157            encode(&img, &EncodeOptions::default()),
1158            Err(Error::InvalidData(_))
1159        ));
1160    }
1161
1162    #[test]
1163    fn deep_gray_and_packed_ya_round_trip() {
1164        // 10-bit gray + alpha → Ya16Le after decode; encode takes it back.
1165        let words: Vec<u16> = (0..8 * 4).map(|i| (i * 33 % 1024) as u16).collect();
1166        let alpha: Vec<u16> = (0..8 * 4).map(|i| (1023 - i * 29 % 1024) as u16).collect();
1167        let still = StillImage::yuv(
1168            8,
1169            4,
1170            10,
1171            StillChroma::Monochrome,
1172            words.clone(),
1173            vec![],
1174            vec![],
1175        )
1176        .unwrap()
1177        .with_alpha(alpha.clone())
1178        .unwrap();
1179        let bytes = crate::still::encode_still(&still, &EncodeOptions::default()).unwrap();
1180        let img = decode(&bytes).unwrap();
1181        assert_eq!(img.format, PixelFormat::Ya16Le);
1182        assert_eq!(img.bit_depth, 10);
1183        assert_eq!(crate::info(&bytes).unwrap().format, PixelFormat::Ya16Le);
1184        let again = encode(&img, &EncodeOptions::default()).unwrap();
1185        let back = decode(&again).unwrap();
1186        assert_eq!(back.planes, img.planes);
1187        assert_eq!(back.bit_depth, 10);
1188        // The RGBA view scales 10 → 8 bits.
1189        let rgba = img.to_rgba8();
1190        assert_eq!(rgba.len(), 8 * 4 * 4);
1191        assert_eq!(rgba[0], ((u32::from(words[0]) * 255 + 511) / 1023) as u8);
1192        assert_eq!(rgba[3], ((u32::from(alpha[0]) * 255 + 511) / 1023) as u8);
1193    }
1194
1195    #[test]
1196    fn sequence_decodes_every_sample_with_delays() {
1197        let bytes = fixture("alpha_video.avif");
1198        let info = crate::info(&bytes).unwrap();
1199        assert!(info.is_sequence);
1200        let frames = decode_all(&bytes).unwrap();
1201        assert_eq!(frames.len(), info.frames as usize);
1202        assert!(frames
1203            .iter()
1204            .all(|f| f.delay.is_some_and(|d| d > Duration::ZERO)));
1205        assert!(frames
1206            .iter()
1207            .all(|f| f.image.width == frames[0].image.width));
1208        let first = decode(&bytes).unwrap();
1209        assert_eq!(first, frames[0].image);
1210        let rgb = first.to_rgb8();
1211        assert_eq!(rgb.len(), first.width as usize * first.height as usize * 3);
1212    }
1213
1214    #[test]
1215    fn stills_decode_all_as_one_frame() {
1216        let bytes = fixture("kimono_rotate90.avif");
1217        let frames = decode_all(&bytes).unwrap();
1218        assert_eq!(frames.len(), 1);
1219        assert_eq!(frames[0].delay, None);
1220        assert_eq!(frames[0].image, decode(&bytes).unwrap());
1221    }
1222
1223    #[test]
1224    fn limits_and_strictness_refuse_before_decoding() {
1225        let bytes = fixture("monochrome.avif");
1226        let info = crate::info(&bytes).unwrap();
1227        let too_small = DecodeOptions::default().with_max_width(Some(info.width - 1));
1228        assert!(matches!(
1229            decode_with(&bytes, &too_small),
1230            Err(Error::LimitExceeded(_))
1231        ));
1232        let few_pixels = DecodeOptions::default().with_max_pixels(Some(16));
1233        assert!(matches!(
1234            decode_with(&bytes, &few_pixels),
1235            Err(Error::LimitExceeded(_))
1236        ));
1237        let few_bytes = DecodeOptions::default().with_max_bytes(Some(bytes.len() as u64 - 1));
1238        assert!(matches!(
1239            decode_with(&bytes, &few_bytes),
1240            Err(Error::LimitExceeded(_))
1241        ));
1242        assert!(matches!(
1243            decode_all_with(&bytes, &few_bytes),
1244            Err(Error::LimitExceeded(_))
1245        ));
1246        // The fixture declares the AVIF brand, so strict passes.
1247        let strict = decode_with(&bytes, &DecodeOptions::default().with_strict(true)).unwrap();
1248        assert_eq!(strict, decode(&bytes).unwrap());
1249        // A file whose ftyp carries only HEIF structural brands is
1250        // refused under strict and accepted otherwise.
1251        let mut structural = bytes.clone();
1252        let pos = structural
1253            .windows(4)
1254            .position(|w| w == b"avif")
1255            .expect("brand in ftyp");
1256        structural[pos..pos + 4].copy_from_slice(b"mif1");
1257        while let Some(p) = structural[..64].windows(4).position(|w| w == b"avif") {
1258            structural[p..p + 4].copy_from_slice(b"mif1");
1259        }
1260        assert!(decode(&structural).is_ok());
1261        assert!(matches!(
1262            decode_with(&structural, &DecodeOptions::default().with_strict(true)),
1263            Err(Error::InvalidData(_))
1264        ));
1265    }
1266
1267    #[test]
1268    fn framework_frame_conversions_round_trip() {
1269        let img = AvifImage::from_rgba8(4, 2, (0..32).collect())
1270            .unwrap()
1271            .with_bit_depth(8);
1272        let vf = VideoFrame::from(img.clone());
1273        // Four image planes; the colour signal rides as a side-channel
1274        // record behind them.
1275        assert_eq!(vf.image_plane_count(), 4);
1276        assert_eq!(
1277            vf.color_signal(),
1278            Some(color_signal_for(Some(&img.color.to_colr())))
1279        );
1280        let mut params = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
1281        params.width = Some(4);
1282        params.height = Some(2);
1283        params.pixel_format = Some(oxideav_core::PixelFormat::Yuva444P);
1284        let back = AvifImage::from_video_frame(&vf, &params).unwrap();
1285        assert_eq!(back, img);
1286        // The decoder's own label for the same picture works too.
1287        params.pixel_format = Some(oxideav_core::PixelFormat::Gbrap8);
1288        let back = AvifImage::try_from((&vf, &params)).unwrap();
1289        assert_eq!(back, img);
1290        // Missing geometry is an error, not a guess.
1291        params.width = None;
1292        assert!(AvifImage::from_video_frame(&vf, &params).is_err());
1293        // Ya16Le carries its depth on the significant-bits channel.
1294        let ya = AvifImage::new(2, 1, PixelFormat::Ya16Le, vec![Plane::new(8, vec![0; 8])])
1295            .unwrap()
1296            .with_bit_depth(12);
1297        let vf = VideoFrame::from(ya.clone());
1298        assert_eq!(vf.significant_bits(), Some(&[12u8][..]));
1299        let mut params = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
1300        params.width = Some(2);
1301        params.height = Some(1);
1302        params.pixel_format = Some(oxideav_core::PixelFormat::Ya16Le);
1303        assert_eq!(
1304            AvifImage::from_video_frame(&vf, &params).unwrap().bit_depth,
1305            12
1306        );
1307    }
1308}