use oxideav_heif::derived::{GridDescriptor, OverlayDescriptor};
use oxideav_heif::props::{self as hprops, Property as HProp};
use oxideav_heif::{Av1Config, HeifWriter, RawProperty};
use crate::error::{AvifError as Error, Result};
use crate::meta::{Amve, Clap, Clli, Colr, Imir, Irot, Mdcv, Pasp};
pub(crate) type Prop = (HProp, bool);
pub(crate) fn prop_av1c(av1c: &[u8], what: &str) -> Result<Prop> {
if av1c.len() < 4 {
return Err(Error::invalid(format!(
"avif mux: {what}av1C configuration record must be at least 4 bytes"
)));
}
Ok((HProp::Av1C(Av1Config::parse(av1c)?), true))
}
pub(crate) fn prop_ispe(width: u32, height: u32) -> Prop {
(HProp::Ispe(hprops::Ispe::new(width, height)), false)
}
pub(crate) fn prop_pixi(bits: &[u8]) -> Prop {
(HProp::Pixi(hprops::Pixi::new(bits.to_vec())), false)
}
pub(crate) fn prop_colr(colr: &Colr) -> Result<Prop> {
if let Colr::Unknown(t) = colr {
return Err(Error::unsupported(format!(
"avif mux: cannot emit colr of unknown type '{}'",
String::from_utf8_lossy(t)
)));
}
Ok((HProp::Colr(hprops::Colr::from(colr)), false))
}
fn prop_pasp(pasp: &Pasp) -> Prop {
(
HProp::Pasp(hprops::Pasp::new(pasp.h_spacing, pasp.v_spacing)),
false,
)
}
pub(crate) fn prop_clap(clap: &Clap) -> Prop {
(HProp::Clap(hprops::Clap::from(clap)), true)
}
fn prop_irot(irot: &Irot) -> Prop {
(HProp::Irot(hprops::Irot::new(irot.angle & 0x03)), true)
}
fn prop_imir(imir: &Imir) -> Prop {
(HProp::Imir(hprops::Imir::new(imir.axis & 0x01)), true)
}
fn prop_auxc(urn: &str) -> Prop {
(
HProp::AuxC(hprops::AuxC::new(urn.to_string(), Vec::new())),
false,
)
}
fn prop_a1lx(layer_size: [u32; 3]) -> Prop {
let large_size = layer_size.iter().any(|&v| v > u32::from(u16::MAX));
(
HProp::A1lx(hprops::A1lx::new(large_size, layer_size)),
false,
)
}
fn prop_lsel(layer_id: u16) -> Prop {
(HProp::Lsel(hprops::Lsel::new(layer_id)), true)
}
fn prop_a1op(op_index: u8) -> Prop {
(HProp::A1op(hprops::A1op::new(op_index)), true)
}
fn prop_mdcv(m: &Mdcv) -> Prop {
let mut body = Vec::with_capacity(24);
for (x, y) in m.display_primaries_xy {
body.extend_from_slice(&x.to_be_bytes());
body.extend_from_slice(&y.to_be_bytes());
}
body.extend_from_slice(&m.white_point_xy.0.to_be_bytes());
body.extend_from_slice(&m.white_point_xy.1.to_be_bytes());
body.extend_from_slice(&m.max_display_mastering_luminance.to_be_bytes());
body.extend_from_slice(&m.min_display_mastering_luminance.to_be_bytes());
(
HProp::Unknown(RawProperty::new(
*b"mdcv",
None,
body.clone(),
body.len() + 8,
)),
false,
)
}
fn prop_clli(c: &Clli) -> Prop {
(HProp::Clli(hprops::Clli::from(c)), false)
}
fn prop_amve(a: &Amve) -> Prop {
(HProp::Amve(hprops::Amve::from(a)), false)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProfileBrand {
Baseline,
Advanced,
Bare,
}
fn still_brands(profile: ProfileBrand) -> ([u8; 4], Vec<[u8; 4]>) {
let mut compat = vec![*b"avif", *b"mif1", *b"miaf"];
match profile {
ProfileBrand::Baseline => compat.push(*b"MA1B"),
ProfileBrand::Advanced => compat.push(*b"MA1A"),
ProfileBrand::Bare => {}
}
(*b"avif", compat)
}
fn add_exif_item(w: &mut HeifWriter, image: u32, payload: &[u8]) -> Result<u32> {
let Some((head, rest)) = payload.split_first_chunk::<4>() else {
return Err(Error::invalid(
"avif mux: Exif item body needs the 4-byte exif_tiff_header_offset word",
));
};
if u32::from_be_bytes(*head) as usize > rest.len() {
return Err(Error::invalid(
"avif mux: Exif exif_tiff_header_offset points past the payload (HEIF A.2.1)",
));
}
Ok(w.add_exif_raw(image, payload.to_vec()))
}
fn add_xmp_item(w: &mut HeifWriter, image: u32, payload: &[u8]) -> u32 {
w.add_xmp_bytes(image, payload.to_vec())
}
fn expect_id(got: u32, want: u32, what: &str) -> Result<()> {
if got != want {
return Err(Error::invalid(format!(
"avif mux: {what} got item id {got}, expected {want}"
)));
}
Ok(())
}
pub struct AvifMuxer {
width: u32,
height: u32,
primary_payload: Vec<u8>,
av1c: Vec<u8>,
pixi: Option<Vec<u8>>,
colr: Option<Colr>,
pasp: Option<Pasp>,
clap: Option<Clap>,
irot: Option<Irot>,
imir: Option<Imir>,
mdcv: Option<Mdcv>,
clli: Option<Clli>,
amve: Option<Amve>,
alpha: Option<AlphaImage>,
depth: Option<AuxCoded>,
exif: Option<Vec<u8>>,
xmp: Option<Vec<u8>>,
icc: Option<Vec<u8>>,
identity: Option<IdentityDerivation>,
layered_index: Option<[u32; 3]>,
layer_selector: Option<u16>,
operating_point: Option<u8>,
profile_brand: ProfileBrand,
item_name: Option<String>,
entity_groups: Vec<EntityGroupSpec>,
tone_map: Option<ToneMapItem>,
burst: Vec<BurstMember>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct BurstMember {
pub width: u32,
pub height: u32,
pub payload: Vec<u8>,
pub av1c: Vec<u8>,
pub pixi: Option<Vec<u8>>,
pub colr: Option<Colr>,
pub icc: Option<Vec<u8>>,
pub clap: Option<Clap>,
pub alpha: Option<BurstAlpha>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct BurstAlpha {
pub payload: Vec<u8>,
pub av1c: Vec<u8>,
pub pixi: Option<Vec<u8>>,
pub premultiplied: bool,
}
impl BurstAlpha {
pub fn new(
payload: Vec<u8>,
av1c: Vec<u8>,
pixi: Option<Vec<u8>>,
premultiplied: bool,
) -> Self {
Self {
payload,
av1c,
pixi,
premultiplied,
}
}
}
impl BurstMember {
pub fn new(width: u32, height: u32, payload: Vec<u8>, av1c: Vec<u8>) -> Self {
Self {
width,
height,
payload,
av1c,
pixi: None,
colr: None,
icc: None,
clap: None,
alpha: None,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct ToneMapItem {
pub payload: Vec<u8>,
pub av1c: Vec<u8>,
pub width: u32,
pub height: u32,
pub pixi: Option<Vec<u8>>,
pub colr: Colr,
pub metadata: oxideav_heif::gainmap::GainMapMetadata,
pub alternate_colr: Colr,
pub alternate_clli: Option<Clli>,
pub alternate_pixi: Option<Vec<u8>>,
}
impl ToneMapItem {
#[allow(clippy::too_many_arguments)]
pub fn new(
payload: Vec<u8>,
av1c: Vec<u8>,
width: u32,
height: u32,
pixi: Option<Vec<u8>>,
colr: Colr,
metadata: oxideav_heif::gainmap::GainMapMetadata,
alternate_colr: Colr,
alternate_clli: Option<Clli>,
alternate_pixi: Option<Vec<u8>>,
) -> Self {
Self {
payload,
av1c,
width,
height,
pixi,
colr,
metadata,
alternate_colr,
alternate_clli,
alternate_pixi,
}
}
pub fn with_payload(mut self, payload: Vec<u8>) -> Self {
self.payload = payload;
self
}
pub fn with_av1c(mut self, av1c: Vec<u8>) -> Self {
self.av1c = av1c;
self
}
pub fn with_width(mut self, width: u32) -> Self {
self.width = width;
self
}
pub fn with_height(mut self, height: u32) -> Self {
self.height = height;
self
}
pub fn with_pixi(mut self, pixi: Option<Vec<u8>>) -> Self {
self.pixi = pixi;
self
}
pub fn with_colr(mut self, colr: Colr) -> Self {
self.colr = colr;
self
}
pub fn with_metadata(mut self, metadata: oxideav_heif::gainmap::GainMapMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_alternate_colr(mut self, alternate_colr: Colr) -> Self {
self.alternate_colr = alternate_colr;
self
}
pub fn with_alternate_clli(mut self, alternate_clli: Option<Clli>) -> Self {
self.alternate_clli = alternate_clli;
self
}
pub fn with_alternate_pixi(mut self, alternate_pixi: Option<Vec<u8>>) -> Self {
self.alternate_pixi = alternate_pixi;
self
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EntityGroupSpec {
pub grouping_type: [u8; 4],
pub group_id: u32,
pub flags: u32,
pub entity_ids: Vec<u32>,
}
impl EntityGroupSpec {
#[allow(clippy::too_many_arguments)]
pub fn new(grouping_type: [u8; 4], group_id: u32, flags: u32, entity_ids: Vec<u32>) -> Self {
Self {
grouping_type,
group_id,
flags,
entity_ids,
}
}
pub fn with_grouping_type(mut self, grouping_type: [u8; 4]) -> Self {
self.grouping_type = grouping_type;
self
}
pub fn with_group_id(mut self, group_id: u32) -> Self {
self.group_id = group_id;
self
}
pub fn with_flags(mut self, flags: u32) -> Self {
self.flags = flags;
self
}
pub fn with_entity_ids(mut self, entity_ids: Vec<u32>) -> Self {
self.entity_ids = entity_ids;
self
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct IdentityDerivation {
pub clap: Option<Clap>,
pub irot: Option<u8>,
pub imir: Option<u8>,
}
impl IdentityDerivation {
#[allow(clippy::too_many_arguments)]
pub fn new(clap: Option<Clap>, irot: Option<u8>, imir: Option<u8>) -> Self {
Self { clap, irot, imir }
}
pub fn with_clap(mut self, clap: Option<Clap>) -> Self {
self.clap = clap;
self
}
pub fn with_irot(mut self, irot: Option<u8>) -> Self {
self.irot = irot;
self
}
pub fn with_imir(mut self, imir: Option<u8>) -> Self {
self.imir = imir;
self
}
}
struct AuxCoded {
payload: Vec<u8>,
av1c: Vec<u8>,
pixi: Option<Vec<u8>>,
}
struct AlphaImage {
payload: Vec<u8>,
av1c: Vec<u8>,
pixi: Option<Vec<u8>>,
premultiplied: bool,
}
impl AvifMuxer {
pub fn new(width: u32, height: u32, payload: Vec<u8>, av1c: Vec<u8>) -> Self {
Self {
width,
height,
primary_payload: payload,
av1c,
pixi: None,
colr: None,
pasp: None,
clap: None,
irot: None,
imir: None,
mdcv: None,
clli: None,
amve: None,
alpha: None,
depth: None,
exif: None,
icc: None,
xmp: None,
identity: None,
layered_index: None,
layer_selector: None,
operating_point: None,
profile_brand: ProfileBrand::Baseline,
item_name: None,
entity_groups: Vec::new(),
tone_map: None,
burst: Vec::new(),
}
}
pub fn with_burst_member(mut self, member: BurstMember) -> Self {
self.burst.push(member);
self
}
pub fn with_tone_map(mut self, tone_map: ToneMapItem) -> Self {
self.tone_map = Some(tone_map);
self
}
pub fn with_item_name(mut self, name: &str) -> Self {
self.item_name = Some(name.to_owned());
self
}
pub fn with_entity_group(mut self, group: EntityGroupSpec) -> Self {
self.entity_groups.push(group);
self
}
pub fn with_pixi(mut self, bits: Vec<u8>) -> Self {
self.pixi = Some(bits);
self
}
pub fn with_colr(mut self, colr: Colr) -> Self {
self.colr = Some(colr);
self
}
pub fn with_pasp(mut self, pasp: Pasp) -> Self {
self.pasp = Some(pasp);
self
}
pub fn with_clap(mut self, clap: Clap) -> Self {
self.clap = Some(clap);
self
}
pub fn with_irot(mut self, angle: u8) -> Self {
self.irot = Some(Irot {
angle: angle & 0x03,
});
self
}
pub fn with_imir(mut self, axis: u8) -> Self {
self.imir = Some(Imir { axis: axis & 0x01 });
self
}
pub fn with_mdcv(mut self, mdcv: Mdcv) -> Self {
self.mdcv = Some(mdcv);
self
}
pub fn with_clli(mut self, clli: Clli) -> Self {
self.clli = Some(clli);
self
}
pub fn with_amve(mut self, amve: Amve) -> Self {
self.amve = Some(amve);
self
}
pub fn with_exif(mut self, payload: Vec<u8>) -> Self {
self.exif = Some(payload);
self
}
pub fn with_xmp(mut self, payload: Vec<u8>) -> Self {
self.xmp = Some(payload);
self
}
pub fn with_icc(mut self, profile: Vec<u8>) -> Self {
self.icc = Some(profile);
self
}
pub fn advanced_profile(mut self) -> Self {
self.profile_brand = ProfileBrand::Advanced;
self
}
pub fn no_profile_brand(mut self) -> Self {
self.profile_brand = ProfileBrand::Bare;
self
}
pub fn with_alpha(mut self, payload: Vec<u8>, av1c: Vec<u8>, premultiplied: bool) -> Self {
self.alpha = Some(AlphaImage {
payload,
av1c,
pixi: Some(vec![8]),
premultiplied,
});
self
}
pub fn with_alpha_pixi(mut self, bits: Vec<u8>) -> Self {
if let Some(a) = self.alpha.as_mut() {
a.pixi = Some(bits);
}
self
}
pub fn with_depth(mut self, payload: Vec<u8>, av1c: Vec<u8>) -> Self {
self.depth = Some(AuxCoded {
payload,
av1c,
pixi: Some(vec![8]),
});
self
}
pub fn with_layered_index(mut self, layer_size: [u32; 3]) -> Self {
self.layered_index = Some(layer_size);
self
}
pub fn with_layer_selector(mut self, layer_id: u16) -> Self {
self.layer_selector = Some(layer_id);
self
}
pub fn with_operating_point(mut self, op_index: u8) -> Self {
self.operating_point = Some(op_index);
self
}
pub fn with_identity_derivation(mut self, derivation: IdentityDerivation) -> Self {
self.identity = Some(derivation);
self
}
pub fn build(self) -> Result<Vec<u8>> {
let (major, compat) = still_brands(self.profile_brand);
let mut w = HeifWriter::new().with_brands(major, compat);
let mut props = vec![
prop_av1c(&self.av1c, "")?,
prop_ispe(self.width, self.height),
];
if let Some(op) = self.operating_point {
props.push(prop_a1op(op));
}
if let Some(layer) = self.layer_selector {
props.push(prop_lsel(layer));
}
if let Some(sizes) = self.layered_index {
props.push(prop_a1lx(sizes));
}
if let Some(bits) = &self.pixi {
props.push(prop_pixi(bits));
}
if let Some(colr) = &self.colr {
props.push(prop_colr(colr)?);
}
if let Some(icc) = &self.icc {
props.push(prop_colr(&Colr::Icc(icc.clone()))?);
}
if let Some(pasp) = &self.pasp {
props.push(prop_pasp(pasp));
}
if let Some(mdcv) = &self.mdcv {
props.push(prop_mdcv(mdcv));
}
if let Some(clli) = &self.clli {
props.push(prop_clli(clli));
}
if let Some(amve) = &self.amve {
props.push(prop_amve(amve));
}
if let Some(clap) = &self.clap {
props.push(prop_clap(clap));
}
if let Some(irot) = &self.irot {
props.push(prop_irot(irot));
}
if let Some(imir) = &self.imir {
props.push(prop_imir(imir));
}
let coded = w.add_coded_item(*b"av01", self.primary_payload, props);
expect_id(coded, 1, "primary")?;
let mut primary_id = coded;
if let Some(iden) = self.identity {
let (mut iw, mut ih) = (self.width, self.height);
if let Some(clap) = &self.clap {
(iw, ih) = crate::derived::DimTransform::Crop {
width_n: clap.clean_aperture_width_n,
width_d: clap.clean_aperture_width_d,
height_n: clap.clean_aperture_height_n,
height_d: clap.clean_aperture_height_d,
}
.apply_dims(iw, ih);
}
if let Some(irot) = &self.irot {
(iw, ih) =
crate::derived::DimTransform::Rotate { angle: irot.angle }.apply_dims(iw, ih);
}
let mut iprops = vec![prop_ispe(iw, ih)];
if let Some(bits) = &self.pixi {
iprops.push(prop_pixi(bits));
}
if let Some(clap) = &iden.clap {
iprops.push(prop_clap(clap));
}
if let Some(angle) = iden.irot {
iprops.push(prop_irot(&Irot {
angle: angle & 0x03,
}));
}
if let Some(axis) = iden.imir {
iprops.push(prop_imir(&Imir { axis: axis & 0x01 }));
}
primary_id = w.add_identity(coded, iprops);
}
if let Some(alpha) = self.alpha {
let mut aprops = vec![
prop_av1c(&alpha.av1c, "alpha ")?,
prop_ispe(self.width, self.height),
prop_auxc(crate::alpha::ALPHA_URN_PREFIX),
];
if let Some(bits) = &alpha.pixi {
aprops.push(prop_pixi(bits));
}
let id = w.add_alpha(coded, *b"av01", alpha.payload, aprops, alpha.premultiplied);
w.set_hidden(id, true);
}
if let Some(depth) = self.depth {
let mut dprops = vec![
prop_av1c(&depth.av1c, "depth ")?,
prop_ispe(self.width, self.height),
prop_auxc(crate::meta::AUX_URN_DEPTH_MPEG),
];
if let Some(bits) = &depth.pixi {
dprops.push(prop_pixi(bits));
}
let id = w.add_depth(coded, *b"av01", depth.payload, dprops);
w.set_hidden(id, true);
}
if let Some(exif) = &self.exif {
add_exif_item(&mut w, coded, exif)?;
}
if let Some(xmp) = &self.xmp {
add_xmp_item(&mut w, coded, xmp);
}
if let Some(name) = &self.item_name {
w.set_item_name(primary_id, name);
}
if let Some(tm) = self.tone_map {
let gain_colr = match &tm.colr {
Colr::Nclx {
matrix_coefficients,
full_range,
..
} => Colr::Nclx {
colour_primaries: 2,
transfer_characteristics: 2,
matrix_coefficients: *matrix_coefficients,
full_range: *full_range,
},
_ => {
return Err(Error::invalid(
"avif mux: the gain map's colr must be nclx (HEIF Amd 1 §6.6.2.4.1)",
))
}
};
let mut gprops = vec![
prop_av1c(&tm.av1c, "gain map ")?,
prop_ispe(tm.width, tm.height),
prop_colr(&gain_colr)?,
];
if let Some(bits) = &tm.pixi {
gprops.push(prop_pixi(bits));
}
let gain = w.add_coded_item(*b"av01", tm.payload, gprops);
let (alternate, _) = prop_colr(&tm.alternate_colr)?;
let mut tprops = Vec::new();
if let Some(clli) = &tm.alternate_clli {
tprops.push(prop_clli(clli));
}
if let Some(bits) = &tm.alternate_pixi {
tprops.push(prop_pixi(bits));
}
w.add_tone_map(primary_id, gain, &tm.metadata, alternate, tprops)?;
}
if !self.burst.is_empty() {
let mut members = vec![primary_id];
for m in self.burst {
let mut mprops = vec![prop_av1c(&m.av1c, "burst ")?, prop_ispe(m.width, m.height)];
if let Some(bits) = &m.pixi {
mprops.push(prop_pixi(bits));
}
if let Some(colr) = &m.colr {
mprops.push(prop_colr(colr)?);
}
if let Some(icc) = &m.icc {
mprops.push(prop_colr(&Colr::Icc(icc.clone()))?);
}
if let Some(clap) = &m.clap {
mprops.push(prop_clap(clap));
}
let id = w.add_coded_item(*b"av01", m.payload, mprops);
if let Some(a) = m.alpha {
let mut aprops = vec![
prop_av1c(&a.av1c, "burst alpha ")?,
prop_ispe(m.width, m.height),
prop_auxc(crate::alpha::ALPHA_URN_PREFIX),
];
if let Some(bits) = &a.pixi {
aprops.push(prop_pixi(bits));
}
let aid = w.add_alpha(id, *b"av01", a.payload, aprops, a.premultiplied);
w.set_hidden(aid, true);
}
members.push(id);
}
let group_id = 1000 + members.len() as u32;
w.add_entity_group(*b"brst", group_id, members);
}
for g in self.entity_groups {
w.add_entity_group_with_flags(g.grouping_type, g.group_id, g.flags, g.entity_ids);
}
w.set_primary(primary_id);
Ok(w.write_to_vec()?)
}
}
pub fn encode_still_av1(
width: u32,
height: u32,
payload: Vec<u8>,
av1c: Vec<u8>,
pixi_bits: Vec<u8>,
) -> Result<Vec<u8>> {
AvifMuxer::new(width, height, payload, av1c)
.with_pixi(pixi_bits)
.build()
}
#[non_exhaustive]
pub struct GridTile {
pub width: u32,
pub height: u32,
pub payload: Vec<u8>,
pub av1c: Vec<u8>,
}
impl GridTile {
#[allow(clippy::too_many_arguments)]
pub fn new(width: u32, height: u32, payload: Vec<u8>, av1c: Vec<u8>) -> Self {
Self {
width,
height,
payload,
av1c,
}
}
pub fn with_width(mut self, width: u32) -> Self {
self.width = width;
self
}
pub fn with_height(mut self, height: u32) -> Self {
self.height = height;
self
}
pub fn with_payload(mut self, payload: Vec<u8>) -> Self {
self.payload = payload;
self
}
pub fn with_av1c(mut self, av1c: Vec<u8>) -> Self {
self.av1c = av1c;
self
}
}
pub struct AvifGridMuxer {
rows: u16,
columns: u16,
output_width: u32,
output_height: u32,
tiles: Vec<GridTile>,
pixi: Option<Vec<u8>>,
colr: Option<Colr>,
pasp: Option<Pasp>,
irot: Option<Irot>,
imir: Option<Imir>,
mdcv: Option<Mdcv>,
clli: Option<Clli>,
amve: Option<Amve>,
exif: Option<Vec<u8>>,
xmp: Option<Vec<u8>>,
icc: Option<Vec<u8>>,
alpha_tiles: Vec<GridTile>,
alpha_pixi: Option<Vec<u8>>,
premultiplied: bool,
profile_brand: ProfileBrand,
}
impl AvifGridMuxer {
pub fn new(rows: u16, columns: u16, output_width: u32, output_height: u32) -> Self {
Self {
rows,
columns,
output_width,
output_height,
tiles: Vec::new(),
pixi: None,
colr: None,
pasp: None,
irot: None,
imir: None,
mdcv: None,
clli: None,
amve: None,
exif: None,
icc: None,
xmp: None,
alpha_tiles: Vec::new(),
alpha_pixi: None,
premultiplied: false,
profile_brand: ProfileBrand::Baseline,
}
}
pub fn tile(mut self, tile: GridTile) -> Self {
self.tiles.push(tile);
self
}
pub fn alpha_tile(mut self, tile: GridTile) -> Self {
self.alpha_tiles.push(tile);
self
}
pub fn with_alpha_pixi(mut self, bits: Vec<u8>) -> Self {
self.alpha_pixi = Some(bits);
self
}
pub fn premultiplied_alpha(mut self, premultiplied: bool) -> Self {
self.premultiplied = premultiplied;
self
}
pub fn with_pasp(mut self, pasp: Pasp) -> Self {
self.pasp = Some(pasp);
self
}
pub fn with_irot(mut self, angle: u8) -> Self {
self.irot = Some(Irot {
angle: angle & 0x03,
});
self
}
pub fn with_imir(mut self, axis: u8) -> Self {
self.imir = Some(Imir { axis: axis & 0x01 });
self
}
pub fn with_mdcv(mut self, mdcv: Mdcv) -> Self {
self.mdcv = Some(mdcv);
self
}
pub fn with_clli(mut self, clli: Clli) -> Self {
self.clli = Some(clli);
self
}
pub fn with_amve(mut self, amve: Amve) -> Self {
self.amve = Some(amve);
self
}
pub fn with_exif(mut self, payload: Vec<u8>) -> Self {
self.exif = Some(payload);
self
}
pub fn with_xmp(mut self, payload: Vec<u8>) -> Self {
self.xmp = Some(payload);
self
}
pub fn with_icc(mut self, profile: Vec<u8>) -> Self {
self.icc = Some(profile);
self
}
pub fn with_pixi(mut self, bits: Vec<u8>) -> Self {
self.pixi = Some(bits);
self
}
pub fn with_colr(mut self, colr: Colr) -> Self {
self.colr = Some(colr);
self
}
pub fn advanced_profile(mut self) -> Self {
self.profile_brand = ProfileBrand::Advanced;
self
}
pub fn no_profile_brand(mut self) -> Self {
self.profile_brand = ProfileBrand::Bare;
self
}
pub fn build(self) -> Result<Vec<u8>> {
let expected = self.rows as usize * self.columns as usize;
if self.tiles.len() != expected {
return Err(Error::invalid(format!(
"avif mux: {}×{} grid needs {expected} tiles, got {}",
self.rows,
self.columns,
self.tiles.len()
)));
}
if self.tiles.is_empty() {
return Err(Error::invalid("avif mux: grid needs at least one tile"));
}
if !self.alpha_tiles.is_empty() && self.alpha_tiles.len() != expected {
return Err(Error::invalid(format!(
"avif mux: {}×{} alpha grid needs {expected} tiles, got {}",
self.rows,
self.columns,
self.alpha_tiles.len()
)));
}
let (major, compat) = still_brands(self.profile_brand);
let mut w = HeifWriter::new().with_brands(major, compat);
let desc = GridDescriptor::new(
self.rows,
self.columns,
self.output_width,
self.output_height,
);
let mut grid_props = vec![prop_ispe(self.output_width, self.output_height)];
if let Some(bits) = &self.pixi {
grid_props.push(prop_pixi(bits));
}
if let Some(colr) = &self.colr {
grid_props.push(prop_colr(colr)?);
}
if let Some(icc) = &self.icc {
grid_props.push(prop_colr(&Colr::Icc(icc.clone()))?);
}
if let Some(pasp) = &self.pasp {
grid_props.push(prop_pasp(pasp));
}
if let Some(mdcv) = &self.mdcv {
grid_props.push(prop_mdcv(mdcv));
}
if let Some(clli) = &self.clli {
grid_props.push(prop_clli(clli));
}
if let Some(amve) = &self.amve {
grid_props.push(prop_amve(amve));
}
if let Some(irot) = &self.irot {
grid_props.push(prop_irot(irot));
}
if let Some(imir) = &self.imir {
grid_props.push(prop_imir(imir));
}
let n = self.tiles.len() as u32;
let tile_ids: Vec<u32> = (2..2 + n).collect();
let grid_id = w.add_grid(desc, &tile_ids, grid_props)?;
expect_id(grid_id, 1, "grid")?;
for (i, tile) in self.tiles.into_iter().enumerate() {
let id = w.add_coded_item(
*b"av01",
tile.payload,
vec![
prop_av1c(&tile.av1c, "grid tile ")?,
prop_ispe(tile.width, tile.height),
],
);
expect_id(id, tile_ids[i], "grid tile")?;
w.set_hidden(id, true);
}
if !self.alpha_tiles.is_empty() {
let alpha_grid_id = 2 + n;
let alpha_tile_ids: Vec<u32> = (alpha_grid_id + 1..alpha_grid_id + 1 + n).collect();
let mut aprops = vec![
prop_ispe(self.output_width, self.output_height),
prop_auxc(crate::alpha::ALPHA_URN_PREFIX),
];
if let Some(bits) = &self.alpha_pixi {
aprops.push(prop_pixi(bits));
}
let ag = w.add_grid(desc, &alpha_tile_ids, aprops)?;
expect_id(ag, alpha_grid_id, "alpha grid")?;
w.set_hidden(ag, true);
for (i, tile) in self.alpha_tiles.into_iter().enumerate() {
let id = w.add_coded_item(
*b"av01",
tile.payload,
vec![
prop_av1c(&tile.av1c, &format!("alpha grid tile {i} "))?,
prop_ispe(tile.width, tile.height),
],
);
expect_id(id, alpha_tile_ids[i], "alpha grid tile")?;
w.set_hidden(id, true);
}
w.add_reference(*b"auxl", ag, vec![grid_id]);
if self.premultiplied {
w.add_reference(*b"prem", grid_id, vec![ag]);
}
}
if let Some(exif) = &self.exif {
add_exif_item(&mut w, grid_id, exif)?;
}
if let Some(xmp) = &self.xmp {
add_xmp_item(&mut w, grid_id, xmp);
}
w.set_primary(grid_id);
Ok(w.write_to_vec()?)
}
}
#[non_exhaustive]
pub struct OverlayLayer {
pub width: u32,
pub height: u32,
pub payload: Vec<u8>,
pub av1c: Vec<u8>,
pub pixi: Option<Vec<u8>>,
pub colr: Option<Colr>,
pub clap: Option<Clap>,
pub alpha: Option<(Vec<u8>, Vec<u8>)>,
pub alpha_pixi: Option<Vec<u8>>,
pub premultiplied: bool,
pub offset_x: i32,
pub offset_y: i32,
}
impl OverlayLayer {
#[allow(clippy::too_many_arguments)]
pub fn new(
width: u32,
height: u32,
payload: Vec<u8>,
av1c: Vec<u8>,
pixi: Option<Vec<u8>>,
colr: Option<Colr>,
clap: Option<Clap>,
alpha: Option<(Vec<u8>, Vec<u8>)>,
alpha_pixi: Option<Vec<u8>>,
premultiplied: bool,
offset_x: i32,
offset_y: i32,
) -> Self {
Self {
width,
height,
payload,
av1c,
pixi,
colr,
clap,
alpha,
alpha_pixi,
premultiplied,
offset_x,
offset_y,
}
}
pub fn with_width(mut self, width: u32) -> Self {
self.width = width;
self
}
pub fn with_height(mut self, height: u32) -> Self {
self.height = height;
self
}
pub fn with_payload(mut self, payload: Vec<u8>) -> Self {
self.payload = payload;
self
}
pub fn with_av1c(mut self, av1c: Vec<u8>) -> Self {
self.av1c = av1c;
self
}
pub fn with_pixi(mut self, pixi: Option<Vec<u8>>) -> Self {
self.pixi = pixi;
self
}
pub fn with_colr(mut self, colr: Option<Colr>) -> Self {
self.colr = colr;
self
}
pub fn with_clap(mut self, clap: Option<Clap>) -> Self {
self.clap = clap;
self
}
pub fn with_alpha(mut self, alpha: Option<(Vec<u8>, Vec<u8>)>) -> Self {
self.alpha = alpha;
self
}
pub fn with_alpha_pixi(mut self, alpha_pixi: Option<Vec<u8>>) -> Self {
self.alpha_pixi = alpha_pixi;
self
}
pub fn with_premultiplied(mut self, premultiplied: bool) -> Self {
self.premultiplied = premultiplied;
self
}
pub fn with_offset_x(mut self, offset_x: i32) -> Self {
self.offset_x = offset_x;
self
}
pub fn with_offset_y(mut self, offset_y: i32) -> Self {
self.offset_y = offset_y;
self
}
}
pub struct AvifOverlayMuxer {
output_width: u32,
output_height: u32,
fill: [u16; 4],
layers: Vec<OverlayLayer>,
pixi: Option<Vec<u8>>,
colr: Option<Colr>,
irot: Option<Irot>,
imir: Option<Imir>,
profile_brand: ProfileBrand,
}
impl AvifOverlayMuxer {
pub fn new(output_width: u32, output_height: u32) -> Self {
Self {
output_width,
output_height,
fill: [0, 0, 0, 65535],
layers: Vec::new(),
pixi: None,
colr: None,
irot: None,
imir: None,
profile_brand: ProfileBrand::Baseline,
}
}
pub fn with_fill(mut self, rgba: [u16; 4]) -> Self {
self.fill = rgba;
self
}
pub fn layer(mut self, layer: OverlayLayer) -> Self {
self.layers.push(layer);
self
}
pub fn with_pixi(mut self, bits: Vec<u8>) -> Self {
self.pixi = Some(bits);
self
}
pub fn with_colr(mut self, colr: Colr) -> Self {
self.colr = Some(colr);
self
}
pub fn with_irot(mut self, angle: u8) -> Self {
self.irot = Some(Irot {
angle: angle & 0x03,
});
self
}
pub fn with_imir(mut self, axis: u8) -> Self {
self.imir = Some(Imir { axis: axis & 0x01 });
self
}
pub fn advanced_profile(mut self) -> Self {
self.profile_brand = ProfileBrand::Advanced;
self
}
pub fn no_profile_brand(mut self) -> Self {
self.profile_brand = ProfileBrand::Bare;
self
}
pub fn build(self) -> Result<Vec<u8>> {
if self.layers.is_empty() {
return Err(Error::invalid("avif mux: overlay needs at least one layer"));
}
if self.output_width == 0 || self.output_height == 0 {
return Err(Error::invalid("avif mux: overlay canvas dimensions zero"));
}
let (major, compat) = still_brands(self.profile_brand);
let mut w = HeifWriter::new().with_brands(major, compat);
let desc = OverlayDescriptor::new(
self.fill,
self.output_width,
self.output_height,
self.layers
.iter()
.map(|l| (l.offset_x, l.offset_y))
.collect(),
);
let mut props = vec![prop_ispe(self.output_width, self.output_height)];
if let Some(bits) = &self.pixi {
props.push(prop_pixi(bits));
}
if let Some(colr) = &self.colr {
props.push(prop_colr(colr)?);
}
if let Some(irot) = &self.irot {
props.push(prop_irot(irot));
}
if let Some(imir) = &self.imir {
props.push(prop_imir(imir));
}
let mut layer_ids = Vec::with_capacity(self.layers.len());
let mut next = 2u32;
for layer in &self.layers {
layer_ids.push(next);
next += 1 + u32::from(layer.alpha.is_some());
}
let ov = w.add_overlay(desc, &layer_ids, props)?;
expect_id(ov, 1, "overlay")?;
for (i, layer) in self.layers.into_iter().enumerate() {
let mut lprops = vec![
prop_av1c(&layer.av1c, &format!("overlay layer {i} "))?,
prop_ispe(layer.width, layer.height),
];
if let Some(bits) = &layer.pixi {
lprops.push(prop_pixi(bits));
}
if let Some(colr) = &layer.colr {
lprops.push(prop_colr(colr)?);
}
if let Some(clap) = &layer.clap {
lprops.push(prop_clap(clap));
}
let id = w.add_coded_item(*b"av01", layer.payload, lprops);
expect_id(id, layer_ids[i], "overlay layer")?;
w.set_hidden(id, true);
if let Some((apayload, aav1c)) = layer.alpha {
let mut aprops = vec![
prop_av1c(&aav1c, &format!("overlay layer {i} alpha "))?,
prop_ispe(layer.width, layer.height),
prop_auxc(crate::alpha::ALPHA_URN_PREFIX),
];
if let Some(bits) = &layer.alpha_pixi {
aprops.push(prop_pixi(bits));
}
if let Some(clap) = &layer.clap {
aprops.push(prop_clap(clap));
}
let aid = w.add_alpha(id, *b"av01", apayload, aprops, layer.premultiplied);
w.set_hidden(aid, true);
}
}
w.set_primary(ov);
Ok(w.write_to_vec()?)
}
}
#[cfg(feature = "registry")]
pub(crate) fn sequence_brands(profile: ProfileBrand, all_sync: bool) -> ([u8; 4], Vec<[u8; 4]>) {
let mut compat = vec![*b"avis", *b"avif", *b"mif1", *b"msf1", *b"miaf", *b"av01"];
if all_sync {
compat.push(*b"avio");
}
match profile {
ProfileBrand::Baseline => compat.push(*b"MA1B"),
ProfileBrand::Advanced => compat.push(*b"MA1A"),
ProfileBrand::Bare => {}
}
(*b"avis", compat)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::{parse, parse_header};
fn synth_av1c() -> Vec<u8> {
vec![0x81, 0x0c, 0x0c, 0x00]
}
#[test]
fn still_round_trips_through_parse() {
let payload = b"\x12\x00\x0a\x0bfake-av1-obu-bytes".to_vec();
let bytes = AvifMuxer::new(64, 48, payload.clone(), synth_av1c())
.with_pixi(vec![8, 8, 8])
.with_colr(Colr::Nclx {
colour_primaries: 1,
transfer_characteristics: 13,
matrix_coefficients: 6,
full_range: true,
})
.with_pasp(Pasp {
h_spacing: 1,
v_spacing: 1,
})
.build()
.expect("mux");
let img = parse(&bytes).expect("parse muxed avif");
assert_eq!(&img.major_brand, b"avif");
assert!(img.compatible_brands.iter().any(|b| b == b"mif1"));
assert!(img.compatible_brands.iter().any(|b| b == b"MA1B"));
assert_eq!(img.primary_item_id, 1);
assert_eq!(&img.primary_item.item_type, b"av01");
assert_eq!(&*img.primary_item_data, payload.as_slice());
assert_eq!(img.av1c.as_deref(), Some(synth_av1c().as_slice()));
let ispe = img.ispe.expect("ispe");
assert_eq!((ispe.width, ispe.height), (64, 48));
assert_eq!(img.pixi.unwrap().bits_per_channel, vec![8, 8, 8]);
match img.colr.unwrap() {
Colr::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
} => {
assert_eq!(colour_primaries, 1);
assert_eq!(transfer_characteristics, 13);
assert_eq!(matrix_coefficients, 6);
assert!(full_range);
}
_ => panic!("expected nclx"),
}
let pasp = img.pasp.unwrap();
assert_eq!((pasp.h_spacing, pasp.v_spacing), (1, 1));
}
#[test]
fn transforms_round_trip() {
let payload = b"obu".to_vec();
let bytes = AvifMuxer::new(10, 20, payload, synth_av1c())
.with_irot(1)
.with_imir(1)
.build()
.expect("mux");
let hdr = parse_header(&bytes).expect("parse header");
let irot = hdr.meta.property_for(1, b"irot").expect("irot");
assert!(matches!(irot, crate::meta::Property::Irot(i) if i.angle == 1));
let imir = hdr.meta.property_for(1, b"imir").expect("imir");
assert!(matches!(imir, crate::meta::Property::Imir(i) if i.axis == 1));
}
#[test]
fn alpha_auxiliary_round_trips() {
let color = b"color-obu".to_vec();
let alpha = b"alpha-obu".to_vec();
let bytes = AvifMuxer::new(32, 32, color, synth_av1c())
.with_pixi(vec![8, 8, 8])
.with_alpha(alpha.clone(), synth_av1c(), false)
.build()
.expect("mux");
let hdr = parse_header(&bytes).expect("parse");
let alpha_id = crate::alpha::find_alpha_item_id(&hdr.meta, 1).expect("alpha id");
assert_eq!(alpha_id, 2);
let alpha_item = hdr.meta.item_by_id(2).expect("alpha item");
assert!(alpha_item.is_hidden());
let auxc = hdr.meta.property_for(2, b"auxC").expect("auxC");
assert!(matches!(auxc, crate::meta::Property::AuxC(a) if a.is_alpha()));
let loc = hdr.meta.location_by_id(2).expect("alpha iloc");
let got = crate::parser::item_bytes(&bytes, loc).expect("alpha bytes");
assert_eq!(got, alpha.as_slice());
}
#[test]
fn premultiplied_alpha_sets_prem_iref() {
let bytes = AvifMuxer::new(8, 8, b"c".to_vec(), synth_av1c())
.with_alpha(b"a".to_vec(), synth_av1c(), true)
.build()
.expect("mux");
let hdr = parse_header(&bytes).expect("parse");
assert!(hdr.meta.is_alpha_premultiplied_for(1));
}
#[test]
fn grid_round_trips_through_parse_header() {
let av1c = synth_av1c();
let mk = |n: u8| GridTile {
width: 16,
height: 16,
payload: vec![n; 5],
av1c: av1c.clone(),
};
let bytes = AvifGridMuxer::new(2, 2, 32, 32)
.tile(mk(1))
.tile(mk(2))
.tile(mk(3))
.tile(mk(4))
.with_pixi(vec![8, 8, 8])
.build()
.expect("mux grid");
let hdr = parse_header(&bytes).expect("parse grid header");
assert_eq!(hdr.meta.primary_item_id, Some(1));
let grid_item = hdr.meta.item_by_id(1).expect("grid item");
assert_eq!(&grid_item.item_type, b"grid");
let tiles = hdr.meta.iref_targets_of(b"dimg", 1);
assert_eq!(tiles, vec![2, 3, 4, 5]);
assert!(hdr.meta.location_by_id(1).is_some(), "grid iloc");
let payload = hdr.item_data(1).expect("grid bytes");
let g = crate::grid::ImageGrid::parse(&payload).expect("grid parse");
assert_eq!((g.rows, g.columns), (2, 2));
assert_eq!((g.output_width, g.output_height), (32, 32));
for id in [2u32, 3, 4, 5] {
assert!(hdr.meta.item_by_id(id).unwrap().is_hidden());
assert!(hdr.meta.property_for(id, b"av1C").is_some());
assert!(hdr.meta.property_for(id, b"ispe").is_some());
}
}
#[test]
fn rejects_short_av1c() {
let err = AvifMuxer::new(1, 1, vec![0], vec![0x81])
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn grid_rejects_tile_count_mismatch() {
let err = AvifGridMuxer::new(2, 2, 32, 32)
.tile(GridTile {
width: 16,
height: 16,
payload: vec![0],
av1c: synth_av1c(),
})
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn hdr_metadata_round_trips() {
let bytes = AvifMuxer::new(8, 8, b"obu".to_vec(), synth_av1c())
.with_pixi(vec![10, 10, 10])
.with_mdcv(Mdcv {
display_primaries_xy: [(34000, 16000), (13250, 34500), (7500, 3000)],
white_point_xy: (15635, 16450),
max_display_mastering_luminance: 10_000_000,
min_display_mastering_luminance: 50,
})
.with_clli(Clli {
max_content_light_level: 1000,
max_pic_average_light_level: 400,
})
.with_amve(Amve {
ambient_illuminance: 100_000,
ambient_light_x: 15635,
ambient_light_y: 16450,
})
.build()
.expect("mux hdr");
let img = parse(&bytes).expect("parse");
let mdcv = img.mdcv.expect("mdcv");
assert_eq!(mdcv.white_point_xy, (15635, 16450));
assert_eq!(mdcv.max_display_mastering_luminance, 10_000_000);
assert_eq!(mdcv.display_primaries_xy[0], (34000, 16000));
let clli = img.clli.expect("clli");
assert_eq!(clli.max_content_light_level, 1000);
assert_eq!(clli.max_pic_average_light_level, 400);
let amve = img.amve.expect("amve");
assert_eq!(amve.ambient_illuminance, 100_000);
assert_eq!((amve.ambient_light_x, amve.ambient_light_y), (15635, 16450));
}
#[test]
fn exif_and_xmp_metadata_items_round_trip() {
let exif = b"\x00\x00\x00\x00II*\x00exif-tiff".to_vec();
let xmp = br#"<?xpacket?><x:xmpmeta/>"#.to_vec();
let bytes = AvifMuxer::new(16, 16, b"obu".to_vec(), synth_av1c())
.with_pixi(vec![8, 8, 8])
.with_exif(exif.clone())
.with_xmp(xmp.clone())
.build()
.expect("mux metadata");
let info = crate::inspect::inspect(&bytes).expect("inspect");
assert!(info.has_descriptive_metadata());
let exif_id = info.exif_item_id.expect("exif item");
let xmp_id = info.xmp_item_id.expect("xmp item");
let got_exif = crate::inspect::item_payload_bytes(&bytes, exif_id).expect("exif");
assert_eq!(got_exif, exif);
let got_xmp = crate::inspect::item_payload_bytes(&bytes, xmp_id).expect("xmp");
assert_eq!(got_xmp, xmp);
}
#[test]
fn raw_exif_offset_and_non_utf8_xmp_round_trip() {
let exif = b"\x00\x00\x00\x06Exif\x00\x00II*\x00tiff".to_vec();
let xmp: Vec<u8> = b"\xff\xfe<\x00x\x00/\x00>\x00".to_vec();
let bytes = AvifMuxer::new(16, 16, b"obu".to_vec(), synth_av1c())
.with_exif(exif.clone())
.with_xmp(xmp.clone())
.build()
.expect("mux metadata");
let info = crate::inspect::inspect(&bytes).expect("inspect");
let exif_id = info.exif_item_id.expect("exif item");
let xmp_id = info.xmp_item_id.expect("xmp item");
assert_eq!(
crate::inspect::item_payload_bytes(&bytes, exif_id).unwrap(),
exif
);
assert_eq!(
crate::inspect::item_payload_bytes(&bytes, xmp_id).unwrap(),
xmp
);
let err = AvifMuxer::new(16, 16, b"obu".to_vec(), synth_av1c())
.with_exif(b"\x00\x00\x00\x40II*\x00".to_vec())
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidData(_)), "{err:?}");
assert!(AvifMuxer::new(16, 16, b"obu".to_vec(), synth_av1c())
.with_exif(b"\x00\x00".to_vec())
.build()
.is_err());
}
#[test]
fn item_name_and_flagged_entity_group_round_trip() {
let bytes = AvifMuxer::new(16, 16, b"obu".to_vec(), synth_av1c())
.with_item_name("cover photo")
.with_alpha(b"alpha".to_vec(), synth_av1c(), false)
.with_entity_group(EntityGroupSpec {
grouping_type: *b"altr",
group_id: 100,
flags: 0x00_00_01,
entity_ids: vec![1, 2],
})
.build()
.expect("mux");
let hdr = parse_header(&bytes).expect("parse");
assert_eq!(hdr.meta.item_by_id(1).unwrap().name, "cover photo");
let groups = hdr.meta.groups().expect("grpl");
assert_eq!(groups.len(), 1);
assert_eq!(&groups[0].grouping_type, b"altr");
assert_eq!(groups[0].group_id, 100);
assert_eq!(groups[0].flags, 1);
assert_eq!(groups[0].entity_ids, vec![1, 2]);
}
#[test]
fn depth_auxiliary_round_trips() {
let bytes = AvifMuxer::new(24, 24, b"color".to_vec(), synth_av1c())
.with_pixi(vec![8, 8, 8])
.with_depth(b"depth-obu".to_vec(), synth_av1c())
.build()
.expect("mux depth");
let info = crate::inspect::inspect(&bytes).expect("inspect");
assert!(info.has_depth_map());
let depth_id = info.depth_map_item_id.expect("depth id");
let hdr = parse_header(&bytes).expect("parse");
assert!(hdr.meta.item_by_id(depth_id).unwrap().is_hidden());
let auxc = hdr.meta.property_for(depth_id, b"auxC").expect("auxC");
assert!(matches!(auxc, crate::meta::Property::AuxC(a) if a.is_depth_map()));
}
#[test]
fn advanced_profile_sets_ma1a_brand() {
let bytes = AvifMuxer::new(8, 8, b"obu".to_vec(), synth_av1c())
.advanced_profile()
.build()
.expect("mux");
let img = parse(&bytes).expect("parse");
assert!(img.compatible_brands.iter().any(|b| b == b"MA1A"));
assert!(!img.compatible_brands.iter().any(|b| b == b"MA1B"));
}
#[test]
fn no_profile_brand_omits_ma1a_and_ma1b() {
let bytes = AvifMuxer::new(8, 8, b"obu".to_vec(), synth_av1c())
.no_profile_brand()
.build()
.expect("mux");
let img = parse(&bytes).expect("parse");
assert!(!img.compatible_brands.iter().any(|b| b == b"MA1A"));
assert!(!img.compatible_brands.iter().any(|b| b == b"MA1B"));
assert!(img.compatible_brands.iter().any(|b| b == b"mif1"));
assert!(img.compatible_brands.iter().any(|b| b == b"miaf"));
assert!(crate::parser::audit_mif1(&bytes)
.expect("audit")
.is_compliant());
}
#[test]
fn grid_muxer_profile_brand_controls() {
let av1c = synth_av1c();
let mk = |n: u8| GridTile {
width: 16,
height: 16,
payload: vec![n; 5],
av1c: av1c.clone(),
};
let adv = AvifGridMuxer::new(1, 2, 32, 16)
.tile(mk(1))
.tile(mk(2))
.advanced_profile()
.build()
.expect("mux adv grid");
let img = parse_header(&adv).expect("parse");
assert!(img.compatible_brands.iter().any(|b| b == b"MA1A"));
let bare = AvifGridMuxer::new(1, 2, 32, 16)
.tile(mk(1))
.tile(mk(2))
.no_profile_brand()
.build()
.expect("mux bare grid");
let img = parse_header(&bare).expect("parse");
assert!(!img.compatible_brands.iter().any(|b| b == b"MA1A"));
assert!(!img.compatible_brands.iter().any(|b| b == b"MA1B"));
}
#[test]
fn wide_grid_emits_32bit_descriptor() {
let av1c = synth_av1c();
let mut m = AvifGridMuxer::new(1, 2, 70_000, 8);
for _ in 0..2 {
m = m.tile(GridTile {
width: 35_000,
height: 8,
payload: vec![7; 4],
av1c: av1c.clone(),
});
}
let bytes = m.build().expect("mux wide grid");
let hdr = parse_header(&bytes).expect("parse");
assert!(hdr.meta.location_by_id(1).is_some(), "grid iloc");
let payload = hdr.item_data(1).expect("grid bytes");
let g = crate::grid::ImageGrid::parse(&payload).expect("grid parse");
assert_eq!((g.output_width, g.output_height), (70_000, 8));
assert_eq!((g.rows, g.columns), (1, 2));
}
}