use crate::error::CodecError;
#[derive(Debug, Clone)]
pub struct AvifConfig {
pub quality: u8,
pub speed: u8,
pub color_primaries: u8,
pub transfer_characteristics: u8,
pub matrix_coefficients: u8,
pub full_range: bool,
pub alpha_quality: Option<u8>,
}
impl Default for AvifConfig {
fn default() -> Self {
Self {
quality: 60,
speed: 6,
color_primaries: 1,
transfer_characteristics: 1,
matrix_coefficients: 1,
full_range: false,
alpha_quality: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum YuvFormat {
Yuv420,
Yuv422,
Yuv444,
}
#[derive(Debug, Clone)]
pub struct AvifImage {
pub width: u32,
pub height: u32,
pub depth: u8,
pub yuv_format: YuvFormat,
pub y_plane: Vec<u8>,
pub u_plane: Vec<u8>,
pub v_plane: Vec<u8>,
pub alpha_plane: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct AvifProbeResult {
pub width: u32,
pub height: u32,
pub bit_depth: u8,
pub has_alpha: bool,
pub color_primaries: u8,
pub transfer_characteristics: u8,
}
#[derive(Debug, Clone)]
pub struct AvifEncoder {
config: AvifConfig,
}
impl AvifEncoder {
pub fn new(config: AvifConfig) -> Self {
Self { config }
}
pub fn encode(&self, image: &AvifImage) -> Result<Vec<u8>, CodecError> {
validate_image(image)?;
let av1_payload = build_av1_obu(image, &self.config);
let has_alpha = self.config.alpha_quality.is_some() && image.alpha_plane.is_some();
let alpha_payload = if has_alpha {
Some(build_alpha_av1_obu(image))
} else {
None
};
let mut out = Vec::with_capacity(4096 + av1_payload.len());
write_ftyp(&mut out);
let meta_buf = build_meta(image, &self.config, &av1_payload, &alpha_payload)?;
out.extend_from_slice(&meta_buf);
let mdat_header_size = 8u32; let mdat_size = mdat_header_size as usize
+ av1_payload.len()
+ alpha_payload.as_ref().map_or(0, |a| a.len());
write_u32(&mut out, mdat_size as u32);
out.extend_from_slice(b"mdat");
out.extend_from_slice(&av1_payload);
if let Some(ref ap) = alpha_payload {
out.extend_from_slice(ap);
}
patch_iloc_offsets(&mut out, &meta_buf, &av1_payload, alpha_payload.as_deref())?;
Ok(out)
}
}
#[derive(Debug, Default, Clone)]
pub struct AvifDecoder;
impl AvifDecoder {
pub fn new() -> Self {
Self
}
pub fn decode(data: &[u8]) -> Result<AvifImage, CodecError> {
let probe = Self::probe(data)?;
let (color_offset, color_len, alpha_offset, alpha_len) =
locate_mdat_items(data, probe.has_alpha)?;
let y_plane = data
.get(color_offset..color_offset + color_len)
.ok_or_else(|| CodecError::InvalidBitstream("mdat color extent out of range".into()))?
.to_vec();
let alpha_plane = if probe.has_alpha {
let (ao, al) = (alpha_offset, alpha_len);
let slice = data
.get(ao..ao + al)
.ok_or_else(|| {
CodecError::InvalidBitstream("mdat alpha extent out of range".into())
})?
.to_vec();
Some(slice)
} else {
None
};
Ok(AvifImage {
width: probe.width,
height: probe.height,
depth: probe.bit_depth,
yuv_format: YuvFormat::Yuv420,
y_plane,
u_plane: Vec::new(),
v_plane: Vec::new(),
alpha_plane,
})
}
pub fn probe(data: &[u8]) -> Result<AvifProbeResult, CodecError> {
check_avif_signature(data)?;
parse_meta_for_probe(data)
}
}
fn build_av1_obu(image: &AvifImage, config: &AvifConfig) -> Vec<u8> {
let mut bits = BitWriter::new();
let obu_header: u8 = (1 << 3) | (1 << 1); bits.write_byte(obu_header);
let mut seq = BitWriter::new();
write_sequence_header_payload(&mut seq, image, config);
let seq_bytes = seq.finish();
let mut leb = Vec::new();
write_leb128(&mut leb, seq_bytes.len() as u64);
bits.extend_bytes(&leb);
bits.extend_bytes(&seq_bytes);
bits.write_byte((2 << 3) | (1 << 1)); bits.write_byte(0);
bits.finish()
}
fn build_alpha_av1_obu(image: &AvifImage) -> Vec<u8> {
let alpha_config = AvifConfig {
quality: 80,
color_primaries: 1,
transfer_characteristics: 1,
matrix_coefficients: 0, full_range: true,
..AvifConfig::default()
};
let mono = AvifImage {
width: image.width,
height: image.height,
depth: image.depth,
yuv_format: YuvFormat::Yuv444,
y_plane: image.alpha_plane.clone().unwrap_or_default(),
u_plane: Vec::new(),
v_plane: Vec::new(),
alpha_plane: None,
};
build_av1_obu(&mono, &alpha_config)
}
fn write_sequence_header_payload(bits: &mut BitWriter, image: &AvifImage, config: &AvifConfig) {
let seq_profile: u8 = match image.yuv_format {
YuvFormat::Yuv444 => 1,
_ => 0,
};
bits.write_bits(seq_profile as u32, 3);
bits.write_bits(1, 1);
bits.write_bits(1, 1);
bits.write_bits(13, 5);
let high_bitdepth = image.depth >= 10;
bits.write_bits(high_bitdepth as u32, 1);
if seq_profile == 2 && high_bitdepth {
let twelve_bit = image.depth == 12;
bits.write_bits(twelve_bit as u32, 1);
}
bits.write_bits(0, 1);
bits.write_bits(1, 1);
bits.write_bits(config.color_primaries as u32, 8);
bits.write_bits(config.transfer_characteristics as u32, 8);
bits.write_bits(config.matrix_coefficients as u32, 8);
bits.write_bits(config.full_range as u32, 1);
let (sub_x, sub_y): (u32, u32) = match image.yuv_format {
YuvFormat::Yuv420 => (1, 1),
YuvFormat::Yuv422 => (1, 0),
YuvFormat::Yuv444 => (0, 0),
};
if seq_profile != 1 {
if config.color_primaries == 1
&& config.transfer_characteristics == 1
&& config.matrix_coefficients == 1
{
bits.write_bits(0, 1);
} else {
bits.write_bits(sub_x, 1);
if sub_x == 1 {
bits.write_bits(sub_y, 1);
}
if sub_x == 1 && sub_y == 1 {
bits.write_bits(0, 2); }
}
}
bits.write_bits(0, 1);
let w_bits = bits_needed(image.width);
let h_bits = bits_needed(image.height);
bits.write_bits((w_bits - 1) as u32, 4); bits.write_bits((h_bits - 1) as u32, 4); bits.write_bits((image.width - 1) as u32, w_bits as u32);
bits.write_bits((image.height - 1) as u32, h_bits as u32);
bits.write_bits(0, 1);
}
fn bits_needed(n: u32) -> u8 {
if n == 0 {
return 1;
}
let mut bits = 0u8;
let mut v = n;
while v > 0 {
bits += 1;
v >>= 1;
}
bits
}
fn write_leb128(buf: &mut Vec<u8>, mut value: u64) {
loop {
let mut byte = (value & 0x7F) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
buf.push(byte);
if value == 0 {
break;
}
}
}
fn write_ftyp(out: &mut Vec<u8>) {
let compat: &[&[u8; 4]] = &[b"avif", b"mif1", b"miaf"];
let size = 4 + 4 + 4 + 4 + 4 * compat.len(); write_u32(out, size as u32);
out.extend_from_slice(b"ftyp");
out.extend_from_slice(b"avif"); write_u32(out, 0); for brand in compat {
out.extend_from_slice(*brand);
}
}
fn build_meta(
image: &AvifImage,
config: &AvifConfig,
av1_payload: &[u8],
alpha_payload: &Option<Vec<u8>>,
) -> Result<Vec<u8>, CodecError> {
let has_alpha = alpha_payload.is_some();
let mut body = Vec::<u8>::new();
body.extend_from_slice(&build_hdlr());
body.extend_from_slice(&build_pitm(1));
body.extend_from_slice(&build_iloc(
has_alpha,
av1_payload.len(),
alpha_payload.as_ref().map_or(0, |a| a.len()),
));
body.extend_from_slice(&build_iinf(has_alpha));
body.extend_from_slice(&build_iprp(image, config, has_alpha)?);
let meta_size = 4 + 4 + 4 + body.len(); let mut meta = Vec::with_capacity(meta_size);
write_u32(&mut meta, meta_size as u32);
meta.extend_from_slice(b"meta");
write_u32(&mut meta, 0u32); meta.extend_from_slice(&body);
Ok(meta)
}
fn build_hdlr() -> Vec<u8> {
let size = 4 + 4 + 4 + 4 + 4 + 12 + 1; let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"hdlr");
write_u32(&mut b, 0); write_u32(&mut b, 0); b.extend_from_slice(b"pict"); b.extend_from_slice(&[0u8; 12]); b.push(0); b
}
fn build_pitm(item_id: u16) -> Vec<u8> {
let size = 4 + 4 + 4 + 2; let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"pitm");
write_u32(&mut b, 0); write_u16(&mut b, item_id);
b
}
fn build_iloc(has_alpha: bool, color_len: usize, alpha_len: usize) -> Vec<u8> {
let item_count: u16 = if has_alpha { 2 } else { 1 };
let item_entry_size = 2 + 2 + 2 + 2 + 4 + 4;
let payload_size = 1 + 1 + 2 + item_count as usize * item_entry_size;
let size = 8 + 4 + payload_size;
let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"iloc");
write_u32(&mut b, 1 << 24);
b.push(0x44); b.push(0x00);
write_u16(&mut b, item_count);
write_u16(&mut b, 1); write_u16(&mut b, 0); write_u16(&mut b, 0); write_u16(&mut b, 1); write_u32(&mut b, 0); write_u32(&mut b, color_len as u32);
if has_alpha {
write_u16(&mut b, 2); write_u16(&mut b, 0); write_u16(&mut b, 0); write_u16(&mut b, 1); write_u32(&mut b, 0); write_u32(&mut b, alpha_len as u32); }
b
}
fn build_iinf(has_alpha: bool) -> Vec<u8> {
let item_count: u16 = if has_alpha { 2 } else { 1 };
let entry1 = build_infe(1, b"av01", b"Color Image\0");
let entry2 = if has_alpha {
Some(build_infe(2, b"av01", b"Alpha Image\0"))
} else {
None
};
let entries_size = entry1.len() + entry2.as_ref().map_or(0, |e| e.len());
let size = 8 + 4 + 2 + entries_size; let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"iinf");
write_u32(&mut b, 0); write_u16(&mut b, item_count);
b.extend_from_slice(&entry1);
if let Some(e2) = entry2 {
b.extend_from_slice(&e2);
}
b
}
fn build_infe(item_id: u16, item_type: &[u8; 4], item_name: &[u8]) -> Vec<u8> {
let payload = 2 + 2 + 4 + item_name.len();
let size = 8 + 4 + payload; let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"infe");
write_u32(&mut b, 2 << 24); write_u16(&mut b, item_id);
write_u16(&mut b, 0); b.extend_from_slice(item_type);
b.extend_from_slice(item_name);
b
}
fn build_iprp(
image: &AvifImage,
config: &AvifConfig,
has_alpha: bool,
) -> Result<Vec<u8>, CodecError> {
let ispe = build_ispe(image.width, image.height);
let colr = build_colr(config);
let av1c = build_av1c(image, config);
let pixi = build_pixi(image.depth);
let ipco_payload_len = ispe.len() + colr.len() + av1c.len() + pixi.len();
let ipco_size = 8 + ipco_payload_len;
let mut ipco = Vec::with_capacity(ipco_size);
write_u32(&mut ipco, ipco_size as u32);
ipco.extend_from_slice(b"ipco");
ipco.extend_from_slice(&ispe);
ipco.extend_from_slice(&colr);
ipco.extend_from_slice(&av1c);
ipco.extend_from_slice(&pixi);
let ipma = build_ipma(has_alpha);
let iprp_size = 8 + ipco.len() + ipma.len();
let mut b = Vec::with_capacity(iprp_size);
write_u32(&mut b, iprp_size as u32);
b.extend_from_slice(b"iprp");
b.extend_from_slice(&ipco);
b.extend_from_slice(&ipma);
Ok(b)
}
fn build_ispe(width: u32, height: u32) -> Vec<u8> {
let size = 8 + 4 + 4 + 4; let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"ispe");
write_u32(&mut b, 0); write_u32(&mut b, width);
write_u32(&mut b, height);
b
}
fn build_colr(config: &AvifConfig) -> Vec<u8> {
let payload_size = 4 + 2 + 2 + 2 + 1;
let size = 8 + payload_size;
let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"colr");
b.extend_from_slice(b"nclx"); write_u16(&mut b, config.color_primaries as u16);
write_u16(&mut b, config.transfer_characteristics as u16);
write_u16(&mut b, config.matrix_coefficients as u16);
let full_range_byte: u8 = if config.full_range { 0x80 } else { 0x00 };
b.push(full_range_byte);
b
}
fn build_av1c(image: &AvifImage, config: &AvifConfig) -> Vec<u8> {
let seq_profile: u8 = match image.yuv_format {
YuvFormat::Yuv444 => 1,
_ => 0,
};
let seq_level_idx_0: u8 = 13;
let byte0: u8 = 0x81; let byte1: u8 = (seq_profile << 5) | seq_level_idx_0;
let high_bitdepth = image.depth >= 10;
let twelve_bit = image.depth == 12;
let (sub_x, sub_y): (u8, u8) = match image.yuv_format {
YuvFormat::Yuv420 => (1, 1),
YuvFormat::Yuv422 => (1, 0),
YuvFormat::Yuv444 => (0, 0),
};
let byte2: u8 = (high_bitdepth as u8) << 6
| (twelve_bit as u8) << 5
| 0 << 4 | sub_x << 3
| sub_y << 2
| 0; let _ = config; let byte3: u8 = 0x00;
let size = 8 + 4; let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"av1C");
b.push(byte0);
b.push(byte1);
b.push(byte2);
b.push(byte3);
b
}
fn build_pixi(depth: u8) -> Vec<u8> {
let num_channels: u8 = 3;
let size = 8 + 4 + 1 + num_channels as usize;
let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"pixi");
write_u32(&mut b, 0); b.push(num_channels);
for _ in 0..num_channels {
b.push(depth);
}
b
}
fn build_ipma(has_alpha: bool) -> Vec<u8> {
let item_count: u32 = if has_alpha { 2 } else { 1 };
let assoc_per_item: &[(u8, u8)] = &[
(0, 1), (0, 2), (1, 3), (0, 4), ];
let per_item_size = 2 + 1 + assoc_per_item.len(); let payload_size = 4 + item_count as usize * per_item_size;
let size = 8 + 4 + payload_size;
let mut b = Vec::with_capacity(size);
write_u32(&mut b, size as u32);
b.extend_from_slice(b"ipma");
write_u32(&mut b, 0); write_u32(&mut b, item_count);
for item_id in 1..=item_count as u16 {
write_u16(&mut b, item_id);
b.push(assoc_per_item.len() as u8);
for &(essential, prop_idx) in assoc_per_item {
b.push((essential << 7) | (prop_idx & 0x7F));
}
}
b
}
fn patch_iloc_offsets(
out: &mut Vec<u8>,
meta_buf: &[u8],
av1_payload: &[u8],
alpha_payload: Option<&[u8]>,
) -> Result<(), CodecError> {
let ftyp_size = u32::from_be_bytes(
out.get(0..4)
.ok_or_else(|| CodecError::Internal("output too short for ftyp size".into()))?
.try_into()
.map_err(|_| CodecError::Internal("slice conversion error".into()))?,
) as usize;
let meta_size = meta_buf.len();
let mdat_data_start = ftyp_size + meta_size + 8;
let color_offset = mdat_data_start as u32;
let alpha_offset = (mdat_data_start + av1_payload.len()) as u32;
let meta_start = ftyp_size;
let meta_body_start = meta_start + 12;
let iloc_pos = find_box_in(out, meta_body_start, meta_start + meta_size, b"iloc")
.ok_or_else(|| CodecError::Internal("iloc box not found in output".into()))?;
let item0_start = iloc_pos + 16;
let color_extent_offset_pos = item0_start + 6; let color_extent_offset_pos = item0_start + 8;
patch_u32(out, color_extent_offset_pos, color_offset)?;
if alpha_payload.is_some() {
let item1_start = item0_start + 16;
let alpha_extent_offset_pos = item1_start + 8;
patch_u32(out, alpha_extent_offset_pos, alpha_offset)?;
}
Ok(())
}
fn find_box_in(data: &[u8], start: usize, end: usize, box_type: &[u8; 4]) -> Option<usize> {
let mut pos = start;
while pos + 8 <= end.min(data.len()) {
let size = u32::from_be_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
if size < 8 {
break;
}
if &data[pos + 4..pos + 8] == box_type {
return Some(pos);
}
pos += size;
}
None
}
fn patch_u32(buf: &mut Vec<u8>, pos: usize, value: u32) -> Result<(), CodecError> {
if pos + 4 > buf.len() {
return Err(CodecError::Internal(format!(
"patch_u32: pos={pos} out of range (buf.len={})",
buf.len()
)));
}
let bytes = value.to_be_bytes();
buf[pos] = bytes[0];
buf[pos + 1] = bytes[1];
buf[pos + 2] = bytes[2];
buf[pos + 3] = bytes[3];
Ok(())
}
fn check_avif_signature(data: &[u8]) -> Result<(), CodecError> {
if data.len() < 12 {
return Err(CodecError::InvalidBitstream(
"file too short to be AVIF".into(),
));
}
let size = u32::from_be_bytes(
data[0..4]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("cannot read ftyp size".into()))?,
) as usize;
if size < 12 || size > data.len() {
return Err(CodecError::InvalidBitstream("invalid ftyp box size".into()));
}
if &data[4..8] != b"ftyp" {
return Err(CodecError::InvalidBitstream("first box is not ftyp".into()));
}
let brands_region = &data[8..size];
let has_avif = brands_region
.chunks(4)
.any(|c| c.len() == 4 && c == b"avif");
if !has_avif {
return Err(CodecError::InvalidBitstream(
"ftyp does not contain 'avif' brand".into(),
));
}
Ok(())
}
fn parse_meta_for_probe(data: &[u8]) -> Result<AvifProbeResult, CodecError> {
let meta_pos = find_top_level_box(data, b"meta")
.ok_or_else(|| CodecError::InvalidBitstream("meta box not found".into()))?;
let meta_size = u32::from_be_bytes(
data[meta_pos..meta_pos + 4]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("meta size read error".into()))?,
) as usize;
let meta_end = meta_pos + meta_size;
let meta_body = meta_pos + 12;
let (width, height) = parse_ispe(data, meta_body, meta_end)?;
let (color_primaries, transfer_characteristics) =
parse_colr(data, meta_body, meta_end).unwrap_or((1, 1));
let bit_depth = parse_pixi(data, meta_body, meta_end).unwrap_or(8);
let has_alpha = parse_iinf_has_alpha(data, meta_body, meta_end);
Ok(AvifProbeResult {
width,
height,
bit_depth,
has_alpha,
color_primaries,
transfer_characteristics,
})
}
fn parse_ispe(data: &[u8], start: usize, end: usize) -> Result<(u32, u32), CodecError> {
let pos = find_box_in(data, start, end, b"iprp")
.and_then(|iprp| {
let iprp_end =
iprp + u32::from_be_bytes(data[iprp..iprp + 4].try_into().ok()?) as usize;
find_box_in(data, iprp + 8, iprp_end, b"ipco").and_then(|ipco| {
let ipco_end =
ipco + u32::from_be_bytes(data[ipco..ipco + 4].try_into().ok()?) as usize;
find_box_in(data, ipco + 8, ipco_end, b"ispe")
})
})
.ok_or_else(|| CodecError::InvalidBitstream("ispe not found".into()))?;
if pos + 20 > data.len() {
return Err(CodecError::InvalidBitstream("ispe box truncated".into()));
}
let w = u32::from_be_bytes(
data[pos + 12..pos + 16]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("ispe width read error".into()))?,
);
let h = u32::from_be_bytes(
data[pos + 16..pos + 20]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("ispe height read error".into()))?,
);
Ok((w, h))
}
fn parse_colr(data: &[u8], start: usize, end: usize) -> Option<(u8, u8)> {
let iprp = find_box_in(data, start, end, b"iprp")?;
let iprp_end = iprp + u32::from_be_bytes(data[iprp..iprp + 4].try_into().ok()?) as usize;
let ipco = find_box_in(data, iprp + 8, iprp_end, b"ipco")?;
let ipco_end = ipco + u32::from_be_bytes(data[ipco..ipco + 4].try_into().ok()?) as usize;
let pos = find_box_in(data, ipco + 8, ipco_end, b"colr")?;
if pos + 15 > data.len() {
return None;
}
if &data[pos + 8..pos + 12] != b"nclx" {
return None;
}
let cp = u16::from_be_bytes(data[pos + 12..pos + 14].try_into().ok()?) as u8;
let tc = u16::from_be_bytes(data[pos + 14..pos + 16].try_into().ok()?) as u8;
Some((cp, tc))
}
fn parse_pixi(data: &[u8], start: usize, end: usize) -> Option<u8> {
let iprp = find_box_in(data, start, end, b"iprp")?;
let iprp_end = iprp + u32::from_be_bytes(data[iprp..iprp + 4].try_into().ok()?) as usize;
let ipco = find_box_in(data, iprp + 8, iprp_end, b"ipco")?;
let ipco_end = ipco + u32::from_be_bytes(data[ipco..ipco + 4].try_into().ok()?) as usize;
let pos = find_box_in(data, ipco + 8, ipco_end, b"pixi")?;
if pos + 14 > data.len() {
return None;
}
Some(data[pos + 13])
}
fn parse_iinf_has_alpha(data: &[u8], start: usize, end: usize) -> bool {
let pos = match find_box_in(data, start, end, b"iinf") {
Some(p) => p,
None => return false,
};
let iinf_size = u32::from_be_bytes(match data[pos..pos + 4].try_into() {
Ok(b) => b,
Err(_) => return false,
}) as usize;
let entry_count = u16::from_be_bytes(match data[pos + 12..pos + 14].try_into() {
Ok(b) => b,
Err(_) => return false,
});
entry_count >= 2 && iinf_size >= 14
}
fn locate_mdat_items(
data: &[u8],
has_alpha: bool,
) -> Result<(usize, usize, usize, usize), CodecError> {
let meta_pos = find_top_level_box(data, b"meta")
.ok_or_else(|| CodecError::InvalidBitstream("meta box not found".into()))?;
let meta_size = u32::from_be_bytes(
data[meta_pos..meta_pos + 4]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("meta size".into()))?,
) as usize;
let meta_end = meta_pos + meta_size;
let meta_body = meta_pos + 12;
let iloc_pos = find_box_in(data, meta_body, meta_end, b"iloc")
.ok_or_else(|| CodecError::InvalidBitstream("iloc not found".into()))?;
let version = data[iloc_pos + 8];
if version != 1 {
return Err(CodecError::UnsupportedFeature(format!(
"iloc version {version} not supported"
)));
}
let item_count = u16::from_be_bytes(
data[iloc_pos + 14..iloc_pos + 16]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("iloc item_count".into()))?,
);
if item_count == 0 {
return Err(CodecError::InvalidBitstream("iloc has no items".into()));
}
let item0 = iloc_pos + 16;
let color_offset = u32::from_be_bytes(
data[item0 + 8..item0 + 12]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("color extent offset".into()))?,
) as usize;
let color_len = u32::from_be_bytes(
data[item0 + 12..item0 + 16]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("color extent length".into()))?,
) as usize;
let (alpha_offset, alpha_len) = if has_alpha && item_count >= 2 {
let item1 = item0 + 16;
let ao = u32::from_be_bytes(
data[item1 + 8..item1 + 12]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("alpha extent offset".into()))?,
) as usize;
let al = u32::from_be_bytes(
data[item1 + 12..item1 + 16]
.try_into()
.map_err(|_| CodecError::InvalidBitstream("alpha extent length".into()))?,
) as usize;
(ao, al)
} else {
(0, 0)
};
Ok((color_offset, color_len, alpha_offset, alpha_len))
}
fn find_top_level_box(data: &[u8], box_type: &[u8; 4]) -> Option<usize> {
let mut pos = 0usize;
while pos + 8 <= data.len() {
let size = u32::from_be_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
if size < 8 {
break;
}
if &data[pos + 4..pos + 8] == box_type {
return Some(pos);
}
pos += size;
}
None
}
fn validate_image(image: &AvifImage) -> Result<(), CodecError> {
if image.width == 0 || image.height == 0 {
return Err(CodecError::InvalidParameter(
"image dimensions must be non-zero".into(),
));
}
if ![8u8, 10, 12].contains(&image.depth) {
return Err(CodecError::InvalidParameter(format!(
"unsupported bit depth {}; must be 8, 10, or 12",
image.depth
)));
}
let luma_samples = image.width as usize * image.height as usize;
let bytes_per_sample: usize = if image.depth > 8 { 2 } else { 1 };
let min_y = luma_samples * bytes_per_sample;
if image.y_plane.len() < min_y {
return Err(CodecError::InvalidParameter(format!(
"y_plane too small: need {min_y}, have {}",
image.y_plane.len()
)));
}
Ok(())
}
fn write_u32(out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&v.to_be_bytes());
}
fn write_u16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_be_bytes());
}
struct BitWriter {
buf: Vec<u8>,
current: u8,
bits_in_current: u8,
}
impl BitWriter {
fn new() -> Self {
Self {
buf: Vec::new(),
current: 0,
bits_in_current: 0,
}
}
fn write_bits(&mut self, value: u32, n: u32) {
for i in (0..n).rev() {
let bit = ((value >> i) & 1) as u8;
self.current = (self.current << 1) | bit;
self.bits_in_current += 1;
if self.bits_in_current == 8 {
self.buf.push(self.current);
self.current = 0;
self.bits_in_current = 0;
}
}
}
fn write_byte(&mut self, byte: u8) {
self.write_bits(byte as u32, 8);
}
fn extend_bytes(&mut self, bytes: &[u8]) {
for &b in bytes {
self.write_byte(b);
}
}
fn finish(mut self) -> Vec<u8> {
if self.bits_in_current > 0 {
self.current <<= 8 - self.bits_in_current;
self.buf.push(self.current);
}
self.buf
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_image(width: u32, height: u32, depth: u8, fmt: YuvFormat) -> AvifImage {
let luma = width as usize * height as usize * if depth > 8 { 2 } else { 1 };
let chroma = match fmt {
YuvFormat::Yuv420 => (width as usize / 2) * (height as usize / 2),
YuvFormat::Yuv422 => (width as usize / 2) * height as usize,
YuvFormat::Yuv444 => width as usize * height as usize,
} * if depth > 8 { 2 } else { 1 };
AvifImage {
width,
height,
depth,
yuv_format: fmt,
y_plane: vec![128u8; luma],
u_plane: vec![128u8; chroma],
v_plane: vec![128u8; chroma],
alpha_plane: None,
}
}
#[test]
fn test_ftyp_box() {
let mut out = Vec::new();
write_ftyp(&mut out);
assert!(out.len() >= 20, "ftyp must be at least 20 bytes");
assert_eq!(&out[4..8], b"ftyp", "box type must be 'ftyp'");
assert_eq!(&out[8..12], b"avif", "major brand must be 'avif'");
let brands_region = &out[8..];
let has_avif = brands_region
.chunks(4)
.any(|c| c.len() == 4 && c == b"avif");
assert!(has_avif, "compatible brands must contain 'avif'");
}
#[test]
fn test_encode_produces_valid_ftyp() {
let image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
let config = AvifConfig::default();
let encoder = AvifEncoder::new(config);
let bytes = encoder.encode(&image).expect("encode failed");
assert!(bytes.len() > 32, "encoded output too short");
assert_eq!(&bytes[4..8], b"ftyp");
assert_eq!(&bytes[8..12], b"avif");
}
#[test]
fn test_encode_contains_meta_and_mdat() {
let image = make_test_image(128, 96, 8, YuvFormat::Yuv420);
let encoder = AvifEncoder::new(AvifConfig::default());
let bytes = encoder.encode(&image).expect("encode failed");
assert!(
find_top_level_box(&bytes, b"meta").is_some(),
"meta box missing"
);
assert!(
find_top_level_box(&bytes, b"mdat").is_some(),
"mdat box missing"
);
}
#[test]
fn test_probe_roundtrip_dimensions() {
let image = make_test_image(320, 240, 8, YuvFormat::Yuv420);
let encoder = AvifEncoder::new(AvifConfig::default());
let bytes = encoder.encode(&image).expect("encode failed");
let probe = AvifDecoder::probe(&bytes).expect("probe failed");
assert_eq!(probe.width, 320, "probed width mismatch");
assert_eq!(probe.height, 240, "probed height mismatch");
assert_eq!(probe.bit_depth, 8, "probed bit_depth mismatch");
assert!(!probe.has_alpha, "should not have alpha");
}
#[test]
fn test_probe_10bit() {
let image = make_test_image(160, 120, 10, YuvFormat::Yuv420);
let encoder = AvifEncoder::new(AvifConfig::default());
let bytes = encoder.encode(&image).expect("encode 10-bit failed");
let probe = AvifDecoder::probe(&bytes).expect("probe 10-bit failed");
assert_eq!(probe.bit_depth, 10);
}
#[test]
fn test_probe_12bit() {
let image = make_test_image(64, 64, 12, YuvFormat::Yuv420);
let encoder = AvifEncoder::new(AvifConfig::default());
let bytes = encoder.encode(&image).expect("encode 12-bit failed");
let probe = AvifDecoder::probe(&bytes).expect("probe 12-bit failed");
assert_eq!(probe.bit_depth, 12);
}
#[test]
fn test_decode_roundtrip_color_payload() {
let image = make_test_image(64, 48, 8, YuvFormat::Yuv420);
let encoder = AvifEncoder::new(AvifConfig::default());
let bytes = encoder.encode(&image).expect("encode failed");
let decoded = AvifDecoder::decode(&bytes).expect("decode failed");
assert_eq!(decoded.width, 64);
assert_eq!(decoded.height, 48);
assert!(
!decoded.y_plane.is_empty(),
"decoded y_plane (AV1 OBU) should not be empty"
);
}
#[test]
fn test_encode_with_alpha() {
let mut image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
image.alpha_plane = Some(vec![255u8; 64 * 64]);
let config = AvifConfig {
alpha_quality: Some(80),
..AvifConfig::default()
};
let encoder = AvifEncoder::new(config);
let bytes = encoder.encode(&image).expect("encode with alpha failed");
let probe = AvifDecoder::probe(&bytes).expect("probe with alpha failed");
assert!(probe.has_alpha, "probe should detect alpha");
}
#[test]
fn test_decode_with_alpha() {
let mut image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
image.alpha_plane = Some(vec![200u8; 64 * 64]);
let config = AvifConfig {
alpha_quality: Some(90),
..AvifConfig::default()
};
let encoder = AvifEncoder::new(config);
let bytes = encoder.encode(&image).expect("encode failed");
let decoded = AvifDecoder::decode(&bytes).expect("decode failed");
assert!(
decoded.alpha_plane.is_some(),
"decoded image should have alpha"
);
assert!(!decoded
.alpha_plane
.expect("alpha plane should exist")
.is_empty());
}
#[test]
fn test_invalid_signature_rejected() {
let garbage = b"not an avif file at all".to_vec();
assert!(
AvifDecoder::probe(&garbage).is_err(),
"garbage input must be rejected"
);
}
#[test]
fn test_zero_dimension_rejected() {
let image = AvifImage {
width: 0,
height: 100,
depth: 8,
yuv_format: YuvFormat::Yuv420,
y_plane: vec![0u8; 100],
u_plane: vec![],
v_plane: vec![],
alpha_plane: None,
};
let encoder = AvifEncoder::new(AvifConfig::default());
assert!(encoder.encode(&image).is_err());
}
#[test]
fn test_invalid_bit_depth_rejected() {
let image = AvifImage {
width: 8,
height: 8,
depth: 9, yuv_format: YuvFormat::Yuv420,
y_plane: vec![0u8; 64],
u_plane: vec![0u8; 16],
v_plane: vec![0u8; 16],
alpha_plane: None,
};
let encoder = AvifEncoder::new(AvifConfig::default());
assert!(encoder.encode(&image).is_err());
}
#[test]
fn test_colr_box_written() {
let image = make_test_image(32, 32, 8, YuvFormat::Yuv420);
let config = AvifConfig {
color_primaries: 9,
transfer_characteristics: 16,
matrix_coefficients: 9,
..AvifConfig::default()
};
let encoder = AvifEncoder::new(config);
let bytes = encoder.encode(&image).expect("encode failed");
let probe = AvifDecoder::probe(&bytes).expect("probe failed");
assert_eq!(probe.color_primaries, 9);
assert_eq!(probe.transfer_characteristics, 16);
}
#[test]
fn test_yuv444_encode() {
let image = make_test_image(64, 64, 8, YuvFormat::Yuv444);
let encoder = AvifEncoder::new(AvifConfig::default());
let bytes = encoder.encode(&image).expect("yuv444 encode failed");
let probe = AvifDecoder::probe(&bytes).expect("yuv444 probe failed");
assert_eq!(probe.width, 64);
assert_eq!(probe.height, 64);
}
#[test]
fn test_leb128_encoding() {
let mut buf = Vec::new();
write_leb128(&mut buf, 0);
assert_eq!(buf, &[0x00]);
buf.clear();
write_leb128(&mut buf, 127);
assert_eq!(buf, &[0x7F]);
buf.clear();
write_leb128(&mut buf, 128);
assert_eq!(buf, &[0x80, 0x01]);
buf.clear();
write_leb128(&mut buf, 300);
assert_eq!(buf, &[0xAC, 0x02]);
}
#[test]
fn test_bit_writer() {
let mut bw = BitWriter::new();
bw.write_bits(0b10110011, 8);
let out = bw.finish();
assert_eq!(out, &[0b10110011]);
let mut bw = BitWriter::new();
bw.write_bits(1, 1);
bw.write_bits(0, 1);
bw.write_bits(1, 1);
bw.write_bits(0, 4);
bw.write_bits(1, 1);
let out = bw.finish();
assert_eq!(out, &[0b10100001]);
}
#[test]
fn test_av1c_box_structure() {
let image = make_test_image(64, 64, 8, YuvFormat::Yuv420);
let config = AvifConfig::default();
let av1c = build_av1c(&image, &config);
assert_eq!(av1c.len(), 12, "av1C box must be 12 bytes");
assert_eq!(&av1c[4..8], b"av1C");
assert_eq!(av1c[8], 0x81, "marker+version byte must be 0x81");
}
#[test]
fn test_ispe_box_structure() {
let ispe = build_ispe(1920, 1080);
assert_eq!(ispe.len(), 20);
assert_eq!(&ispe[4..8], b"ispe");
let w = u32::from_be_bytes(ispe[12..16].try_into().expect("4-byte slice for width"));
let h = u32::from_be_bytes(ispe[16..20].try_into().expect("4-byte slice for height"));
assert_eq!(w, 1920);
assert_eq!(h, 1080);
}
#[test]
fn test_large_image_encode() {
let image = make_test_image(3840, 2160, 8, YuvFormat::Yuv420);
let encoder = AvifEncoder::new(AvifConfig::default());
let bytes = encoder.encode(&image).expect("4K encode failed");
let probe = AvifDecoder::probe(&bytes).expect("4K probe failed");
assert_eq!(probe.width, 3840);
assert_eq!(probe.height, 2160);
}
}