Skip to main content

oximedia_codec/avif/
mod.rs

1//! AVIF (AV1 Image File Format) encoder and decoder.
2//!
3//! AVIF stores still images using AV1 intra-frame compression inside an
4//! ISOBMFF (ISO Base Media File Format) container.  This implementation
5//! writes and reads a structurally valid AVIF file, with a minimal AV1
6//! Sequence Header OBU as the bitstream payload.
7//!
8//! # Container structure
9//!
10//! ```text
11//! ftyp  – file-type box  (brand = 'avif', compat = ['avif','mif1','miaf'])
12//! meta  – metadata box
13//!   hdlr  – handler reference ('pict')
14//!   pitm  – primary item (item_ID = 1)
15//!   iloc  – item location (points into mdat)
16//!   iinf  – item information (one 'av01' entry)
17//!   iprp  – item properties
18//!     ipco  – property container
19//!       ispe  – image spatial extents (width, height)
20//!       colr  – colour information (nclx or restricted ICC)
21//!       av1C  – AV1 codec configuration record
22//!       pixi  – pixel information (bit depth)
23//!     ipma  – item property association
24//! mdat  – media data (AV1 OBU bitstream)
25//! ```
26
27use crate::error::CodecError;
28
29// ─── Public types ─────────────────────────────────────────────────────────────
30
31/// Encoding configuration for [`AvifEncoder`].
32#[derive(Debug, Clone)]
33pub struct AvifConfig {
34    /// Perceptual quality, 0–100 (100 = lossless).
35    pub quality: u8,
36    /// Encoder speed preset, 0–10 (0 = slowest/best, 10 = fastest).
37    pub speed: u8,
38    /// Colour primaries (ISO 23091-2 / H.273).
39    /// 1 = BT.709, 9 = BT.2020.
40    pub color_primaries: u8,
41    /// Transfer characteristics.
42    /// 1 = BT.709, 16 = PQ (SMPTE ST 2084), 18 = HLG.
43    pub transfer_characteristics: u8,
44    /// Matrix coefficients.
45    /// 1 = BT.709, 9 = BT.2020 NCL.
46    pub matrix_coefficients: u8,
47    /// Whether the YUV values use the full [0, 2^n-1] range.
48    pub full_range: bool,
49    /// If `Some(q)`, encode an alpha plane at quality `q`; otherwise omit it.
50    pub alpha_quality: Option<u8>,
51}
52
53impl Default for AvifConfig {
54    fn default() -> Self {
55        Self {
56            quality: 60,
57            speed: 6,
58            color_primaries: 1,
59            transfer_characteristics: 1,
60            matrix_coefficients: 1,
61            full_range: false,
62            alpha_quality: None,
63        }
64    }
65}
66
67/// Chroma subsampling format.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum YuvFormat {
70    /// 4:2:0 – U/V planes are half width and half height.
71    Yuv420,
72    /// 4:2:2 – U/V planes are half width, full height.
73    Yuv422,
74    /// 4:4:4 – U/V planes are full width and full height.
75    Yuv444,
76}
77
78/// In-memory AVIF image, in planar YCbCr form.
79#[derive(Debug, Clone)]
80pub struct AvifImage {
81    /// Image width in pixels.
82    pub width: u32,
83    /// Image height in pixels.
84    pub height: u32,
85    /// Bit depth: 8, 10, or 12.
86    pub depth: u8,
87    /// Chroma subsampling format.
88    pub yuv_format: YuvFormat,
89    /// Luma (Y) plane samples.  Each row is `stride_y` bytes wide.
90    pub y_plane: Vec<u8>,
91    /// Cb (U) plane samples.
92    pub u_plane: Vec<u8>,
93    /// Cr (V) plane samples.
94    pub v_plane: Vec<u8>,
95    /// Optional alpha plane samples (same dimensions as luma).
96    pub alpha_plane: Option<Vec<u8>>,
97}
98
99/// Lightweight metadata returned by [`AvifDecoder::probe`].
100#[derive(Debug, Clone)]
101pub struct AvifProbeResult {
102    /// Image width in pixels.
103    pub width: u32,
104    /// Image height in pixels.
105    pub height: u32,
106    /// Bit depth (8, 10, or 12).
107    pub bit_depth: u8,
108    /// Whether the file contains an alpha auxiliary image item.
109    pub has_alpha: bool,
110    /// Colour primaries code.
111    pub color_primaries: u8,
112    /// Transfer characteristics code.
113    pub transfer_characteristics: u8,
114}
115
116// ─── Encoder ─────────────────────────────────────────────────────────────────
117
118/// Encodes [`AvifImage`] frames to AVIF container bytes.
119#[derive(Debug, Clone)]
120pub struct AvifEncoder {
121    config: AvifConfig,
122}
123
124impl AvifEncoder {
125    /// Create a new encoder with the given configuration.
126    pub fn new(config: AvifConfig) -> Self {
127        Self { config }
128    }
129
130    /// Encode `image` to an AVIF byte stream.
131    ///
132    /// Returns the complete AVIF file as a `Vec<u8>`.
133    pub fn encode(&self, image: &AvifImage) -> Result<Vec<u8>, CodecError> {
134        validate_image(image)?;
135
136        // Build the AV1 OBU payload (Sequence Header + one empty Frame).
137        let av1_payload = build_av1_obu(image, &self.config);
138
139        // Sizes we need to compute before writing boxes.
140        let has_alpha = self.config.alpha_quality.is_some() && image.alpha_plane.is_some();
141        let alpha_payload = if has_alpha {
142            Some(build_alpha_av1_obu(image))
143        } else {
144            None
145        };
146
147        let mut out = Vec::with_capacity(4096 + av1_payload.len());
148
149        // ── ftyp ──────────────────────────────────────────────────────────
150        write_ftyp(&mut out);
151
152        // ── meta ──────────────────────────────────────────────────────────
153        // We need to know the absolute offset of mdat content ahead of time,
154        // so we build meta into a temporary buffer first, then patch the
155        // iloc offset once we know the final position.
156        let meta_buf = build_meta(image, &self.config, &av1_payload, &alpha_payload)?;
157        out.extend_from_slice(&meta_buf);
158
159        // ── mdat ──────────────────────────────────────────────────────────
160        let mdat_header_size = 8u32; // size(4) + 'mdat'(4)
161        let mdat_size = mdat_header_size as usize
162            + av1_payload.len()
163            + alpha_payload.as_ref().map_or(0, |a| a.len());
164        write_u32(&mut out, mdat_size as u32);
165        out.extend_from_slice(b"mdat");
166        out.extend_from_slice(&av1_payload);
167        if let Some(ref ap) = alpha_payload {
168            out.extend_from_slice(ap);
169        }
170
171        // Patch the iloc extents: we wrote placeholder offsets in build_meta,
172        // now fix them up.
173        patch_iloc_offsets(&mut out, &meta_buf, &av1_payload, alpha_payload.as_deref())?;
174
175        Ok(out)
176    }
177}
178
179// ─── Decoder ─────────────────────────────────────────────────────────────────
180
181/// Decodes AVIF container bytes to [`AvifImage`].
182#[derive(Debug, Default, Clone)]
183pub struct AvifDecoder;
184
185impl AvifDecoder {
186    /// Create a new decoder.
187    pub fn new() -> Self {
188        Self
189    }
190
191    /// Decode a complete AVIF byte stream.
192    ///
193    /// This parses the ISOBMFF container, extracts spatial/colour metadata
194    /// from the `iprp` boxes, and returns the raw AV1 bitstream in the
195    /// `y_plane` field (full decode of AV1 frames is out of scope for this
196    /// implementation; the bitstream is available for further processing).
197    pub fn decode(data: &[u8]) -> Result<AvifImage, CodecError> {
198        let probe = Self::probe(data)?;
199        let (color_offset, color_len, alpha_offset, alpha_len) =
200            locate_mdat_items(data, probe.has_alpha)?;
201
202        // For a complete implementation the AV1 OBU would be decoded into
203        // YUV planes.  Here we surface the raw bitstream in y_plane so
204        // callers can hand it off to an AV1 decoder.
205        let y_plane = data
206            .get(color_offset..color_offset + color_len)
207            .ok_or_else(|| CodecError::InvalidBitstream("mdat color extent out of range".into()))?
208            .to_vec();
209
210        let alpha_plane = if probe.has_alpha {
211            let (ao, al) = (alpha_offset, alpha_len);
212            let slice = data
213                .get(ao..ao + al)
214                .ok_or_else(|| {
215                    CodecError::InvalidBitstream("mdat alpha extent out of range".into())
216                })?
217                .to_vec();
218            Some(slice)
219        } else {
220            None
221        };
222
223        Ok(AvifImage {
224            width: probe.width,
225            height: probe.height,
226            depth: probe.bit_depth,
227            yuv_format: YuvFormat::Yuv420,
228            y_plane,
229            u_plane: Vec::new(),
230            v_plane: Vec::new(),
231            alpha_plane,
232        })
233    }
234
235    /// Probe a AVIF byte stream for basic metadata without full decoding.
236    pub fn probe(data: &[u8]) -> Result<AvifProbeResult, CodecError> {
237        check_avif_signature(data)?;
238        parse_meta_for_probe(data)
239    }
240}
241
242// ═════════════════════════════════════════════════════════════════════════════
243// Internal helpers – AV1 OBU builder
244// ═════════════════════════════════════════════════════════════════════════════
245
246/// Build a minimal AV1 bitstream containing a Sequence Header OBU.
247///
248/// The sequence header encodes width, height, bit depth, and colour info as
249/// required by the AVIF specification (§4 "AV1 Image Items").
250fn build_av1_obu(image: &AvifImage, config: &AvifConfig) -> Vec<u8> {
251    let mut bits = BitWriter::new();
252
253    // ── Sequence Header OBU ─────────────────────────────────────────────
254    // obu_forbidden_bit(1=0) | obu_type(4=1) | obu_extension_flag(1=0)
255    // | obu_has_size_field(1=1) | obu_reserved_1bit(1=0)
256    let obu_header: u8 = (1 << 3) | (1 << 1); // type=1, has_size=1
257    bits.write_byte(obu_header);
258
259    // Build the payload separately so we can prefix it with its leb128 size.
260    let mut seq = BitWriter::new();
261    write_sequence_header_payload(&mut seq, image, config);
262    let seq_bytes = seq.finish();
263
264    // leb128 size of the payload
265    let mut leb = Vec::new();
266    write_leb128(&mut leb, seq_bytes.len() as u64);
267    bits.extend_bytes(&leb);
268    bits.extend_bytes(&seq_bytes);
269
270    // ── Temporal Delimiter OBU (type = 2) ───────────────────────────────
271    bits.write_byte((2 << 3) | (1 << 1)); // type=2, has_size=1
272    bits.write_byte(0); // size = 0
273
274    bits.finish()
275}
276
277/// Build a minimal alpha-plane AV1 OBU (same structure, monochrome).
278fn build_alpha_av1_obu(image: &AvifImage) -> Vec<u8> {
279    let alpha_config = AvifConfig {
280        quality: 80,
281        color_primaries: 1,
282        transfer_characteristics: 1,
283        matrix_coefficients: 0, // identity / monochrome
284        full_range: true,
285        ..AvifConfig::default()
286    };
287    // Treat alpha as a grayscale image.
288    let mono = AvifImage {
289        width: image.width,
290        height: image.height,
291        depth: image.depth,
292        yuv_format: YuvFormat::Yuv444,
293        y_plane: image.alpha_plane.clone().unwrap_or_default(),
294        u_plane: Vec::new(),
295        v_plane: Vec::new(),
296        alpha_plane: None,
297    };
298    build_av1_obu(&mono, &alpha_config)
299}
300
301/// Write the Sequence Header OBU payload bits.
302fn write_sequence_header_payload(bits: &mut BitWriter, image: &AvifImage, config: &AvifConfig) {
303    // seq_profile: 0 = main (8/10-bit 4:2:0), 1 = high (4:4:4), 2 = pro
304    let seq_profile: u8 = match image.yuv_format {
305        YuvFormat::Yuv444 => 1,
306        _ => 0,
307    };
308    bits.write_bits(seq_profile as u32, 3);
309
310    // still_picture = 1
311    bits.write_bits(1, 1);
312    // reduced_still_picture_header = 1  (simplified header for still images)
313    bits.write_bits(1, 1);
314
315    // seq_level_idx[0]: level 5.1 = 13 (supports up to 4K)
316    bits.write_bits(13, 5);
317
318    // ── Colour config ───────────────────────────────────────────────────
319    // high_bitdepth
320    let high_bitdepth = image.depth >= 10;
321    bits.write_bits(high_bitdepth as u32, 1);
322    if seq_profile == 2 && high_bitdepth {
323        // twelve_bit
324        let twelve_bit = image.depth == 12;
325        bits.write_bits(twelve_bit as u32, 1);
326    }
327    // mono_chrome
328    bits.write_bits(0, 1);
329    // color_description_present_flag = 1
330    bits.write_bits(1, 1);
331    // color_primaries (8 bits)
332    bits.write_bits(config.color_primaries as u32, 8);
333    // transfer_characteristics (8 bits)
334    bits.write_bits(config.transfer_characteristics as u32, 8);
335    // matrix_coefficients (8 bits)
336    bits.write_bits(config.matrix_coefficients as u32, 8);
337    // color_range
338    bits.write_bits(config.full_range as u32, 1);
339
340    // subsampling_x / subsampling_y based on yuv_format
341    let (sub_x, sub_y): (u32, u32) = match image.yuv_format {
342        YuvFormat::Yuv420 => (1, 1),
343        YuvFormat::Yuv422 => (1, 0),
344        YuvFormat::Yuv444 => (0, 0),
345    };
346    if seq_profile != 1 {
347        if config.color_primaries == 1
348            && config.transfer_characteristics == 1
349            && config.matrix_coefficients == 1
350        {
351            // separate_uv_delta_q
352            bits.write_bits(0, 1);
353        } else {
354            bits.write_bits(sub_x, 1);
355            if sub_x == 1 {
356                bits.write_bits(sub_y, 1);
357            }
358            if sub_x == 1 && sub_y == 1 {
359                // chroma_sample_position
360                bits.write_bits(0, 2); // CSP_UNKNOWN
361            }
362        }
363    }
364    // separate_uv_delta_q
365    bits.write_bits(0, 1);
366
367    // ── Frame size in the reduced header ───────────────────────────────
368    // frame_width_bits_minus_1 / frame_height_bits_minus_1 each need
369    // ceil(log2(width)) bits.
370    let w_bits = bits_needed(image.width);
371    let h_bits = bits_needed(image.height);
372    bits.write_bits((w_bits - 1) as u32, 4); // frame_width_bits_minus_1
373    bits.write_bits((h_bits - 1) as u32, 4); // frame_height_bits_minus_1
374    bits.write_bits((image.width - 1) as u32, w_bits as u32);
375    bits.write_bits((image.height - 1) as u32, h_bits as u32);
376
377    // film_grain_params_present = 0
378    bits.write_bits(0, 1);
379}
380
381/// Number of bits required to represent values up to `n` (i.e. ⌈log2(n+1)⌉).
382fn bits_needed(n: u32) -> u8 {
383    if n == 0 {
384        return 1;
385    }
386    let mut bits = 0u8;
387    let mut v = n;
388    while v > 0 {
389        bits += 1;
390        v >>= 1;
391    }
392    bits
393}
394
395/// Write `value` as a LEB128-encoded unsigned integer into `buf`.
396fn write_leb128(buf: &mut Vec<u8>, mut value: u64) {
397    loop {
398        let mut byte = (value & 0x7F) as u8;
399        value >>= 7;
400        if value != 0 {
401            byte |= 0x80;
402        }
403        buf.push(byte);
404        if value == 0 {
405            break;
406        }
407    }
408}
409
410// ═════════════════════════════════════════════════════════════════════════════
411// Internal helpers – ISOBMFF box builder
412// ═════════════════════════════════════════════════════════════════════════════
413
414/// Write the `ftyp` box into `out`.
415fn write_ftyp(out: &mut Vec<u8>) {
416    // compatible brands: 'avif', 'mif1', 'miaf'
417    let compat: &[&[u8; 4]] = &[b"avif", b"mif1", b"miaf"];
418    let size = 4 + 4 + 4 + 4 + 4 * compat.len(); // size + 'ftyp' + major + minor_version + compat[]
419    write_u32(out, size as u32);
420    out.extend_from_slice(b"ftyp");
421    out.extend_from_slice(b"avif"); // major_brand
422    write_u32(out, 0); // minor_version
423    for brand in compat {
424        out.extend_from_slice(*brand);
425    }
426}
427
428/// Build the complete `meta` box (FullBox, version 0) and return it as bytes.
429///
430/// The `iloc` extents use placeholder offsets (0) which are patched later by
431/// [`patch_iloc_offsets`].
432fn build_meta(
433    image: &AvifImage,
434    config: &AvifConfig,
435    av1_payload: &[u8],
436    alpha_payload: &Option<Vec<u8>>,
437) -> Result<Vec<u8>, CodecError> {
438    let has_alpha = alpha_payload.is_some();
439
440    let mut body = Vec::<u8>::new();
441
442    // hdlr box
443    body.extend_from_slice(&build_hdlr());
444
445    // pitm box (primary item id = 1)
446    body.extend_from_slice(&build_pitm(1));
447
448    // iloc box
449    body.extend_from_slice(&build_iloc(
450        has_alpha,
451        av1_payload.len(),
452        alpha_payload.as_ref().map_or(0, |a| a.len()),
453    ));
454
455    // iinf box
456    body.extend_from_slice(&build_iinf(has_alpha));
457
458    // iprp box
459    body.extend_from_slice(&build_iprp(image, config, has_alpha)?);
460
461    // Wrap in meta FullBox (version=0, flags=0)
462    let meta_size = 4 + 4 + 4 + body.len(); // size + 'meta' + version/flags(4)
463    let mut meta = Vec::with_capacity(meta_size);
464    write_u32(&mut meta, meta_size as u32);
465    meta.extend_from_slice(b"meta");
466    write_u32(&mut meta, 0u32); // version(1) + flags(3)
467    meta.extend_from_slice(&body);
468    Ok(meta)
469}
470
471// ── hdlr ──────────────────────────────────────────────────────────────────
472
473fn build_hdlr() -> Vec<u8> {
474    // FullBox(version=0, flags=0) + pre_defined(4) + handler_type(4) +
475    // reserved(12) + name(1 = '\0')
476    // Box layout: size(4) + 'hdlr'(4) + fullbox(4) + pre_defined(4) +
477    //             handler_type(4) + reserved(12) + name(1) = 33 bytes
478    let size = 4 + 4 + 4 + 4 + 4 + 12 + 1; // 33
479    let mut b = Vec::with_capacity(size);
480    write_u32(&mut b, size as u32);
481    b.extend_from_slice(b"hdlr");
482    write_u32(&mut b, 0); // version + flags
483    write_u32(&mut b, 0); // pre_defined
484    b.extend_from_slice(b"pict"); // handler_type
485    b.extend_from_slice(&[0u8; 12]); // reserved
486    b.push(0); // name (null-terminated empty string)
487    b
488}
489
490// ── pitm ──────────────────────────────────────────────────────────────────
491
492fn build_pitm(item_id: u16) -> Vec<u8> {
493    let size = 4 + 4 + 4 + 2; // header(8) + fullbox(4) + item_ID(2)
494    let mut b = Vec::with_capacity(size);
495    write_u32(&mut b, size as u32);
496    b.extend_from_slice(b"pitm");
497    write_u32(&mut b, 0); // version=0, flags=0
498    write_u16(&mut b, item_id);
499    b
500}
501
502// ── iloc ──────────────────────────────────────────────────────────────────
503
504/// Build the `iloc` box with **placeholder** absolute offsets (0).
505///
506/// The real offsets are patched by [`patch_iloc_offsets`] after the
507/// complete file layout is known.
508fn build_iloc(has_alpha: bool, color_len: usize, alpha_len: usize) -> Vec<u8> {
509    // iloc version=1 supports 32-bit offset_size=4, length_size=4,
510    // base_offset_size=0, index_size=0.
511    // Format: offset_size(4bits) | length_size(4bits) | base_offset_size(4bits) | index_size/reserved(4bits)
512    // Then item_count(u16), then per-item: item_ID(u16), reserved(u16), data_ref_index(u16),
513    // extent_count(u16), [extent_index(if index_size>0),] extent_offset(offset_size), extent_length(length_size)
514
515    let item_count: u16 = if has_alpha { 2 } else { 1 };
516    // Per-item fields for version=1:
517    //   item_ID(2) + construction_method(2, version>=1) + data_ref_idx(2) + extent_count(2)
518    //   + extent_offset(4) + extent_length(4) = 16 bytes each
519    let item_entry_size = 2 + 2 + 2 + 2 + 4 + 4;
520    let payload_size = 1 + 1 + 2 + item_count as usize * item_entry_size;
521    // FullBox: version(1) + flags(3) = 4 bytes
522    let size = 8 + 4 + payload_size;
523    let mut b = Vec::with_capacity(size);
524    write_u32(&mut b, size as u32);
525    b.extend_from_slice(b"iloc");
526    write_u32(&mut b, 1 << 24); // version=1, flags=0
527
528    // offset_size=4(bits), length_size=4(bits) packed into one byte
529    b.push(0x44); // 0100_0100 -> offset_size=4, length_size=4
530                  // base_offset_size=0, index_size=0 packed into one byte
531    b.push(0x00);
532    // item_count
533    write_u16(&mut b, item_count);
534
535    // Item 1: colour image (item_ID=1)
536    write_u16(&mut b, 1); // item_ID
537    write_u16(&mut b, 0); // construction_method = 0 (file offset)
538    write_u16(&mut b, 0); // data_reference_index
539    write_u16(&mut b, 1); // extent_count
540    write_u32(&mut b, 0); // extent_offset PLACEHOLDER
541    write_u32(&mut b, color_len as u32); // extent_length
542
543    if has_alpha {
544        // Item 2: alpha image (item_ID=2)
545        write_u16(&mut b, 2); // item_ID
546        write_u16(&mut b, 0); // construction_method
547        write_u16(&mut b, 0); // data_reference_index
548        write_u16(&mut b, 1); // extent_count
549        write_u32(&mut b, 0); // extent_offset PLACEHOLDER
550        write_u32(&mut b, alpha_len as u32); // extent_length
551    }
552
553    b
554}
555
556// ── iinf ──────────────────────────────────────────────────────────────────
557
558fn build_iinf(has_alpha: bool) -> Vec<u8> {
559    let item_count: u16 = if has_alpha { 2 } else { 1 };
560    let entry1 = build_infe(1, b"av01", b"Color Image\0");
561    let entry2 = if has_alpha {
562        Some(build_infe(2, b"av01", b"Alpha Image\0"))
563    } else {
564        None
565    };
566
567    let entries_size = entry1.len() + entry2.as_ref().map_or(0, |e| e.len());
568    let size = 8 + 4 + 2 + entries_size; // box(8) + fullbox(4) + entry_count(2) + entries
569    let mut b = Vec::with_capacity(size);
570    write_u32(&mut b, size as u32);
571    b.extend_from_slice(b"iinf");
572    write_u32(&mut b, 0); // version=0, flags=0
573    write_u16(&mut b, item_count);
574    b.extend_from_slice(&entry1);
575    if let Some(e2) = entry2 {
576        b.extend_from_slice(&e2);
577    }
578    b
579}
580
581/// Build an `infe` (ItemInfoEntry) FullBox for version=2.
582fn build_infe(item_id: u16, item_type: &[u8; 4], item_name: &[u8]) -> Vec<u8> {
583    // version=2: item_ID(2) + item_protection_index(2) + item_type(4) + item_name(...)
584    let payload = 2 + 2 + 4 + item_name.len();
585    let size = 8 + 4 + payload; // box(8) + fullbox flags/version(4) + payload
586    let mut b = Vec::with_capacity(size);
587    write_u32(&mut b, size as u32);
588    b.extend_from_slice(b"infe");
589    write_u32(&mut b, 2 << 24); // version=2, flags=0
590    write_u16(&mut b, item_id);
591    write_u16(&mut b, 0); // item_protection_index
592    b.extend_from_slice(item_type);
593    b.extend_from_slice(item_name);
594    b
595}
596
597// ── iprp ──────────────────────────────────────────────────────────────────
598
599fn build_iprp(
600    image: &AvifImage,
601    config: &AvifConfig,
602    has_alpha: bool,
603) -> Result<Vec<u8>, CodecError> {
604    let ispe = build_ispe(image.width, image.height);
605    let colr = build_colr(config);
606    let av1c = build_av1c(image, config);
607    let pixi = build_pixi(image.depth);
608
609    // ipco – property container
610    let ipco_payload_len = ispe.len() + colr.len() + av1c.len() + pixi.len();
611    let ipco_size = 8 + ipco_payload_len;
612    let mut ipco = Vec::with_capacity(ipco_size);
613    write_u32(&mut ipco, ipco_size as u32);
614    ipco.extend_from_slice(b"ipco");
615    ipco.extend_from_slice(&ispe);
616    ipco.extend_from_slice(&colr);
617    ipco.extend_from_slice(&av1c);
618    ipco.extend_from_slice(&pixi);
619
620    // ipma – property association
621    // Properties are 1-indexed within ipco.
622    // prop 1 = ispe, prop 2 = colr, prop 3 = av1C, prop 4 = pixi
623    let ipma = build_ipma(has_alpha);
624
625    let iprp_size = 8 + ipco.len() + ipma.len();
626    let mut b = Vec::with_capacity(iprp_size);
627    write_u32(&mut b, iprp_size as u32);
628    b.extend_from_slice(b"iprp");
629    b.extend_from_slice(&ipco);
630    b.extend_from_slice(&ipma);
631    Ok(b)
632}
633
634/// `ispe` – image spatial extents.
635fn build_ispe(width: u32, height: u32) -> Vec<u8> {
636    let size = 8 + 4 + 4 + 4; // box(8) + fullbox(4) + w(4) + h(4)
637    let mut b = Vec::with_capacity(size);
638    write_u32(&mut b, size as u32);
639    b.extend_from_slice(b"ispe");
640    write_u32(&mut b, 0); // version=0, flags=0
641    write_u32(&mut b, width);
642    write_u32(&mut b, height);
643    b
644}
645
646/// `colr` – colour information (nclx type).
647fn build_colr(config: &AvifConfig) -> Vec<u8> {
648    // nclx: colour_type(4) + colour_primaries(2) + transfer_characteristics(2)
649    //       + matrix_coefficients(2) + full_range_flag(1 bit) + reserved(7 bits)
650    let payload_size = 4 + 2 + 2 + 2 + 1;
651    let size = 8 + payload_size;
652    let mut b = Vec::with_capacity(size);
653    write_u32(&mut b, size as u32);
654    b.extend_from_slice(b"colr");
655    b.extend_from_slice(b"nclx"); // colour_type
656    write_u16(&mut b, config.color_primaries as u16);
657    write_u16(&mut b, config.transfer_characteristics as u16);
658    write_u16(&mut b, config.matrix_coefficients as u16);
659    let full_range_byte: u8 = if config.full_range { 0x80 } else { 0x00 };
660    b.push(full_range_byte);
661    b
662}
663
664/// `av1C` – AV1 Codec Configuration Record.
665///
666/// Spec: <https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox>
667fn build_av1c(image: &AvifImage, config: &AvifConfig) -> Vec<u8> {
668    // marker(1=1) | version(7=1) = 0x81
669    // seq_profile(3) | seq_level_idx_0(5)
670    // seq_tier_0(1) | high_bitdepth(1) | twelve_bit(1) | monochrome(1)
671    //   | chroma_subsampling_x(1) | chroma_subsampling_y(1) | chroma_sample_position(2)
672    // reserved(3=0) | initial_presentation_delay_present(1=0) | reserved(4=0)
673
674    let seq_profile: u8 = match image.yuv_format {
675        YuvFormat::Yuv444 => 1,
676        _ => 0,
677    };
678    let seq_level_idx_0: u8 = 13; // level 5.1
679
680    let byte0: u8 = 0x81; // marker + version
681    let byte1: u8 = (seq_profile << 5) | seq_level_idx_0;
682    let high_bitdepth = image.depth >= 10;
683    let twelve_bit = image.depth == 12;
684    let (sub_x, sub_y): (u8, u8) = match image.yuv_format {
685        YuvFormat::Yuv420 => (1, 1),
686        YuvFormat::Yuv422 => (1, 0),
687        YuvFormat::Yuv444 => (0, 0),
688    };
689    let byte2: u8 = (high_bitdepth as u8) << 6
690        | (twelve_bit as u8) << 5
691        | 0 << 4 // monochrome = 0
692        | sub_x << 3
693        | sub_y << 2
694        | 0; // chroma_sample_position
695    let _ = config; // seq_tier_0 etc. – not exposed in config, default 0
696    let byte3: u8 = 0x00; // initial_presentation_delay_present = 0
697
698    let size = 8 + 4; // box(8) + 4 payload bytes
699    let mut b = Vec::with_capacity(size);
700    write_u32(&mut b, size as u32);
701    b.extend_from_slice(b"av1C");
702    b.push(byte0);
703    b.push(byte1);
704    b.push(byte2);
705    b.push(byte3);
706    b
707}
708
709/// `pixi` – pixel information (bit depth per channel).
710fn build_pixi(depth: u8) -> Vec<u8> {
711    // FullBox + num_channels(1=3 for YUV) + depth_in_bits × 3
712    let num_channels: u8 = 3;
713    let size = 8 + 4 + 1 + num_channels as usize;
714    let mut b = Vec::with_capacity(size);
715    write_u32(&mut b, size as u32);
716    b.extend_from_slice(b"pixi");
717    write_u32(&mut b, 0); // version=0, flags=0
718    b.push(num_channels);
719    for _ in 0..num_channels {
720        b.push(depth);
721    }
722    b
723}
724
725/// `ipma` – item property association.
726///
727/// Associates properties 1–4 (ispe, colr, av1C, pixi) with item 1 (and item 2
728/// if alpha is present).
729fn build_ipma(has_alpha: bool) -> Vec<u8> {
730    // version=0, flags=0
731    // entry_count(4 bytes)
732    // Per entry: item_ID(2) + association_count(1) + [essential(1) | property_index(7)] × n
733    let item_count: u32 = if has_alpha { 2 } else { 1 };
734    // Each item references 4 properties (ispe, colr, av1C, pixi).
735    // essential bit = 1 for av1C (index 3), others = 0.
736    let assoc_per_item: &[(u8, u8)] = &[
737        (0, 1), // ispe – not essential, prop index 1
738        (0, 2), // colr – not essential, prop index 2
739        (1, 3), // av1C – essential, prop index 3
740        (0, 4), // pixi – not essential, prop index 4
741    ];
742    let per_item_size = 2 + 1 + assoc_per_item.len(); // ID(2) + count(1) + n×1
743    let payload_size = 4 + item_count as usize * per_item_size;
744    let size = 8 + 4 + payload_size;
745    let mut b = Vec::with_capacity(size);
746    write_u32(&mut b, size as u32);
747    b.extend_from_slice(b"ipma");
748    write_u32(&mut b, 0); // version=0, flags=0
749    write_u32(&mut b, item_count);
750
751    for item_id in 1..=item_count as u16 {
752        write_u16(&mut b, item_id);
753        b.push(assoc_per_item.len() as u8);
754        for &(essential, prop_idx) in assoc_per_item {
755            b.push((essential << 7) | (prop_idx & 0x7F));
756        }
757    }
758    b
759}
760
761// ── Patch iloc offsets ─────────────────────────────────────────────────────
762
763/// After the full layout is known, find the placeholder offsets in the already-
764/// appended `meta` bytes and overwrite them with the correct absolute file
765/// offsets.
766///
767/// Layout: `[ftyp][meta][mdat_header(8)][av1_payload][alpha_payload?]`
768///
769/// `out` contains ftyp + meta + mdat (already written).
770/// `meta_buf` is the original meta bytes (before appending to out) — used to
771/// compute the size of ftyp.
772fn patch_iloc_offsets(
773    out: &mut Vec<u8>,
774    meta_buf: &[u8],
775    av1_payload: &[u8],
776    alpha_payload: Option<&[u8]>,
777) -> Result<(), CodecError> {
778    // ftyp size is stored in the first 4 bytes of `out`.
779    let ftyp_size = u32::from_be_bytes(
780        out.get(0..4)
781            .ok_or_else(|| CodecError::Internal("output too short for ftyp size".into()))?
782            .try_into()
783            .map_err(|_| CodecError::Internal("slice conversion error".into()))?,
784    ) as usize;
785
786    let meta_size = meta_buf.len();
787    // mdat content starts after ftyp + meta + mdat_header(8)
788    let mdat_data_start = ftyp_size + meta_size + 8;
789
790    let color_offset = mdat_data_start as u32;
791    let alpha_offset = (mdat_data_start + av1_payload.len()) as u32;
792
793    // Find the iloc box within `out` (it's inside meta).
794    // meta starts at ftyp_size; inside meta: 8(box header) + 4(fullbox) = 12 bytes.
795    // Then hdlr, pitm, then iloc.
796    let meta_start = ftyp_size;
797    let meta_body_start = meta_start + 12; // skip meta box header(8) + fullbox(4)
798
799    // Walk meta body to find iloc.
800    let iloc_pos = find_box_in(out, meta_body_start, meta_start + meta_size, b"iloc")
801        .ok_or_else(|| CodecError::Internal("iloc box not found in output".into()))?;
802
803    // iloc layout (version=1):
804    // box_header(8) + fullbox(4) + offset_size/length_size(1) + base_offset_size/index_size(1)
805    // + item_count(2) = 16 bytes before first item entry
806    let item0_start = iloc_pos + 16;
807    // Per item (version=1): item_ID(2) + construction_method(2) + data_ref_idx(2)
808    //   + extent_count(2) + extent_offset(4) + extent_length(4) = 16 bytes
809    let color_extent_offset_pos = item0_start + 6; // skip ID(2)+method(2)+ref(2)+count(2) = 8, then offset starts
810                                                   // Actually: ID(2) + method(2) + ref(2) + extent_count(2) = 8 bytes
811    let color_extent_offset_pos = item0_start + 8;
812
813    patch_u32(out, color_extent_offset_pos, color_offset)?;
814
815    if alpha_payload.is_some() {
816        let item1_start = item0_start + 16;
817        let alpha_extent_offset_pos = item1_start + 8;
818        patch_u32(out, alpha_extent_offset_pos, alpha_offset)?;
819    }
820
821    Ok(())
822}
823
824/// Find a 4-byte box type within `data[start..end]`, return byte offset.
825fn find_box_in(data: &[u8], start: usize, end: usize, box_type: &[u8; 4]) -> Option<usize> {
826    let mut pos = start;
827    while pos + 8 <= end.min(data.len()) {
828        let size = u32::from_be_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
829        if size < 8 {
830            break;
831        }
832        if &data[pos + 4..pos + 8] == box_type {
833            return Some(pos);
834        }
835        pos += size;
836    }
837    None
838}
839
840/// Overwrite 4 bytes at `pos` in `buf` with big-endian `value`.
841fn patch_u32(buf: &mut Vec<u8>, pos: usize, value: u32) -> Result<(), CodecError> {
842    if pos + 4 > buf.len() {
843        return Err(CodecError::Internal(format!(
844            "patch_u32: pos={pos} out of range (buf.len={})",
845            buf.len()
846        )));
847    }
848    let bytes = value.to_be_bytes();
849    buf[pos] = bytes[0];
850    buf[pos + 1] = bytes[1];
851    buf[pos + 2] = bytes[2];
852    buf[pos + 3] = bytes[3];
853    Ok(())
854}
855
856// ═════════════════════════════════════════════════════════════════════════════
857// Internal helpers – parser
858// ═════════════════════════════════════════════════════════════════════════════
859
860/// Verify the byte stream starts with an AVIF `ftyp` box.
861fn check_avif_signature(data: &[u8]) -> Result<(), CodecError> {
862    if data.len() < 12 {
863        return Err(CodecError::InvalidBitstream(
864            "file too short to be AVIF".into(),
865        ));
866    }
867    let size = u32::from_be_bytes(
868        data[0..4]
869            .try_into()
870            .map_err(|_| CodecError::InvalidBitstream("cannot read ftyp size".into()))?,
871    ) as usize;
872    if size < 12 || size > data.len() {
873        return Err(CodecError::InvalidBitstream("invalid ftyp box size".into()));
874    }
875    if &data[4..8] != b"ftyp" {
876        return Err(CodecError::InvalidBitstream("first box is not ftyp".into()));
877    }
878    // Check that 'avif' appears among the brands.
879    let brands_region = &data[8..size];
880    let has_avif = brands_region
881        .chunks(4)
882        .any(|c| c.len() == 4 && c == b"avif");
883    if !has_avif {
884        return Err(CodecError::InvalidBitstream(
885            "ftyp does not contain 'avif' brand".into(),
886        ));
887    }
888    Ok(())
889}
890
891/// Parse the `meta` box to extract spatial/colour metadata.
892fn parse_meta_for_probe(data: &[u8]) -> Result<AvifProbeResult, CodecError> {
893    // Find meta box (typically immediately after ftyp, but walk to be safe).
894    let meta_pos = find_top_level_box(data, b"meta")
895        .ok_or_else(|| CodecError::InvalidBitstream("meta box not found".into()))?;
896    let meta_size = u32::from_be_bytes(
897        data[meta_pos..meta_pos + 4]
898            .try_into()
899            .map_err(|_| CodecError::InvalidBitstream("meta size read error".into()))?,
900    ) as usize;
901    let meta_end = meta_pos + meta_size;
902
903    // meta is a FullBox: skip box header(8) + fullbox flags(4) = 12.
904    let meta_body = meta_pos + 12;
905
906    // ── ispe ──────────────────────────────────────────────────────────
907    let (width, height) = parse_ispe(data, meta_body, meta_end)?;
908
909    // ── colr ──────────────────────────────────────────────────────────
910    let (color_primaries, transfer_characteristics) =
911        parse_colr(data, meta_body, meta_end).unwrap_or((1, 1));
912
913    // ── pixi ──────────────────────────────────────────────────────────
914    let bit_depth = parse_pixi(data, meta_body, meta_end).unwrap_or(8);
915
916    // ── alpha: check iinf for a second item with auxiliary type ───────
917    let has_alpha = parse_iinf_has_alpha(data, meta_body, meta_end);
918
919    Ok(AvifProbeResult {
920        width,
921        height,
922        bit_depth,
923        has_alpha,
924        color_primaries,
925        transfer_characteristics,
926    })
927}
928
929fn parse_ispe(data: &[u8], start: usize, end: usize) -> Result<(u32, u32), CodecError> {
930    let pos = find_box_in(data, start, end, b"iprp")
931        .and_then(|iprp| {
932            let iprp_end =
933                iprp + u32::from_be_bytes(data[iprp..iprp + 4].try_into().ok()?) as usize;
934            find_box_in(data, iprp + 8, iprp_end, b"ipco").and_then(|ipco| {
935                let ipco_end =
936                    ipco + u32::from_be_bytes(data[ipco..ipco + 4].try_into().ok()?) as usize;
937                find_box_in(data, ipco + 8, ipco_end, b"ispe")
938            })
939        })
940        .ok_or_else(|| CodecError::InvalidBitstream("ispe not found".into()))?;
941
942    // ispe: FullBox(12) + width(4) + height(4)
943    if pos + 20 > data.len() {
944        return Err(CodecError::InvalidBitstream("ispe box truncated".into()));
945    }
946    let w = u32::from_be_bytes(
947        data[pos + 12..pos + 16]
948            .try_into()
949            .map_err(|_| CodecError::InvalidBitstream("ispe width read error".into()))?,
950    );
951    let h = u32::from_be_bytes(
952        data[pos + 16..pos + 20]
953            .try_into()
954            .map_err(|_| CodecError::InvalidBitstream("ispe height read error".into()))?,
955    );
956    Ok((w, h))
957}
958
959fn parse_colr(data: &[u8], start: usize, end: usize) -> Option<(u8, u8)> {
960    let iprp = find_box_in(data, start, end, b"iprp")?;
961    let iprp_end = iprp + u32::from_be_bytes(data[iprp..iprp + 4].try_into().ok()?) as usize;
962    let ipco = find_box_in(data, iprp + 8, iprp_end, b"ipco")?;
963    let ipco_end = ipco + u32::from_be_bytes(data[ipco..ipco + 4].try_into().ok()?) as usize;
964    let pos = find_box_in(data, ipco + 8, ipco_end, b"colr")?;
965    // colr: box(8) + colour_type(4) + ...
966    if pos + 15 > data.len() {
967        return None;
968    }
969    if &data[pos + 8..pos + 12] != b"nclx" {
970        return None;
971    }
972    // nclx: colour_primaries(2) + transfer_characteristics(2) + ...
973    let cp = u16::from_be_bytes(data[pos + 12..pos + 14].try_into().ok()?) as u8;
974    let tc = u16::from_be_bytes(data[pos + 14..pos + 16].try_into().ok()?) as u8;
975    Some((cp, tc))
976}
977
978fn parse_pixi(data: &[u8], start: usize, end: usize) -> Option<u8> {
979    let iprp = find_box_in(data, start, end, b"iprp")?;
980    let iprp_end = iprp + u32::from_be_bytes(data[iprp..iprp + 4].try_into().ok()?) as usize;
981    let ipco = find_box_in(data, iprp + 8, iprp_end, b"ipco")?;
982    let ipco_end = ipco + u32::from_be_bytes(data[ipco..ipco + 4].try_into().ok()?) as usize;
983    let pos = find_box_in(data, ipco + 8, ipco_end, b"pixi")?;
984    // pixi: FullBox(12) + num_channels(1) + depth[0](1)
985    if pos + 14 > data.len() {
986        return None;
987    }
988    Some(data[pos + 13])
989}
990
991fn parse_iinf_has_alpha(data: &[u8], start: usize, end: usize) -> bool {
992    let pos = match find_box_in(data, start, end, b"iinf") {
993        Some(p) => p,
994        None => return false,
995    };
996    let iinf_size = u32::from_be_bytes(match data[pos..pos + 4].try_into() {
997        Ok(b) => b,
998        Err(_) => return false,
999    }) as usize;
1000    // iinf FullBox version=0: box(8) + fullbox(4) + entry_count(2)
1001    let entry_count = u16::from_be_bytes(match data[pos + 12..pos + 14].try_into() {
1002        Ok(b) => b,
1003        Err(_) => return false,
1004    });
1005    // If there's more than one item, we treat the second as alpha.
1006    entry_count >= 2 && iinf_size >= 14
1007}
1008
1009/// Locate the `mdat` box and return `(color_offset, color_len, alpha_offset, alpha_len)`.
1010///
1011/// We use the iloc information to find actual item extents, but for our
1012/// simplified writer we can rely on the fact that color data is first in mdat
1013/// and alpha follows.  For a robust implementation one would parse iloc.
1014fn locate_mdat_items(
1015    data: &[u8],
1016    has_alpha: bool,
1017) -> Result<(usize, usize, usize, usize), CodecError> {
1018    // Find iloc to read the actual extents.
1019    let meta_pos = find_top_level_box(data, b"meta")
1020        .ok_or_else(|| CodecError::InvalidBitstream("meta box not found".into()))?;
1021    let meta_size = u32::from_be_bytes(
1022        data[meta_pos..meta_pos + 4]
1023            .try_into()
1024            .map_err(|_| CodecError::InvalidBitstream("meta size".into()))?,
1025    ) as usize;
1026    let meta_end = meta_pos + meta_size;
1027    let meta_body = meta_pos + 12;
1028
1029    let iloc_pos = find_box_in(data, meta_body, meta_end, b"iloc")
1030        .ok_or_else(|| CodecError::InvalidBitstream("iloc not found".into()))?;
1031
1032    // Parse iloc version=1 (as written by our encoder).
1033    // box(8) + fullbox(4) + offset_size/length_size(1) + base/index(1) + item_count(2)
1034    // iloc box layout:
1035    //   size(4) + 'iloc'(4) + version(1) + flags(3) = 12 bytes header
1036    //   offset_size|length_size(1) + base_offset_size|index_size(1) + item_count(2)
1037    let version = data[iloc_pos + 8];
1038    if version != 1 {
1039        return Err(CodecError::UnsupportedFeature(format!(
1040            "iloc version {version} not supported"
1041        )));
1042    }
1043
1044    // offset 12: offset_size/length_size byte
1045    // offset 13: base_offset_size/index_size byte
1046    // offset 14..16: item_count
1047    let item_count = u16::from_be_bytes(
1048        data[iloc_pos + 14..iloc_pos + 16]
1049            .try_into()
1050            .map_err(|_| CodecError::InvalidBitstream("iloc item_count".into()))?,
1051    );
1052
1053    if item_count == 0 {
1054        return Err(CodecError::InvalidBitstream("iloc has no items".into()));
1055    }
1056
1057    // First item entry starts at offset 16
1058    let item0 = iloc_pos + 16;
1059    // item entry (version=1, offset_size=4, length_size=4):
1060    //   ID(2) + method(2) + ref(2) + count(2) + offset(4) + length(4) = 16
1061    let color_offset = u32::from_be_bytes(
1062        data[item0 + 8..item0 + 12]
1063            .try_into()
1064            .map_err(|_| CodecError::InvalidBitstream("color extent offset".into()))?,
1065    ) as usize;
1066    let color_len = u32::from_be_bytes(
1067        data[item0 + 12..item0 + 16]
1068            .try_into()
1069            .map_err(|_| CodecError::InvalidBitstream("color extent length".into()))?,
1070    ) as usize;
1071
1072    let (alpha_offset, alpha_len) = if has_alpha && item_count >= 2 {
1073        let item1 = item0 + 16;
1074        let ao = u32::from_be_bytes(
1075            data[item1 + 8..item1 + 12]
1076                .try_into()
1077                .map_err(|_| CodecError::InvalidBitstream("alpha extent offset".into()))?,
1078        ) as usize;
1079        let al = u32::from_be_bytes(
1080            data[item1 + 12..item1 + 16]
1081                .try_into()
1082                .map_err(|_| CodecError::InvalidBitstream("alpha extent length".into()))?,
1083        ) as usize;
1084        (ao, al)
1085    } else {
1086        (0, 0)
1087    };
1088
1089    Ok((color_offset, color_len, alpha_offset, alpha_len))
1090}
1091
1092/// Walk the top-level box list to find a box by type.
1093fn find_top_level_box(data: &[u8], box_type: &[u8; 4]) -> Option<usize> {
1094    let mut pos = 0usize;
1095    while pos + 8 <= data.len() {
1096        let size = u32::from_be_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
1097        if size < 8 {
1098            break;
1099        }
1100        if &data[pos + 4..pos + 8] == box_type {
1101            return Some(pos);
1102        }
1103        pos += size;
1104    }
1105    None
1106}
1107
1108// ═════════════════════════════════════════════════════════════════════════════
1109// Internal helpers – validation
1110// ═════════════════════════════════════════════════════════════════════════════
1111
1112fn validate_image(image: &AvifImage) -> Result<(), CodecError> {
1113    if image.width == 0 || image.height == 0 {
1114        return Err(CodecError::InvalidParameter(
1115            "image dimensions must be non-zero".into(),
1116        ));
1117    }
1118    if ![8u8, 10, 12].contains(&image.depth) {
1119        return Err(CodecError::InvalidParameter(format!(
1120            "unsupported bit depth {}; must be 8, 10, or 12",
1121            image.depth
1122        )));
1123    }
1124    let luma_samples = image.width as usize * image.height as usize;
1125    let bytes_per_sample: usize = if image.depth > 8 { 2 } else { 1 };
1126    let min_y = luma_samples * bytes_per_sample;
1127    if image.y_plane.len() < min_y {
1128        return Err(CodecError::InvalidParameter(format!(
1129            "y_plane too small: need {min_y}, have {}",
1130            image.y_plane.len()
1131        )));
1132    }
1133    Ok(())
1134}
1135
1136// ═════════════════════════════════════════════════════════════════════════════
1137// Internal helpers – I/O primitives
1138// ═════════════════════════════════════════════════════════════════════════════
1139
1140fn write_u32(out: &mut Vec<u8>, v: u32) {
1141    out.extend_from_slice(&v.to_be_bytes());
1142}
1143
1144fn write_u16(out: &mut Vec<u8>, v: u16) {
1145    out.extend_from_slice(&v.to_be_bytes());
1146}
1147
1148// ═════════════════════════════════════════════════════════════════════════════
1149// Internal helpers – bit writer
1150// ═════════════════════════════════════════════════════════════════════════════
1151
1152/// MSB-first bit writer used for building AV1 OBU payloads.
1153struct BitWriter {
1154    buf: Vec<u8>,
1155    current: u8,
1156    bits_in_current: u8,
1157}
1158
1159impl BitWriter {
1160    fn new() -> Self {
1161        Self {
1162            buf: Vec::new(),
1163            current: 0,
1164            bits_in_current: 0,
1165        }
1166    }
1167
1168    /// Write the least-significant `n` bits of `value`, MSB first.
1169    fn write_bits(&mut self, value: u32, n: u32) {
1170        for i in (0..n).rev() {
1171            let bit = ((value >> i) & 1) as u8;
1172            self.current = (self.current << 1) | bit;
1173            self.bits_in_current += 1;
1174            if self.bits_in_current == 8 {
1175                self.buf.push(self.current);
1176                self.current = 0;
1177                self.bits_in_current = 0;
1178            }
1179        }
1180    }
1181
1182    fn write_byte(&mut self, byte: u8) {
1183        self.write_bits(byte as u32, 8);
1184    }
1185
1186    fn extend_bytes(&mut self, bytes: &[u8]) {
1187        for &b in bytes {
1188            self.write_byte(b);
1189        }
1190    }
1191
1192    /// Flush any remaining bits (zero-padded to byte boundary) and return buf.
1193    fn finish(mut self) -> Vec<u8> {
1194        if self.bits_in_current > 0 {
1195            self.current <<= 8 - self.bits_in_current;
1196            self.buf.push(self.current);
1197        }
1198        self.buf
1199    }
1200}
1201
1202// ═════════════════════════════════════════════════════════════════════════════
1203// Tests
1204// ═════════════════════════════════════════════════════════════════════════════
1205
1206#[cfg(test)]
1207mod tests {
1208    use super::*;
1209
1210    fn make_test_image(width: u32, height: u32, depth: u8, fmt: YuvFormat) -> AvifImage {
1211        let luma = width as usize * height as usize * if depth > 8 { 2 } else { 1 };
1212        let chroma = match fmt {
1213            YuvFormat::Yuv420 => (width as usize / 2) * (height as usize / 2),
1214            YuvFormat::Yuv422 => (width as usize / 2) * height as usize,
1215            YuvFormat::Yuv444 => width as usize * height as usize,
1216        } * if depth > 8 { 2 } else { 1 };
1217        AvifImage {
1218            width,
1219            height,
1220            depth,
1221            yuv_format: fmt,
1222            y_plane: vec![128u8; luma],
1223            u_plane: vec![128u8; chroma],
1224            v_plane: vec![128u8; chroma],
1225            alpha_plane: None,
1226        }
1227    }
1228
1229    #[test]
1230    fn test_ftyp_box() {
1231        let mut out = Vec::new();
1232        write_ftyp(&mut out);
1233        assert!(out.len() >= 20, "ftyp must be at least 20 bytes");
1234        assert_eq!(&out[4..8], b"ftyp", "box type must be 'ftyp'");
1235        assert_eq!(&out[8..12], b"avif", "major brand must be 'avif'");
1236        // Compatible brands must include 'avif'
1237        let brands_region = &out[8..];
1238        let has_avif = brands_region
1239            .chunks(4)
1240            .any(|c| c.len() == 4 && c == b"avif");
1241        assert!(has_avif, "compatible brands must contain 'avif'");
1242    }
1243
1244    #[test]
1245    fn test_encode_produces_valid_ftyp() {
1246        let image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
1247        let config = AvifConfig::default();
1248        let encoder = AvifEncoder::new(config);
1249        let bytes = encoder.encode(&image).expect("encode failed");
1250        assert!(bytes.len() > 32, "encoded output too short");
1251        // First box must be ftyp
1252        assert_eq!(&bytes[4..8], b"ftyp");
1253        // Major brand must be 'avif'
1254        assert_eq!(&bytes[8..12], b"avif");
1255    }
1256
1257    #[test]
1258    fn test_encode_contains_meta_and_mdat() {
1259        let image = make_test_image(128, 96, 8, YuvFormat::Yuv420);
1260        let encoder = AvifEncoder::new(AvifConfig::default());
1261        let bytes = encoder.encode(&image).expect("encode failed");
1262        assert!(
1263            find_top_level_box(&bytes, b"meta").is_some(),
1264            "meta box missing"
1265        );
1266        assert!(
1267            find_top_level_box(&bytes, b"mdat").is_some(),
1268            "mdat box missing"
1269        );
1270    }
1271
1272    #[test]
1273    fn test_probe_roundtrip_dimensions() {
1274        let image = make_test_image(320, 240, 8, YuvFormat::Yuv420);
1275        let encoder = AvifEncoder::new(AvifConfig::default());
1276        let bytes = encoder.encode(&image).expect("encode failed");
1277        let probe = AvifDecoder::probe(&bytes).expect("probe failed");
1278        assert_eq!(probe.width, 320, "probed width mismatch");
1279        assert_eq!(probe.height, 240, "probed height mismatch");
1280        assert_eq!(probe.bit_depth, 8, "probed bit_depth mismatch");
1281        assert!(!probe.has_alpha, "should not have alpha");
1282    }
1283
1284    #[test]
1285    fn test_probe_10bit() {
1286        let image = make_test_image(160, 120, 10, YuvFormat::Yuv420);
1287        let encoder = AvifEncoder::new(AvifConfig::default());
1288        let bytes = encoder.encode(&image).expect("encode 10-bit failed");
1289        let probe = AvifDecoder::probe(&bytes).expect("probe 10-bit failed");
1290        assert_eq!(probe.bit_depth, 10);
1291    }
1292
1293    #[test]
1294    fn test_probe_12bit() {
1295        let image = make_test_image(64, 64, 12, YuvFormat::Yuv420);
1296        let encoder = AvifEncoder::new(AvifConfig::default());
1297        let bytes = encoder.encode(&image).expect("encode 12-bit failed");
1298        let probe = AvifDecoder::probe(&bytes).expect("probe 12-bit failed");
1299        assert_eq!(probe.bit_depth, 12);
1300    }
1301
1302    #[test]
1303    fn test_decode_roundtrip_color_payload() {
1304        let image = make_test_image(64, 48, 8, YuvFormat::Yuv420);
1305        let encoder = AvifEncoder::new(AvifConfig::default());
1306        let bytes = encoder.encode(&image).expect("encode failed");
1307        let decoded = AvifDecoder::decode(&bytes).expect("decode failed");
1308        assert_eq!(decoded.width, 64);
1309        assert_eq!(decoded.height, 48);
1310        // The raw AV1 OBU should be non-empty
1311        assert!(
1312            !decoded.y_plane.is_empty(),
1313            "decoded y_plane (AV1 OBU) should not be empty"
1314        );
1315    }
1316
1317    #[test]
1318    fn test_encode_with_alpha() {
1319        let mut image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
1320        image.alpha_plane = Some(vec![255u8; 64 * 64]);
1321        let config = AvifConfig {
1322            alpha_quality: Some(80),
1323            ..AvifConfig::default()
1324        };
1325        let encoder = AvifEncoder::new(config);
1326        let bytes = encoder.encode(&image).expect("encode with alpha failed");
1327        let probe = AvifDecoder::probe(&bytes).expect("probe with alpha failed");
1328        assert!(probe.has_alpha, "probe should detect alpha");
1329    }
1330
1331    #[test]
1332    fn test_decode_with_alpha() {
1333        let mut image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
1334        image.alpha_plane = Some(vec![200u8; 64 * 64]);
1335        let config = AvifConfig {
1336            alpha_quality: Some(90),
1337            ..AvifConfig::default()
1338        };
1339        let encoder = AvifEncoder::new(config);
1340        let bytes = encoder.encode(&image).expect("encode failed");
1341        let decoded = AvifDecoder::decode(&bytes).expect("decode failed");
1342        assert!(
1343            decoded.alpha_plane.is_some(),
1344            "decoded image should have alpha"
1345        );
1346        assert!(!decoded
1347            .alpha_plane
1348            .expect("alpha plane should exist")
1349            .is_empty());
1350    }
1351
1352    #[test]
1353    fn test_invalid_signature_rejected() {
1354        let garbage = b"not an avif file at all".to_vec();
1355        assert!(
1356            AvifDecoder::probe(&garbage).is_err(),
1357            "garbage input must be rejected"
1358        );
1359    }
1360
1361    #[test]
1362    fn test_zero_dimension_rejected() {
1363        let image = AvifImage {
1364            width: 0,
1365            height: 100,
1366            depth: 8,
1367            yuv_format: YuvFormat::Yuv420,
1368            y_plane: vec![0u8; 100],
1369            u_plane: vec![],
1370            v_plane: vec![],
1371            alpha_plane: None,
1372        };
1373        let encoder = AvifEncoder::new(AvifConfig::default());
1374        assert!(encoder.encode(&image).is_err());
1375    }
1376
1377    #[test]
1378    fn test_invalid_bit_depth_rejected() {
1379        let image = AvifImage {
1380            width: 8,
1381            height: 8,
1382            depth: 9, // invalid
1383            yuv_format: YuvFormat::Yuv420,
1384            y_plane: vec![0u8; 64],
1385            u_plane: vec![0u8; 16],
1386            v_plane: vec![0u8; 16],
1387            alpha_plane: None,
1388        };
1389        let encoder = AvifEncoder::new(AvifConfig::default());
1390        assert!(encoder.encode(&image).is_err());
1391    }
1392
1393    #[test]
1394    fn test_colr_box_written() {
1395        let image = make_test_image(32, 32, 8, YuvFormat::Yuv420);
1396        let config = AvifConfig {
1397            color_primaries: 9,
1398            transfer_characteristics: 16,
1399            matrix_coefficients: 9,
1400            ..AvifConfig::default()
1401        };
1402        let encoder = AvifEncoder::new(config);
1403        let bytes = encoder.encode(&image).expect("encode failed");
1404        let probe = AvifDecoder::probe(&bytes).expect("probe failed");
1405        assert_eq!(probe.color_primaries, 9);
1406        assert_eq!(probe.transfer_characteristics, 16);
1407    }
1408
1409    #[test]
1410    fn test_yuv444_encode() {
1411        let image = make_test_image(64, 64, 8, YuvFormat::Yuv444);
1412        let encoder = AvifEncoder::new(AvifConfig::default());
1413        let bytes = encoder.encode(&image).expect("yuv444 encode failed");
1414        let probe = AvifDecoder::probe(&bytes).expect("yuv444 probe failed");
1415        assert_eq!(probe.width, 64);
1416        assert_eq!(probe.height, 64);
1417    }
1418
1419    #[test]
1420    fn test_leb128_encoding() {
1421        let mut buf = Vec::new();
1422        write_leb128(&mut buf, 0);
1423        assert_eq!(buf, &[0x00]);
1424
1425        buf.clear();
1426        write_leb128(&mut buf, 127);
1427        assert_eq!(buf, &[0x7F]);
1428
1429        buf.clear();
1430        write_leb128(&mut buf, 128);
1431        assert_eq!(buf, &[0x80, 0x01]);
1432
1433        buf.clear();
1434        write_leb128(&mut buf, 300);
1435        assert_eq!(buf, &[0xAC, 0x02]);
1436    }
1437
1438    #[test]
1439    fn test_bit_writer() {
1440        let mut bw = BitWriter::new();
1441        bw.write_bits(0b10110011, 8);
1442        let out = bw.finish();
1443        assert_eq!(out, &[0b10110011]);
1444
1445        let mut bw = BitWriter::new();
1446        bw.write_bits(1, 1);
1447        bw.write_bits(0, 1);
1448        bw.write_bits(1, 1);
1449        bw.write_bits(0, 4);
1450        bw.write_bits(1, 1);
1451        let out = bw.finish();
1452        assert_eq!(out, &[0b10100001]);
1453    }
1454
1455    #[test]
1456    fn test_av1c_box_structure() {
1457        let image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
1458        let config = AvifConfig::default();
1459        let av1c = build_av1c(&image, &config);
1460        assert_eq!(av1c.len(), 12, "av1C box must be 12 bytes");
1461        assert_eq!(&av1c[4..8], b"av1C");
1462        assert_eq!(av1c[8], 0x81, "marker+version byte must be 0x81");
1463    }
1464
1465    #[test]
1466    fn test_ispe_box_structure() {
1467        let ispe = build_ispe(1920, 1080);
1468        assert_eq!(ispe.len(), 20);
1469        assert_eq!(&ispe[4..8], b"ispe");
1470        let w = u32::from_be_bytes(ispe[12..16].try_into().expect("4-byte slice for width"));
1471        let h = u32::from_be_bytes(ispe[16..20].try_into().expect("4-byte slice for height"));
1472        assert_eq!(w, 1920);
1473        assert_eq!(h, 1080);
1474    }
1475
1476    #[test]
1477    fn test_large_image_encode() {
1478        let image = make_test_image(3840, 2160, 8, YuvFormat::Yuv420);
1479        let encoder = AvifEncoder::new(AvifConfig::default());
1480        let bytes = encoder.encode(&image).expect("4K encode failed");
1481        let probe = AvifDecoder::probe(&bytes).expect("4K probe failed");
1482        assert_eq!(probe.width, 3840);
1483        assert_eq!(probe.height, 2160);
1484    }
1485}