use crate::boxes::{FourCc, Reader};
use otf_pixels_core::{Orientation, PixelsError, Result};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Subsampling {
Monochrome,
Yuv420,
Yuv422,
Yuv444,
}
impl Subsampling {
#[must_use]
pub const fn x_shift(self) -> u32 {
match self {
Self::Yuv420 | Self::Yuv422 => 1,
Self::Monochrome | Self::Yuv444 => 0,
}
}
#[must_use]
pub const fn y_shift(self) -> u32 {
match self {
Self::Yuv420 => 1,
Self::Monochrome | Self::Yuv422 | Self::Yuv444 => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Av1Config {
pub seq_profile: u8,
pub seq_level_idx0: u8,
pub seq_tier0: u8,
pub bit_depth: u8,
pub subsampling: Subsampling,
pub chroma_sample_position: u8,
pub config_obus: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Extents {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PixelInfo {
pub bits_per_channel: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Colour {
Nclx {
primaries: u16,
transfer: u16,
matrix: u16,
full_range: bool,
},
Icc(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Property {
Extents(Extents),
Av1Config(Av1Config),
PixelInfo(PixelInfo),
Colour(Colour),
Rotation(u8),
Mirror(bool),
AuxiliaryType(String),
PixelAspect(u32, u32),
Unknown(FourCc),
}
impl Property {
#[must_use]
pub const fn kind(&self) -> FourCc {
match self {
Self::Extents(_) => FourCc::new(b"ispe"),
Self::Av1Config(_) => FourCc::new(b"av1C"),
Self::PixelInfo(_) => FourCc::new(b"pixi"),
Self::Colour(_) => FourCc::new(b"colr"),
Self::Rotation(_) => FourCc::new(b"irot"),
Self::Mirror(_) => FourCc::new(b"imir"),
Self::AuxiliaryType(_) => FourCc::new(b"auxC"),
Self::PixelAspect(_, _) => FourCc::new(b"pasp"),
Self::Unknown(kind) => *kind,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Association {
pub index: u16,
pub essential: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Properties {
entries: Vec<Property>,
associations: HashMap<u32, Vec<Association>>,
}
impl Properties {
pub fn parse(mut iprp: Reader<'_>) -> Result<Self> {
let mut entries = Vec::new();
let mut associations = HashMap::new();
while let Some(header) = iprp.next_box() {
let header = header?;
let payload = iprp.payload(&header);
match &header.kind.0 {
b"ipco" => entries = parse_ipco(payload)?,
b"ipma" => parse_ipma(payload, &mut associations)?,
_ => {}
}
}
Ok(Self {
entries,
associations,
})
}
#[must_use]
pub fn for_item(&self, item_id: u32) -> Vec<&Property> {
let Some(associations) = self.associations.get(&item_id) else {
return Vec::new();
};
associations
.iter()
.filter_map(|association| {
let index = usize::from(association.index).checked_sub(1)?;
self.entries.get(index)
})
.collect()
}
#[must_use]
pub fn essential_unknown(&self, item_id: u32) -> Option<FourCc> {
let associations = self.associations.get(&item_id)?;
associations.iter().find_map(|association| {
if !association.essential {
return None;
}
let index = usize::from(association.index).checked_sub(1)?;
match self.entries.get(index) {
Some(Property::Unknown(kind)) => Some(*kind),
_ => None,
}
})
}
#[must_use]
pub fn extents(&self, item_id: u32) -> Option<Extents> {
self.for_item(item_id)
.into_iter()
.find_map(|property| match property {
Property::Extents(extents) => Some(*extents),
_ => None,
})
}
#[must_use]
pub fn av1_config(&self, item_id: u32) -> Option<&Av1Config> {
self.for_item(item_id)
.into_iter()
.find_map(|property| match property {
Property::Av1Config(config) => Some(config),
_ => None,
})
}
#[must_use]
pub fn colour(&self, item_id: u32) -> Option<&Colour> {
let colours = || {
self.for_item(item_id)
.into_iter()
.filter_map(|property| match property {
Property::Colour(colour) => Some(colour),
_ => None,
})
};
colours()
.find(|colour| matches!(colour, Colour::Nclx { .. }))
.or_else(|| colours().next())
}
#[must_use]
pub fn icc_profile(&self, item_id: u32) -> Option<&[u8]> {
self.for_item(item_id)
.into_iter()
.find_map(|property| match property {
Property::Colour(Colour::Icc(profile)) => Some(profile.as_slice()),
_ => None,
})
}
#[must_use]
pub fn auxiliary_type(&self, item_id: u32) -> Option<&str> {
self.for_item(item_id)
.into_iter()
.find_map(|property| match property {
Property::AuxiliaryType(urn) => Some(urn.as_str()),
_ => None,
})
}
#[must_use]
pub fn orientation(&self, item_id: u32) -> Orientation {
let (mut turns, mut mirrored) = (0_u8, false);
for property in self.for_item(item_id) {
match property {
Property::Rotation(anticlockwise) => {
let clockwise = (4 - anticlockwise % 4) % 4;
turns = if mirrored {
(turns + 4 - clockwise) % 4
} else {
(turns + clockwise) % 4
};
}
Property::Mirror(left_right) => {
if !left_right {
turns = (turns + 2) % 4;
}
mirrored = !mirrored;
}
_ => {}
}
}
Orientation::from_parts(turns, mirrored)
}
}
fn parse_ipco(mut ipco: Reader<'_>) -> Result<Vec<Property>> {
let mut entries = Vec::new();
while let Some(header) = ipco.next_box() {
let header = header?;
let payload = ipco.payload(&header);
entries.push(parse_property(header.kind, payload)?);
}
Ok(entries)
}
fn parse_property(kind: FourCc, mut payload: Reader<'_>) -> Result<Property> {
match &kind.0 {
b"ispe" => {
let (_version, _flags) = payload.full_box()?;
let width = payload.u32()?;
let height = payload.u32()?;
Ok(Property::Extents(Extents { width, height }))
}
b"av1C" => parse_av1c(payload).map(Property::Av1Config),
b"pixi" => {
let (_version, _flags) = payload.full_box()?;
let count = payload.u8()?;
let mut bits_per_channel = Vec::with_capacity(usize::from(count));
for _ in 0..count {
bits_per_channel.push(payload.u8()?);
}
Ok(Property::PixelInfo(PixelInfo { bits_per_channel }))
}
b"colr" => parse_colr(payload).map(Property::Colour),
b"irot" => {
let turns = payload.u8()? & 0x03;
Ok(Property::Rotation(turns))
}
b"imir" => {
let mode = payload.u8()? & 0x01;
Ok(Property::Mirror(mode == 1))
}
b"auxC" => {
let (_version, _flags) = payload.full_box()?;
let urn = payload.cstring()?;
Ok(Property::AuxiliaryType(urn.to_owned()))
}
b"pasp" => {
let horizontal = payload.u32()?;
let vertical = payload.u32()?;
Ok(Property::PixelAspect(horizontal, vertical))
}
_ => Ok(Property::Unknown(kind)),
}
}
fn parse_av1c(mut payload: Reader<'_>) -> Result<Av1Config> {
let first = payload.u8()?;
if first >> 7 != 1 {
return Err(PixelsError::malformed(
"avif",
"the av1C marker bit is not set",
));
}
let version = first & 0x7f;
if version != 1 {
return Err(PixelsError::malformed(
"avif",
format!("av1C record version {version} is not 1"),
));
}
let second = payload.u8()?;
let seq_profile = second >> 5;
let seq_level_idx0 = second & 0x1f;
let third = payload.u8()?;
let seq_tier0 = (third >> 7) & 1;
let high_bitdepth = (third >> 6) & 1;
let twelve_bit = (third >> 5) & 1;
let monochrome = (third >> 4) & 1;
let subsampling_x = (third >> 3) & 1;
let subsampling_y = (third >> 2) & 1;
let chroma_sample_position = third & 0x03;
let _fourth = payload.u8()?;
let bit_depth = if seq_profile == 2 && high_bitdepth == 1 {
if twelve_bit == 1 { 12 } else { 10 }
} else if high_bitdepth == 1 {
10
} else {
8
};
let subsampling = match (monochrome, subsampling_x, subsampling_y) {
(1, _, _) => Subsampling::Monochrome,
(_, 1, 1) => Subsampling::Yuv420,
(_, 1, 0) => Subsampling::Yuv422,
(_, 0, 0) => Subsampling::Yuv444,
(_, x, y) => {
return Err(PixelsError::malformed(
"avif",
format!(
"av1C declares chroma subsampling ({x}, {y}), which AV1 has no such format for"
),
));
}
};
Ok(Av1Config {
seq_profile,
seq_level_idx0,
seq_tier0,
bit_depth,
subsampling,
chroma_sample_position,
config_obus: payload.rest().to_vec(),
})
}
fn parse_colr(mut payload: Reader<'_>) -> Result<Colour> {
let kind = payload.fourcc()?;
match &kind.0 {
b"nclx" => {
let primaries = payload.u16()?;
let transfer = payload.u16()?;
let matrix = payload.u16()?;
let full_range = payload.u8()? >> 7 == 1;
Ok(Colour::Nclx {
primaries,
transfer,
matrix,
full_range,
})
}
b"rICC" | b"prof" => Ok(Colour::Icc(payload.rest().to_vec())),
other => Err(PixelsError::malformed(
"avif",
format!(
"colr declares colour type '{}', which is not one this format defines",
FourCc(*other)
),
)),
}
}
fn parse_ipma(mut payload: Reader<'_>, out: &mut HashMap<u32, Vec<Association>>) -> Result<()> {
let (version, flags) = payload.full_box()?;
let entry_count = payload.u32()?;
let smallest_entry = if version < 1 { 3 } else { 5 };
if u64::from(entry_count) * smallest_entry > payload.remaining() as u64 {
return Err(PixelsError::malformed(
"avif",
format!(
"ipma declares {entry_count} entries, more than its {} remaining bytes can hold",
payload.remaining()
),
));
}
let wide_indices = flags & 1 == 1;
for _ in 0..entry_count {
let item_id = if version < 1 {
u32::from(payload.u16()?)
} else {
payload.u32()?
};
let association_count = payload.u8()?;
let mut associations = Vec::with_capacity(usize::from(association_count));
for _ in 0..association_count {
let (essential, index) = if wide_indices {
let word = payload.u16()?;
(word >> 15 == 1, word & 0x7fff)
} else {
let byte = payload.u8()?;
(byte >> 7 == 1, u16::from(byte & 0x7f))
};
associations.push(Association { index, essential });
}
out.entry(item_id).or_default().extend(associations);
}
Ok(())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::panic,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
use otf_pixels_core::ErrorCode;
fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let total = u32::try_from(8 + payload.len()).unwrap();
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(payload);
out
}
fn ispe(width: u32, height: u32) -> Vec<u8> {
let mut payload = vec![0, 0, 0, 0];
payload.extend_from_slice(&width.to_be_bytes());
payload.extend_from_slice(&height.to_be_bytes());
boxed(b"ispe", &payload)
}
fn av1c_420_8bit() -> Vec<u8> {
boxed(b"av1C", &[0x81, 0x00, 0x0C, 0x00, 0xAA])
}
fn parse_one(kind: &[u8; 4], payload: &[u8]) -> Result<Property> {
let file = boxed(kind, payload);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
parse_property(header.kind, reader.payload(&header))
}
#[test]
fn av1c_decodes_profile_depth_and_subsampling() {
let file = av1c_420_8bit();
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let Property::Av1Config(config) =
parse_property(header.kind, reader.payload(&header)).unwrap()
else {
panic!("expected an av1C");
};
assert_eq!(config.seq_profile, 0);
assert_eq!(config.bit_depth, 8);
assert_eq!(config.subsampling, Subsampling::Yuv420);
assert_eq!(config.config_obus, vec![0xAA]);
}
#[test]
fn av1c_twelve_bit_requires_profile_two() {
let file = boxed(b"av1C", &[0x81, 0x00, 0x6C, 0x00]);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let Property::Av1Config(config) =
parse_property(header.kind, reader.payload(&header)).unwrap()
else {
panic!("expected an av1C");
};
assert_eq!(config.bit_depth, 10);
let file = boxed(b"av1C", &[0x81, 0x40, 0x6C, 0x00]);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let Property::Av1Config(config) =
parse_property(header.kind, reader.payload(&header)).unwrap()
else {
panic!("expected an av1C");
};
assert_eq!(config.seq_profile, 2);
assert_eq!(config.bit_depth, 12);
}
#[test]
fn av1c_rejects_a_bad_marker_or_version() {
let error = parse_one(b"av1C", &[0x01, 0x00, 0x0C, 0x00]).unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
assert!(error.to_string().contains("marker"), "{error}");
let error = parse_one(b"av1C", &[0x82, 0x00, 0x0C, 0x00]).unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
assert!(error.to_string().contains("version 2"), "{error}");
}
#[test]
fn av1c_rejects_a_subsampling_av1_does_not_define() {
let error = parse_one(b"av1C", &[0x81, 0x00, 0x04, 0x00]).unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
assert!(error.to_string().contains("subsampling"), "{error}");
}
#[test]
fn monochrome_wins_over_the_subsampling_bits() {
let file = boxed(b"av1C", &[0x81, 0x00, 0x1C, 0x00]);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let Property::Av1Config(config) =
parse_property(header.kind, reader.payload(&header)).unwrap()
else {
panic!("expected an av1C");
};
assert_eq!(config.subsampling, Subsampling::Monochrome);
assert_eq!(config.subsampling.x_shift(), 0);
assert_eq!(config.subsampling.y_shift(), 0);
}
#[test]
fn colr_reads_nclx_code_points_and_the_range_flag() {
let mut payload = Vec::from(*b"nclx");
payload.extend_from_slice(&1_u16.to_be_bytes());
payload.extend_from_slice(&13_u16.to_be_bytes());
payload.extend_from_slice(&6_u16.to_be_bytes());
payload.push(0x80);
let Property::Colour(Colour::Nclx {
primaries,
transfer,
matrix,
full_range,
}) = parse_one(b"colr", &payload).unwrap()
else {
panic!("expected an nclx colr");
};
assert_eq!((primaries, transfer, matrix), (1, 13, 6));
assert!(full_range);
}
#[test]
fn colr_carries_an_icc_profile_through() {
let mut payload = Vec::from(*b"prof");
payload.extend_from_slice(&[1, 2, 3, 4]);
let Property::Colour(Colour::Icc(profile)) = parse_one(b"colr", &payload).unwrap() else {
panic!("expected an ICC colr");
};
assert_eq!(profile, vec![1, 2, 3, 4]);
}
fn item_with(properties: Vec<Property>) -> Properties {
let associations = (1..=properties.len())
.map(|index| Association {
index: u16::try_from(index).unwrap(),
essential: true,
})
.collect();
Properties {
entries: properties,
associations: HashMap::from([(1, associations)]),
}
}
#[test]
fn irot_and_imir_compose_in_association_order() {
use Property::{Mirror, Rotation};
let cases = [
(vec![], Orientation::Normal),
(vec![Rotation(1)], Orientation::Rotate270),
(vec![Rotation(2)], Orientation::Rotate180),
(vec![Rotation(3)], Orientation::Rotate90),
(vec![Mirror(false)], Orientation::FlipVertical),
(vec![Mirror(true)], Orientation::FlipHorizontal),
(vec![Rotation(3), Mirror(true)], Orientation::Transpose),
(vec![Rotation(1), Mirror(true)], Orientation::Transverse),
(vec![Rotation(1), Mirror(false)], Orientation::Transpose),
(vec![Mirror(true), Rotation(1)], Orientation::Transpose),
];
for (properties, expected) in cases {
assert_eq!(
item_with(properties.clone()).orientation(1),
expected,
"{properties:?}"
);
}
assert_eq!(Properties::default().orientation(7), Orientation::Normal);
}
#[test]
fn orientation_properties_are_masked_to_their_defined_bits() {
assert_eq!(parse_one(b"irot", &[0xFE]).unwrap(), Property::Rotation(2));
assert_eq!(
parse_one(b"imir", &[0xFE]).unwrap(),
Property::Mirror(false)
);
assert_eq!(parse_one(b"imir", &[0xFF]).unwrap(), Property::Mirror(true));
}
#[test]
fn pixi_reads_one_depth_per_channel() {
let Property::PixelInfo(info) = parse_one(b"pixi", &[0, 0, 0, 0, 3, 8, 8, 8]).unwrap()
else {
panic!("expected a pixi");
};
assert_eq!(info.bits_per_channel, vec![8, 8, 8]);
}
#[test]
fn an_uninterpreted_property_is_retained_by_type() {
assert_eq!(
parse_one(b"clap", &[0; 32]).unwrap(),
Property::Unknown(FourCc::new(b"clap"))
);
}
fn sample_iprp() -> Vec<u8> {
let mut ipco = ispe(64, 48);
ipco.extend_from_slice(&av1c_420_8bit());
let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2, 0x01, 0x82];
let mut iprp = boxed(b"ipco", &ipco);
iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
boxed(b"iprp", &iprp)
}
#[test]
fn properties_resolve_through_the_association_map() {
let file = sample_iprp();
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let properties = Properties::parse(reader.payload(&header)).unwrap();
assert_eq!(
properties.extents(1),
Some(Extents {
width: 64,
height: 48
})
);
assert_eq!(properties.av1_config(1).unwrap().bit_depth, 8);
assert!(properties.extents(2).is_none());
assert!(properties.for_item(2).is_empty());
}
#[test]
fn an_out_of_range_association_index_is_skipped_not_indexed() {
let mut ipco = ispe(8, 8);
ipco.extend_from_slice(&av1c_420_8bit());
let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2, 99, 0];
let mut iprp = boxed(b"ipco", &ipco);
iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
let file = boxed(b"iprp", &iprp);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let properties = Properties::parse(reader.payload(&header)).unwrap();
assert!(properties.for_item(1).is_empty());
}
#[test]
fn an_essential_property_we_do_not_understand_is_reported() {
let mut ipco = ispe(8, 8);
ipco.extend_from_slice(&boxed(b"zzzz", &[0; 4]));
let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0x82];
let mut iprp = boxed(b"ipco", &ipco);
iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
let file = boxed(b"iprp", &iprp);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let properties = Properties::parse(reader.payload(&header)).unwrap();
assert_eq!(
properties.essential_unknown(1),
Some(FourCc::new(b"zzzz")),
"an essential unknown property must not be silently ignored"
);
let ipma_payload = vec![0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0x02];
let mut iprp = boxed(b"ipco", &ipco);
iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
let file = boxed(b"iprp", &iprp);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let properties = Properties::parse(reader.payload(&header)).unwrap();
assert_eq!(properties.essential_unknown(1), None);
}
#[test]
fn ipma_supports_wide_indices_and_wide_item_ids() {
let mut ipco = ispe(16, 16);
ipco.extend_from_slice(&av1c_420_8bit());
let mut ipma_payload = vec![1, 0, 0, 1];
ipma_payload.extend_from_slice(&1_u32.to_be_bytes()); ipma_payload.extend_from_slice(&70_000_u32.to_be_bytes()); ipma_payload.push(1); ipma_payload.extend_from_slice(&0x0001_u16.to_be_bytes());
let mut iprp = boxed(b"ipco", &ipco);
iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
let file = boxed(b"iprp", &iprp);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let properties = Properties::parse(reader.payload(&header)).unwrap();
assert_eq!(
properties.extents(70_000),
Some(Extents {
width: 16,
height: 16
})
);
}
#[test]
fn ipma_rejects_more_entries_than_the_box_can_hold() {
let ipma_payload = vec![0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, 0, 1, 0];
let mut iprp = boxed(b"ipco", &ispe(8, 8));
iprp.extend_from_slice(&boxed(b"ipma", &ipma_payload));
let file = boxed(b"iprp", &iprp);
let mut reader = Reader::new(&file);
let header = reader.next_box().unwrap().unwrap();
let error = Properties::parse(reader.payload(&header)).unwrap_err();
assert_eq!(error.code(), ErrorCode::Malformed);
assert!(error.to_string().contains("more than its"), "{error}");
}
#[test]
fn auxc_reads_the_alpha_urn() {
let mut payload = vec![0, 0, 0, 0];
payload.extend_from_slice(b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0");
assert_eq!(
parse_one(b"auxC", &payload).unwrap(),
Property::AuxiliaryType("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha".to_owned())
);
}
}