use crate::boxes::BoxBuilder;
#[derive(Debug, Clone, Copy)]
pub struct Av1cConfig {
pub seq_profile: u8,
pub seq_level_idx_0: u8,
pub seq_tier_0: u8,
pub high_bitdepth: bool,
pub twelve_bit: bool,
pub monochrome: bool,
pub chroma_subsampling_x: u8,
pub chroma_subsampling_y: u8,
pub chroma_sample_position: u8,
}
#[derive(Debug, Clone, Copy)]
pub struct NclxColr {
pub colour_primaries: u16,
pub transfer_characteristics: u16,
pub matrix_coefficients: u16,
pub full_range: bool,
}
#[derive(Debug, Clone)]
pub struct AvifStillImage<'a> {
pub width: u32,
pub height: u32,
pub bit_depth: u8,
pub num_channels: u8,
pub av1c: Av1cConfig,
pub nclx: NclxColr,
pub item_data: &'a [u8],
}
#[must_use]
pub fn write_avif_still(img: &AvifStillImage) -> Vec<u8> {
let mut bb = BoxBuilder::new();
write_ftyp(&mut bb);
let extent_offset_pos = write_meta(&mut bb, img);
let mdat_start = bb.begin_box(b"mdat");
let payload_pos = bb.len();
bb.bytes(img.item_data);
bb.end_box(mdat_start);
bb.patch_u32(extent_offset_pos, payload_pos as u32);
bb.into_vec()
}
fn write_ftyp(bb: &mut BoxBuilder) {
let start = bb.begin_box(b"ftyp");
bb.bytes(b"avif"); bb.u32(0); bb.bytes(b"avif");
bb.bytes(b"mif1");
bb.bytes(b"miaf");
bb.bytes(b"MA1A");
bb.end_box(start);
}
fn write_meta(bb: &mut BoxBuilder, img: &AvifStillImage) -> usize {
let start = bb.begin_box(b"meta");
bb.full_box(0, 0);
write_hdlr(bb);
write_pitm(bb);
let extent_offset_pos = write_iloc(bb, img.item_data.len() as u32);
write_iinf(bb);
write_iprp(bb, img);
bb.end_box(start);
extent_offset_pos
}
fn write_hdlr(bb: &mut BoxBuilder) {
let start = bb.begin_box(b"hdlr");
bb.full_box(0, 0);
bb.u32(0); bb.bytes(b"pict"); bb.u32(0); bb.u32(0); bb.u32(0); bb.u8(0); bb.end_box(start);
}
fn write_pitm(bb: &mut BoxBuilder) {
let start = bb.begin_box(b"pitm");
bb.full_box(0, 0);
bb.u16(1); bb.end_box(start);
}
fn write_iloc(bb: &mut BoxBuilder, extent_length: u32) -> usize {
let start = bb.begin_box(b"iloc");
bb.full_box(0, 0);
bb.u8(0x44); bb.u8(0x00); bb.u16(1); bb.u16(1); bb.u16(0); bb.u16(1); let extent_offset_pos = bb.reserve_u32(); bb.u32(extent_length); bb.end_box(start);
extent_offset_pos
}
fn write_iinf(bb: &mut BoxBuilder) {
let start = bb.begin_box(b"iinf");
bb.full_box(0, 0);
bb.u16(1); let infe = bb.begin_box(b"infe");
bb.full_box(2, 0); bb.u16(1); bb.u16(0); bb.bytes(b"av01"); bb.u8(0); bb.end_box(infe);
bb.end_box(start);
}
fn write_iprp(bb: &mut BoxBuilder, img: &AvifStillImage) {
let start = bb.begin_box(b"iprp");
let ipco = bb.begin_box(b"ipco");
write_av1c(bb, &img.av1c); write_ispe(bb, img.width, img.height); write_pixi(bb, img.num_channels, img.bit_depth); write_colr(bb, &img.nclx); bb.end_box(ipco);
write_ipma(bb);
bb.end_box(start);
}
fn write_av1c(bb: &mut BoxBuilder, c: &Av1cConfig) {
let start = bb.begin_box(b"av1C");
bb.u8(0x81); bb.u8((c.seq_profile << 5) | (c.seq_level_idx_0 & 0x1f));
bb.u8((c.seq_tier_0 << 7)
| (u8::from(c.high_bitdepth) << 6)
| (u8::from(c.twelve_bit) << 5)
| (u8::from(c.monochrome) << 4)
| (c.chroma_subsampling_x << 3)
| (c.chroma_subsampling_y << 2)
| (c.chroma_sample_position & 0x3));
bb.u8(0x00); bb.end_box(start);
}
fn write_ispe(bb: &mut BoxBuilder, width: u32, height: u32) {
let start = bb.begin_box(b"ispe");
bb.full_box(0, 0);
bb.u32(width);
bb.u32(height);
bb.end_box(start);
}
fn write_pixi(bb: &mut BoxBuilder, num_channels: u8, bit_depth: u8) {
let start = bb.begin_box(b"pixi");
bb.full_box(0, 0);
bb.u8(num_channels);
for _ in 0..num_channels {
bb.u8(bit_depth);
}
bb.end_box(start);
}
fn write_colr(bb: &mut BoxBuilder, c: &NclxColr) {
let start = bb.begin_box(b"colr");
bb.bytes(b"nclx");
bb.u16(c.colour_primaries);
bb.u16(c.transfer_characteristics);
bb.u16(c.matrix_coefficients);
bb.u8(u8::from(c.full_range) << 7); bb.end_box(start);
}
fn write_ipma(bb: &mut BoxBuilder) {
let start = bb.begin_box(b"ipma");
bb.full_box(0, 0);
bb.u32(1); bb.u16(1); bb.u8(4); bb.u8(0x80 | 1); bb.u8(2); bb.u8(3); bb.u8(4); bb.end_box(start);
}
#[cfg(test)]
mod tests {
use super::*;
fn be32(buf: &[u8], pos: usize) -> u32 {
u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
}
fn top_level_boxes(buf: &[u8]) -> Vec<([u8; 4], usize, usize)> {
let mut out = Vec::new();
let mut pos = 0;
while pos + 8 <= buf.len() {
let size = be32(buf, pos) as usize;
let ty = [buf[pos + 4], buf[pos + 5], buf[pos + 6], buf[pos + 7]];
assert!(
size >= 8 && pos + size <= buf.len(),
"bad box size {size} at {pos}"
);
out.push((ty, pos + 8, size - 8));
pos += size;
}
assert_eq!(pos, buf.len(), "boxes do not tile the file exactly");
out
}
fn sample_image(item: &[u8]) -> Vec<u8> {
let img = AvifStillImage {
width: 4,
height: 4,
bit_depth: 8,
num_channels: 3,
av1c: Av1cConfig {
seq_profile: 1,
seq_level_idx_0: 1,
seq_tier_0: 0,
high_bitdepth: false,
twelve_bit: false,
monochrome: false,
chroma_subsampling_x: 0,
chroma_subsampling_y: 0,
chroma_sample_position: 0,
},
nclx: NclxColr {
colour_primaries: 1,
transfer_characteristics: 13,
matrix_coefficients: 0,
full_range: true,
},
item_data: item,
};
write_avif_still(&img)
}
#[test]
fn top_level_layout_is_ftyp_meta_mdat() {
let item = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02];
let file = sample_image(&item);
let boxes = top_level_boxes(&file);
let types: Vec<[u8; 4]> = boxes.iter().map(|b| b.0).collect();
assert_eq!(types, vec![*b"ftyp", *b"meta", *b"mdat"]);
}
#[test]
fn ftyp_lists_required_brands() {
let file = sample_image(&[0u8; 4]);
let (_, body, len) = top_level_boxes(&file)[0];
let ftyp = &file[body..body + len];
assert_eq!(&ftyp[0..4], b"avif"); let rest = &ftyp[8..]; for brand in [b"avif", b"mif1", b"miaf", b"MA1A"] {
assert!(rest.windows(4).any(|w| w == brand), "missing brand");
}
}
#[test]
fn iloc_extent_points_at_mdat_payload() {
let item = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let file = sample_image(&item);
let boxes = top_level_boxes(&file);
let (_, mdat_body, mdat_len) = *boxes.iter().find(|b| &b.0 == b"mdat").unwrap();
assert_eq!(&file[mdat_body..mdat_body + mdat_len], &item);
let iloc_pos = file.windows(4).position(|w| w == b"iloc").unwrap();
let body = iloc_pos + 4;
let extent_offset = be32(&file, body + 4 + 1 + 1 + 2 + 2 + 2 + 2);
let extent_length = be32(&file, body + 4 + 1 + 1 + 2 + 2 + 2 + 2 + 4);
assert_eq!(
extent_offset as usize, mdat_body,
"extent offset must hit mdat payload"
);
assert_eq!(extent_length as usize, item.len());
assert_eq!(
&file[extent_offset as usize..(extent_offset + extent_length) as usize],
&item
);
}
#[test]
fn meta_contains_required_property_boxes() {
let file = sample_image(&[0u8; 8]);
for fourcc in [
b"hdlr", b"pitm", b"iinf", b"infe", b"iprp", b"ipco", b"ipma", b"av1C", b"ispe",
b"pixi", b"colr",
] {
assert!(
file.windows(4).any(|w| w == fourcc),
"missing box {fourcc:?}"
);
}
}
}