use std::borrow::Cow;
use crate::alpha::find_alpha_item_id;
use crate::box_parser::{b, BoxType};
use crate::cicp::{effective_cicp, CicpTriple};
use crate::error::{AvifError as Error, Result};
use crate::grid::ImageGrid;
use crate::meta::{
Amve, Cclv, Clli, Colr, Ispe, Mdcv, Meta, Pasp, Pixi, Property, ITEM_TYPE_EXIF, ITEM_TYPE_MIME,
};
use crate::parser::{
classify_brands, parse, parse_header, AvifFile, AvifHeader, BrandClass, ITEM_TYPE_GRID,
};
const AV1C: BoxType = b(b"av1C");
const COLR: BoxType = b(b"colr");
const MDCV: BoxType = b(b"mdcv");
const CLLI: BoxType = b(b"clli");
const CCLV: BoxType = b(b"cclv");
const AMVE: BoxType = b(b"amve");
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct AvifInfo {
pub width: u32,
pub height: u32,
pub bits_per_channel: Vec<u8>,
pub pasp: Option<Pasp>,
pub av1c: Vec<u8>,
pub obu_bytes: Vec<u8>,
pub is_grid: bool,
pub has_alpha: bool,
pub brands: BrandClass,
pub colour: Option<Colr>,
pub mdcv: Option<Mdcv>,
pub clli: Option<Clli>,
pub cclv: Option<Cclv>,
pub amve: Option<Amve>,
pub bit_depth: Option<u8>,
pub monochrome: bool,
pub chroma_subsampling: Option<(bool, bool)>,
pub thumbnail_item_ids: Vec<u32>,
pub exif_item_id: Option<u32>,
pub xmp_item_id: Option<u32>,
pub premultiplied_alpha: bool,
pub aux_items: Vec<(u32, crate::meta::AuxKind)>,
pub alpha_aux_kind: Option<crate::meta::AuxKind>,
pub depth_map_item_id: Option<u32>,
pub hdr_gain_map_item_id: Option<u32>,
pub entity_group_count: usize,
pub mif1_compliance: crate::derived::Mif1Compliance,
pub operating_point: Option<crate::meta::A1op>,
pub layered_index: Option<crate::meta::A1lx>,
pub sato_item_ids: Vec<u32>,
pub tmap_item_ids: Vec<u32>,
pub tone_map_compliance: Vec<crate::derived::ToneMapCompliance>,
pub grid_derivation_compliance: Vec<crate::derived::GridDerivationAudit>,
pub iden_item_ids: Vec<u32>,
pub iden_compliance: Vec<crate::derived::IdenCompliance>,
pub alpha_bit_depth_compliance: Vec<crate::derived::AlphaBitDepthAudit>,
pub sequence_header_obu_compliance: Vec<crate::derived::SequenceHeaderObuAudit>,
pub avif_profile_compliance: Vec<crate::derived::AvifProfileCompliance>,
pub overlay_resolutions: Vec<crate::derived::OverlayResolution>,
pub iden_resolutions: Vec<crate::derived::IdenResolution>,
pub tone_map_resolutions: Vec<crate::derived::ToneMapResolution>,
pub grid_resolutions: Vec<crate::derived::GridResolution>,
}
impl AvifInfo {
#[allow(clippy::too_many_arguments)]
pub fn new(
width: u32,
height: u32,
bits_per_channel: Vec<u8>,
pasp: Option<Pasp>,
av1c: Vec<u8>,
obu_bytes: Vec<u8>,
is_grid: bool,
has_alpha: bool,
brands: BrandClass,
colour: Option<Colr>,
mdcv: Option<Mdcv>,
clli: Option<Clli>,
cclv: Option<Cclv>,
amve: Option<Amve>,
bit_depth: Option<u8>,
monochrome: bool,
chroma_subsampling: Option<(bool, bool)>,
thumbnail_item_ids: Vec<u32>,
exif_item_id: Option<u32>,
xmp_item_id: Option<u32>,
premultiplied_alpha: bool,
aux_items: Vec<(u32, crate::meta::AuxKind)>,
alpha_aux_kind: Option<crate::meta::AuxKind>,
depth_map_item_id: Option<u32>,
hdr_gain_map_item_id: Option<u32>,
entity_group_count: usize,
mif1_compliance: crate::derived::Mif1Compliance,
operating_point: Option<crate::meta::A1op>,
layered_index: Option<crate::meta::A1lx>,
sato_item_ids: Vec<u32>,
tmap_item_ids: Vec<u32>,
tone_map_compliance: Vec<crate::derived::ToneMapCompliance>,
grid_derivation_compliance: Vec<crate::derived::GridDerivationAudit>,
iden_item_ids: Vec<u32>,
iden_compliance: Vec<crate::derived::IdenCompliance>,
alpha_bit_depth_compliance: Vec<crate::derived::AlphaBitDepthAudit>,
sequence_header_obu_compliance: Vec<crate::derived::SequenceHeaderObuAudit>,
avif_profile_compliance: Vec<crate::derived::AvifProfileCompliance>,
overlay_resolutions: Vec<crate::derived::OverlayResolution>,
iden_resolutions: Vec<crate::derived::IdenResolution>,
tone_map_resolutions: Vec<crate::derived::ToneMapResolution>,
grid_resolutions: Vec<crate::derived::GridResolution>,
) -> Self {
Self {
width,
height,
bits_per_channel,
pasp,
av1c,
obu_bytes,
is_grid,
has_alpha,
brands,
colour,
mdcv,
clli,
cclv,
amve,
bit_depth,
monochrome,
chroma_subsampling,
thumbnail_item_ids,
exif_item_id,
xmp_item_id,
premultiplied_alpha,
aux_items,
alpha_aux_kind,
depth_map_item_id,
hdr_gain_map_item_id,
entity_group_count,
mif1_compliance,
operating_point,
layered_index,
sato_item_ids,
tmap_item_ids,
tone_map_compliance,
grid_derivation_compliance,
iden_item_ids,
iden_compliance,
alpha_bit_depth_compliance,
sequence_header_obu_compliance,
avif_profile_compliance,
overlay_resolutions,
iden_resolutions,
tone_map_resolutions,
grid_resolutions,
}
}
}
impl AvifInfo {
pub fn num_channels(&self) -> usize {
self.bits_per_channel.len()
}
pub fn max_bit_depth(&self) -> u8 {
self.bits_per_channel.iter().copied().max().unwrap_or(0)
}
pub fn is_monochrome(&self) -> bool {
self.num_channels() == 1
}
pub fn has_square_pixels(&self) -> bool {
match self.pasp {
None => true,
Some(p) => p.is_square(),
}
}
pub fn has_hdr_metadata(&self) -> bool {
self.mdcv.is_some() || self.clli.is_some() || self.cclv.is_some()
}
pub fn max_cll(&self) -> Option<u16> {
self.clli
.map(|c| c.max_content_light_level)
.or_else(|| self.cclv.map(|c| c.max_content_light_level))
}
pub fn max_fall(&self) -> Option<u16> {
self.clli
.map(|c| c.max_pic_average_light_level)
.or_else(|| self.cclv.map(|c| c.max_pic_average_light_level))
}
pub fn has_ambient_viewing_environment(&self) -> bool {
self.amve.is_some()
}
pub fn has_thumbnails(&self) -> bool {
!self.thumbnail_item_ids.is_empty()
}
pub fn has_descriptive_metadata(&self) -> bool {
self.exif_item_id.is_some() || self.xmp_item_id.is_some()
}
pub fn has_depth_map(&self) -> bool {
self.depth_map_item_id.is_some()
}
pub fn has_hdr_gain_map(&self) -> bool {
self.hdr_gain_map_item_id.is_some()
}
pub fn has_sample_transform(&self) -> bool {
!self.sato_item_ids.is_empty()
}
pub fn has_tone_map(&self) -> bool {
!self.tmap_item_ids.is_empty()
}
pub fn tone_map_strict_compliant(&self) -> bool {
self.tone_map_compliance.iter().all(|c| c.is_compliant())
}
pub fn grid_derivations_strict_compliant(&self) -> bool {
self.grid_derivation_compliance
.iter()
.all(|g| g.is_compliant())
}
pub fn has_iden(&self) -> bool {
!self.iden_item_ids.is_empty()
}
pub fn iden_strict_compliant(&self) -> bool {
self.iden_compliance.iter().all(|i| i.is_compliant())
}
pub fn has_overlay(&self) -> bool {
!self.overlay_resolutions.is_empty()
}
pub fn overlay_for(&self, iovl_item_id: u32) -> Option<&crate::derived::OverlayResolution> {
self.overlay_resolutions
.iter()
.find(|o| o.iovl_item_id == iovl_item_id)
}
pub fn iden_resolution_for(
&self,
iden_item_id: u32,
) -> Option<&crate::derived::IdenResolution> {
self.iden_resolutions
.iter()
.find(|i| i.iden_item_id == iden_item_id)
}
pub fn tone_map_resolution_for(
&self,
tmap_item_id: u32,
) -> Option<&crate::derived::ToneMapResolution> {
self.tone_map_resolutions
.iter()
.find(|t| t.tmap_item_id == tmap_item_id)
}
pub fn has_grid(&self) -> bool {
!self.grid_resolutions.is_empty()
}
pub fn grid_resolution_for(
&self,
grid_item_id: u32,
) -> Option<&crate::derived::GridResolution> {
self.grid_resolutions
.iter()
.find(|g| g.grid_item_id == grid_item_id)
}
pub fn alpha_bit_depth_strict_compliant(&self) -> bool {
self.alpha_bit_depth_compliance
.iter()
.all(|a| a.is_compliant())
}
pub fn sequence_header_obu_strict_compliant(&self) -> bool {
self.sequence_header_obu_compliance
.iter()
.all(|a| a.is_compliant())
}
pub fn avif_profile_strict_compliant(&self) -> bool {
self.avif_profile_compliance
.iter()
.all(|a| a.is_compliant())
}
pub fn is_strict_mif1(&self) -> bool {
self.mif1_compliance.claims_mif1 && self.mif1_compliance.is_compliant()
}
pub fn effective_cicp(&self) -> CicpTriple {
effective_cicp(self.colour.as_ref())
}
}
pub fn inspect(file: &[u8]) -> Result<AvifInfo> {
let hdr = parse_header(file)?;
let primary_id = hdr
.meta
.primary_item_id
.ok_or_else(|| Error::invalid("avif: missing pitm"))?;
let primary_info = hdr
.meta
.item_by_id(primary_id)
.ok_or_else(|| Error::invalid("avif: pitm references unknown item"))?;
let brands = classify_brands(&hdr.major_brand, &hdr.compatible_brands)?;
let mif1_compliance = crate::parser::audit_mif1(file)?;
if primary_info.item_type == ITEM_TYPE_GRID {
build_info_grid(&hdr, primary_id, brands, mif1_compliance)
} else if primary_info.item_type == crate::meta::ITEM_TYPE_IOVL
|| primary_info.item_type == crate::meta::ITEM_TYPE_IDEN
|| primary_info.item_type == crate::meta::ITEM_TYPE_TMAP
|| primary_info.item_type == crate::meta::ITEM_TYPE_SATO
{
build_info_derived(&hdr, primary_id, brands, mif1_compliance)
} else {
let img = parse(file)?;
build_info(
&img,
find_alpha_item_id(&hdr.meta, primary_id).is_some(),
brands,
mif1_compliance,
file,
)
}
}
fn resolve_metadata_items(meta: &Meta, target_id: u32) -> (Option<u32>, Option<u32>) {
const CDSC: BoxType = b(b"cdsc");
let mut exif = None;
let mut xmp = None;
for src in meta.iref_sources_of(&CDSC, target_id) {
let Some(info) = meta.item_by_id(src) else {
continue;
};
if info.item_type == ITEM_TYPE_EXIF {
if exif.is_none() {
exif = Some(src);
}
continue;
}
if info.item_type == ITEM_TYPE_MIME {
let ct = info.content_type.as_deref().unwrap_or("");
let ct_lower = ct.to_ascii_lowercase();
let is_xmp =
ct_lower == "application/rdf+xml" || ct_lower.starts_with("application/rdf+xml");
let is_exif_mime = ct_lower == "application/octet-stream"
|| ct_lower == "image/tiff"
|| ct_lower == "image/x-exif";
if is_xmp && xmp.is_none() {
xmp = Some(src);
} else if is_exif_mime && exif.is_none() {
exif = Some(src);
}
}
}
(exif, xmp)
}
pub fn item_payload_bytes(file: &[u8], item_id: u32) -> Result<Vec<u8>> {
let hdr = parse_header(file)?;
hdr.item_data(item_id).map(Cow::into_owned)
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedRegionItem {
pub region_item_id: u32,
pub image_item_id: u32,
pub region: crate::region::RegionItem,
}
impl ResolvedRegionItem {
#[allow(clippy::too_many_arguments)]
pub fn new(region_item_id: u32, image_item_id: u32, region: crate::region::RegionItem) -> Self {
Self {
region_item_id,
image_item_id,
region,
}
}
}
pub fn region_items_for(file: &[u8], image_item_id: u32) -> Result<Vec<ResolvedRegionItem>> {
let hdr = parse_header(file)?;
region_items_for_meta(file, &hdr.meta, image_item_id)
}
pub fn entity_groups(file: &[u8]) -> Result<Vec<crate::derived::EntityGroup>> {
let hdr = parse_header(file)?;
hdr.meta.groups()
}
pub fn region_items(file: &[u8]) -> Result<Vec<ResolvedRegionItem>> {
let hdr = parse_header(file)?;
let primary = hdr
.meta
.primary_item_id
.ok_or_else(|| crate::error::AvifError::invalid("avif: no primary item (pitm)"))?;
region_items_for_meta(file, &hdr.meta, primary)
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedTextItem {
pub text_item_id: u32,
pub image_item_id: u32,
pub content_type: String,
pub font_item_ids: Vec<u32>,
}
impl ResolvedTextItem {
#[allow(clippy::too_many_arguments)]
pub fn new(
text_item_id: u32,
image_item_id: u32,
content_type: String,
font_item_ids: Vec<u32>,
) -> Self {
Self {
text_item_id,
image_item_id,
content_type,
font_item_ids,
}
}
}
pub fn text_items_for(file: &[u8], image_item_id: u32) -> Result<Vec<ResolvedTextItem>> {
let hdr = parse_header(file)?;
Ok(text_items_for_meta(&hdr.meta, image_item_id))
}
pub fn text_items(file: &[u8]) -> Result<Vec<ResolvedTextItem>> {
let hdr = parse_header(file)?;
let primary = hdr
.meta
.primary_item_id
.ok_or_else(|| crate::error::AvifError::invalid("avif: no primary item (pitm)"))?;
Ok(text_items_for_meta(&hdr.meta, primary))
}
fn text_items_for_meta(meta: &Meta, image_item_id: u32) -> Vec<ResolvedTextItem> {
const TEXT: BoxType = b(b"text");
const FONT: BoxType = b(b"font");
let mut out = Vec::new();
for src in meta.iref_sources_of(&TEXT, image_item_id) {
let Some(info) = meta.item_by_id(src) else {
continue;
};
if info.item_type != ITEM_TYPE_MIME {
continue;
}
let content_type = info.content_type.clone().unwrap_or_default();
let font_item_ids = meta
.irefs
.iter()
.filter(|e| e.reference_type == FONT && e.from_id == src)
.flat_map(|e| e.to_ids.iter().copied())
.collect();
out.push(ResolvedTextItem {
text_item_id: src,
image_item_id,
content_type,
font_item_ids,
});
}
out
}
pub fn is_font_item(item: &crate::meta::ItemInfo) -> bool {
item.item_type == ITEM_TYPE_MIME
&& item
.content_type
.as_deref()
.map(|ct| ct.to_ascii_lowercase().starts_with("font/"))
.unwrap_or(false)
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CodedItemDependencies {
pub pred: Vec<u32>,
pub base: Vec<u32>,
pub exbl: Vec<u32>,
pub tbas: Option<u32>,
}
impl CodedItemDependencies {
#[allow(clippy::too_many_arguments)]
pub fn new(pred: Vec<u32>, base: Vec<u32>, exbl: Vec<u32>, tbas: Option<u32>) -> Self {
Self {
pred,
base,
exbl,
tbas,
}
}
}
impl CodedItemDependencies {
pub fn is_predictively_coded(&self) -> bool {
!self.pred.is_empty()
}
pub fn is_pre_derived(&self) -> bool {
!self.base.is_empty()
}
pub fn has_dependencies(&self) -> bool {
self.is_predictively_coded()
|| self.is_pre_derived()
|| !self.exbl.is_empty()
|| self.tbas.is_some()
}
}
pub fn coded_item_dependencies(file: &[u8], item_id: u32) -> Result<CodedItemDependencies> {
let hdr = parse_header(file)?;
Ok(coded_item_dependencies_from_meta(&hdr.meta, item_id))
}
fn coded_item_dependencies_from_meta(meta: &Meta, item_id: u32) -> CodedItemDependencies {
const PRED: BoxType = b(b"pred");
const BASE: BoxType = b(b"base");
const EXBL: BoxType = b(b"exbl");
const TBAS: BoxType = b(b"tbas");
CodedItemDependencies {
pred: meta.iref_targets_of(&PRED, item_id),
base: meta.iref_targets_of(&BASE, item_id),
exbl: meta.iref_targets_of(&EXBL, item_id),
tbas: meta.iref_targets_of(&TBAS, item_id).into_iter().next(),
}
}
fn region_items_for_meta(
file: &[u8],
meta: &Meta,
image_item_id: u32,
) -> Result<Vec<ResolvedRegionItem>> {
const CDSC: BoxType = b(b"cdsc");
let mut out = Vec::new();
for src in meta.iref_sources_of(&CDSC, image_item_id) {
let Some(info) = meta.item_by_id(src) else {
continue;
};
if info.item_type != crate::region::ITEM_TYPE_RGAN {
continue;
}
let bytes = crate::parser::item_bytes_owned_full(file, meta, src)?;
let region = crate::region::RegionItem::parse(&bytes)?;
out.push(ResolvedRegionItem {
region_item_id: src,
image_item_id,
region,
});
}
Ok(out)
}
pub fn derivation_graph(file: &[u8]) -> Result<crate::derived::DerivationGraph> {
let hdr = parse_header(file)?;
let primary_id = hdr
.meta
.primary_item_id
.ok_or_else(|| Error::invalid("avif: missing pitm"))?;
if hdr.meta.item_by_id(primary_id).is_none() {
return Err(Error::invalid("avif: pitm references unknown item"));
}
let idat = hdr.meta.idat.as_deref();
Ok(crate::derived::build_derivation_graph(
&hdr.meta, primary_id, hdr.file, idat,
))
}
pub fn gain_map_metadata(
file: &[u8],
tmap_item_id: u32,
) -> Result<crate::derived::GainMapMetadata> {
let bytes = item_payload_bytes(file, tmap_item_id)?;
crate::derived::GainMapMetadata::parse_tone_map_image(&bytes)
}
fn prem_between(meta: &Meta, master: u32, alpha: u32) -> bool {
meta.irefs.iter().any(|e| {
&e.reference_type == b"prem"
&& ((e.from_id == alpha && e.to_ids.contains(&master))
|| (e.from_id == master && e.to_ids.contains(&alpha)))
})
}
fn decode_av1c_flags(av1c: &[u8]) -> (Option<u8>, bool, Option<(bool, bool)>) {
if av1c.len() < 3 {
return (None, false, None);
}
let b2 = av1c[2];
let high_bitdepth = ((b2 >> 6) & 1) != 0;
let twelve_bit = ((b2 >> 5) & 1) != 0;
let monochrome = ((b2 >> 4) & 1) != 0;
let chroma_subsampling_x = ((b2 >> 3) & 1) != 0;
let chroma_subsampling_y = ((b2 >> 2) & 1) != 0;
let bit_depth = if twelve_bit {
12
} else if high_bitdepth {
10
} else {
8
};
let subsampling = if monochrome {
None
} else {
Some((chroma_subsampling_x, chroma_subsampling_y))
};
(Some(bit_depth), monochrome, subsampling)
}
pub(crate) fn build_info(
img: &AvifFile<'_>,
has_alpha: bool,
brands: BrandClass,
mif1_compliance: crate::derived::Mif1Compliance,
file: &[u8],
) -> Result<AvifInfo> {
let av1c = img
.av1c
.clone()
.ok_or_else(|| Error::invalid("avif: primary item missing av1C property"))?;
let Ispe { width, height } = img
.ispe
.ok_or_else(|| Error::invalid("avif: primary item missing ispe property"))?;
let bits_per_channel = img
.pixi
.as_ref()
.map(|p: &Pixi| p.bits_per_channel.clone())
.unwrap_or_default();
let (bit_depth, monochrome, chroma_subsampling) = decode_av1c_flags(&av1c);
let mdcv = img.mdcv;
let clli = img.clli;
let cclv = img.cclv;
let amve = img.amve;
let primary_id = img.primary_item_id;
let thumbnail_item_ids = img.meta.iref_sources_of(b"thmb", primary_id);
let (exif_item_id, xmp_item_id) = resolve_metadata_items(&img.meta, primary_id);
let premultiplied_alpha = if has_alpha {
match find_alpha_item_id(&img.meta, primary_id) {
Some(alpha_id) => prem_between(&img.meta, primary_id, alpha_id),
None => false,
}
} else {
false
};
let aux_items = img.meta.aux_items_for(primary_id);
let alpha_aux_kind = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::Alpha))
.map(|(_, k)| *k);
let depth_map_item_id = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::DepthMap))
.map(|(id, _)| *id);
let hdr_gain_map_item_id = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::HdrGainMap))
.map(|(id, _)| *id);
let entity_group_count = img.meta.groups().map(|g| g.len()).unwrap_or(0);
let operating_point = match img.meta.property_for(primary_id, b"a1op") {
Some(Property::A1op(a)) => Some(*a),
_ => None,
};
let layered_index = match img.meta.property_for(primary_id, b"a1lx") {
Some(Property::A1lx(a)) => Some(*a),
_ => None,
};
let sato_item_ids = img.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_SATO);
let tmap_item_ids = img.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_TMAP);
let tone_map_compliance = crate::derived::audit_tone_map(&img.meta);
let grid_derivation_compliance = crate::derived::audit_grid_derivations(&img.meta);
let iden_item_ids = img.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_IDEN);
let iden_compliance = crate::derived::audit_iden_derivations(&img.meta);
let alpha_bit_depth_compliance = crate::derived::audit_alpha_bit_depth(&img.meta);
let sequence_header_obu_compliance = crate::derived::audit_sequence_header_obu(&img.meta, file);
let avif_profile_compliance = crate::derived::audit_avif_profile_compliance(&img.meta, &brands);
let idat = img.meta.idat.as_deref();
let overlay_resolutions = crate::derived::resolve_overlays(&img.meta, file, idat);
let iden_resolutions = crate::derived::resolve_iden_derivations(&img.meta, file, idat);
let tone_map_resolutions = crate::derived::resolve_tone_maps(&img.meta, file, idat);
let grid_resolutions = crate::derived::resolve_grids(&img.meta, file, idat);
Ok(AvifInfo {
width,
height,
bits_per_channel,
pasp: img.pasp,
av1c,
obu_bytes: img.primary_item_data.to_vec(),
is_grid: false,
has_alpha,
brands,
colour: img.colr.clone(),
mdcv,
clli,
cclv,
amve,
bit_depth,
monochrome,
chroma_subsampling,
thumbnail_item_ids,
exif_item_id,
xmp_item_id,
premultiplied_alpha,
aux_items,
alpha_aux_kind,
depth_map_item_id,
hdr_gain_map_item_id,
entity_group_count,
mif1_compliance,
operating_point,
layered_index,
sato_item_ids,
tmap_item_ids,
tone_map_compliance,
grid_derivation_compliance,
iden_item_ids,
iden_compliance,
alpha_bit_depth_compliance,
sequence_header_obu_compliance,
avif_profile_compliance,
overlay_resolutions,
iden_resolutions,
tone_map_resolutions,
grid_resolutions,
})
}
pub(crate) fn build_info_grid(
hdr: &AvifHeader<'_>,
primary_id: u32,
brands: BrandClass,
mif1_compliance: crate::derived::Mif1Compliance,
) -> Result<AvifInfo> {
if hdr.meta.location_by_id(primary_id).is_none() {
return Err(Error::invalid("avif: grid item missing in iloc"));
}
let grid_bytes = hdr.item_data(primary_id)?;
let grid = ImageGrid::parse(&grid_bytes)?;
let dimg = b(b"dimg");
let tile_ids = hdr.meta.iref_targets(&dimg, primary_id);
if tile_ids.is_empty() {
return Err(Error::invalid("avif: grid item has no dimg iref"));
}
let first_tile_id = tile_ids[0];
let av1c = match hdr.meta.property_for(first_tile_id, &AV1C) {
Some(Property::Av1C(bytes)) => bytes.clone(),
_ => {
return Err(Error::invalid(
"avif: first grid tile missing av1C property",
))
}
};
let bits_per_channel = match hdr.meta.property_for(primary_id, b"pixi") {
Some(Property::Pixi(pixi)) => pixi.bits_per_channel.clone(),
_ => match hdr.meta.property_for(first_tile_id, b"pixi") {
Some(Property::Pixi(pixi)) => pixi.bits_per_channel.clone(),
_ => Vec::new(),
},
};
let pasp = match hdr.meta.property_for(primary_id, b"pasp") {
Some(Property::Pasp(p)) => Some(*p),
_ => match hdr.meta.property_for(first_tile_id, b"pasp") {
Some(Property::Pasp(p)) => Some(*p),
_ => None,
},
};
let colour = match hdr.meta.property_for(primary_id, &COLR) {
Some(Property::Colr(c)) => Some(c.clone()),
_ => match hdr.meta.property_for(first_tile_id, &COLR) {
Some(Property::Colr(c)) => Some(c.clone()),
_ => None,
},
};
let mdcv = match hdr.meta.property_for(primary_id, &MDCV) {
Some(Property::Mdcv(m)) => Some(*m),
_ => match hdr.meta.property_for(first_tile_id, &MDCV) {
Some(Property::Mdcv(m)) => Some(*m),
_ => None,
},
};
let clli = match hdr.meta.property_for(primary_id, &CLLI) {
Some(Property::Clli(c)) => Some(*c),
_ => match hdr.meta.property_for(first_tile_id, &CLLI) {
Some(Property::Clli(c)) => Some(*c),
_ => None,
},
};
let cclv = match hdr.meta.property_for(primary_id, &CCLV) {
Some(Property::Cclv(c)) => Some(*c),
_ => match hdr.meta.property_for(first_tile_id, &CCLV) {
Some(Property::Cclv(c)) => Some(*c),
_ => None,
},
};
let amve = match hdr.meta.property_for(primary_id, &AMVE) {
Some(Property::Amve(a)) => Some(*a),
_ => match hdr.meta.property_for(first_tile_id, &AMVE) {
Some(Property::Amve(a)) => Some(*a),
_ => None,
},
};
let (bit_depth, monochrome, chroma_subsampling) = decode_av1c_flags(&av1c);
let thumbnail_item_ids = hdr.meta.iref_sources_of(b"thmb", primary_id);
let (exif_item_id, xmp_item_id) = resolve_metadata_items(&hdr.meta, primary_id);
let has_alpha = find_alpha_item_id(&hdr.meta, primary_id).is_some();
let premultiplied_alpha = if has_alpha {
match find_alpha_item_id(&hdr.meta, primary_id) {
Some(alpha_id) => prem_between(&hdr.meta, primary_id, alpha_id),
None => false,
}
} else {
false
};
let aux_items = hdr.meta.aux_items_for(primary_id);
let alpha_aux_kind = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::Alpha))
.map(|(_, k)| *k);
let depth_map_item_id = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::DepthMap))
.map(|(id, _)| *id);
let hdr_gain_map_item_id = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::HdrGainMap))
.map(|(id, _)| *id);
let entity_group_count = hdr.meta.groups().map(|g| g.len()).unwrap_or(0);
let operating_point = match hdr.meta.property_for(primary_id, b"a1op") {
Some(Property::A1op(a)) => Some(*a),
_ => None,
};
let layered_index = match hdr.meta.property_for(primary_id, b"a1lx") {
Some(Property::A1lx(a)) => Some(*a),
_ => None,
};
let sato_item_ids = hdr.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_SATO);
let tmap_item_ids = hdr.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_TMAP);
let tone_map_compliance = crate::derived::audit_tone_map(&hdr.meta);
let grid_derivation_compliance = crate::derived::audit_grid_derivations(&hdr.meta);
let iden_item_ids = hdr.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_IDEN);
let iden_compliance = crate::derived::audit_iden_derivations(&hdr.meta);
let alpha_bit_depth_compliance = crate::derived::audit_alpha_bit_depth(&hdr.meta);
let sequence_header_obu_compliance =
crate::derived::audit_sequence_header_obu(&hdr.meta, hdr.file);
let avif_profile_compliance = crate::derived::audit_avif_profile_compliance(&hdr.meta, &brands);
let idat = hdr.meta.idat.as_deref();
let overlay_resolutions = crate::derived::resolve_overlays(&hdr.meta, hdr.file, idat);
let iden_resolutions = crate::derived::resolve_iden_derivations(&hdr.meta, hdr.file, idat);
let tone_map_resolutions = crate::derived::resolve_tone_maps(&hdr.meta, hdr.file, idat);
let grid_resolutions = crate::derived::resolve_grids(&hdr.meta, hdr.file, idat);
Ok(AvifInfo {
width: grid.output_width,
height: grid.output_height,
bits_per_channel,
pasp,
av1c,
obu_bytes: Vec::new(),
is_grid: true,
has_alpha,
brands,
colour,
mdcv,
clli,
cclv,
amve,
bit_depth,
monochrome,
chroma_subsampling,
thumbnail_item_ids,
exif_item_id,
xmp_item_id,
premultiplied_alpha,
aux_items,
alpha_aux_kind,
depth_map_item_id,
hdr_gain_map_item_id,
entity_group_count,
mif1_compliance,
operating_point,
layered_index,
sato_item_ids,
tmap_item_ids,
tone_map_compliance,
grid_derivation_compliance,
iden_item_ids,
iden_compliance,
alpha_bit_depth_compliance,
sequence_header_obu_compliance,
avif_profile_compliance,
overlay_resolutions,
iden_resolutions,
tone_map_resolutions,
grid_resolutions,
})
}
fn first_coded_leaf(meta: &Meta, item_id: u32, depth: u32) -> Option<u32> {
let mut visited = Vec::new();
first_coded_leaf_visited(meta, item_id, depth, &mut visited)
}
fn first_coded_leaf_visited(
meta: &Meta,
item_id: u32,
depth: u32,
visited: &mut Vec<u32>,
) -> Option<u32> {
if depth > crate::derived::MAX_DERIVATION_DEPTH || visited.contains(&item_id) {
return None;
}
visited.push(item_id);
let item = meta.item_by_id(item_id)?;
if item.item_type == crate::parser::ITEM_TYPE_AV01 {
return Some(item_id);
}
let inputs = meta.iref_targets(b"dimg", item_id);
for src in inputs {
if let Some(leaf) = first_coded_leaf_visited(meta, src, depth + 1, visited) {
return Some(leaf);
}
}
None
}
pub(crate) fn build_info_derived(
hdr: &AvifHeader<'_>,
primary_id: u32,
brands: BrandClass,
mif1_compliance: crate::derived::Mif1Compliance,
) -> Result<AvifInfo> {
let idat = hdr.meta.idat.as_deref();
let (width, height) = crate::derived::reconstructed_dims(&hdr.meta, primary_id, hdr.file, idat)
.ok_or_else(|| {
Error::invalid("avif: could not resolve derived primary output dimensions")
})?;
let (width, height) =
crate::derived::output_dims_from_reconstructed(&hdr.meta, primary_id, width, height);
let leaf_id = first_coded_leaf(&hdr.meta, primary_id, 0)
.ok_or_else(|| Error::invalid("avif: derived primary has no coded av01 leaf for av1C"))?;
let av1c = match hdr.meta.property_for(leaf_id, &AV1C) {
Some(Property::Av1C(bytes)) => bytes.clone(),
_ => {
return Err(Error::invalid(
"avif: derived primary's coded leaf missing av1C property",
))
}
};
let bits_per_channel = match hdr.meta.property_for(primary_id, b"pixi") {
Some(Property::Pixi(p)) => p.bits_per_channel.clone(),
_ => match hdr.meta.property_for(leaf_id, b"pixi") {
Some(Property::Pixi(p)) => p.bits_per_channel.clone(),
_ => Vec::new(),
},
};
let pasp = match hdr.meta.property_for(primary_id, b"pasp") {
Some(Property::Pasp(p)) => Some(*p),
_ => match hdr.meta.property_for(leaf_id, b"pasp") {
Some(Property::Pasp(p)) => Some(*p),
_ => None,
},
};
let colour = match hdr.meta.property_for(primary_id, &COLR) {
Some(Property::Colr(c)) => Some(c.clone()),
_ => match hdr.meta.property_for(leaf_id, &COLR) {
Some(Property::Colr(c)) => Some(c.clone()),
_ => None,
},
};
let mdcv = match hdr.meta.property_for(primary_id, &MDCV) {
Some(Property::Mdcv(m)) => Some(*m),
_ => match hdr.meta.property_for(leaf_id, &MDCV) {
Some(Property::Mdcv(m)) => Some(*m),
_ => None,
},
};
let clli = match hdr.meta.property_for(primary_id, &CLLI) {
Some(Property::Clli(c)) => Some(*c),
_ => match hdr.meta.property_for(leaf_id, &CLLI) {
Some(Property::Clli(c)) => Some(*c),
_ => None,
},
};
let cclv = match hdr.meta.property_for(primary_id, &CCLV) {
Some(Property::Cclv(c)) => Some(*c),
_ => match hdr.meta.property_for(leaf_id, &CCLV) {
Some(Property::Cclv(c)) => Some(*c),
_ => None,
},
};
let amve = match hdr.meta.property_for(primary_id, &AMVE) {
Some(Property::Amve(a)) => Some(*a),
_ => match hdr.meta.property_for(leaf_id, &AMVE) {
Some(Property::Amve(a)) => Some(*a),
_ => None,
},
};
let (bit_depth, monochrome, chroma_subsampling) = decode_av1c_flags(&av1c);
let thumbnail_item_ids = hdr.meta.iref_sources_of(b"thmb", primary_id);
let (exif_item_id, xmp_item_id) = resolve_metadata_items(&hdr.meta, primary_id);
let has_alpha = find_alpha_item_id(&hdr.meta, primary_id).is_some();
let premultiplied_alpha = if has_alpha {
match find_alpha_item_id(&hdr.meta, primary_id) {
Some(alpha_id) => prem_between(&hdr.meta, primary_id, alpha_id),
None => false,
}
} else {
false
};
let aux_items = hdr.meta.aux_items_for(primary_id);
let alpha_aux_kind = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::Alpha))
.map(|(_, k)| *k);
let depth_map_item_id = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::DepthMap))
.map(|(id, _)| *id);
let hdr_gain_map_item_id = aux_items
.iter()
.find(|(_, k)| matches!(k, crate::meta::AuxKind::HdrGainMap))
.map(|(id, _)| *id);
let entity_group_count = hdr.meta.groups().map(|g| g.len()).unwrap_or(0);
let operating_point = match hdr.meta.property_for(primary_id, b"a1op") {
Some(Property::A1op(a)) => Some(*a),
_ => None,
};
let layered_index = match hdr.meta.property_for(primary_id, b"a1lx") {
Some(Property::A1lx(a)) => Some(*a),
_ => None,
};
let sato_item_ids = hdr.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_SATO);
let tmap_item_ids = hdr.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_TMAP);
let tone_map_compliance = crate::derived::audit_tone_map(&hdr.meta);
let grid_derivation_compliance = crate::derived::audit_grid_derivations(&hdr.meta);
let iden_item_ids = hdr.meta.item_ids_of_type(&crate::meta::ITEM_TYPE_IDEN);
let iden_compliance = crate::derived::audit_iden_derivations(&hdr.meta);
let alpha_bit_depth_compliance = crate::derived::audit_alpha_bit_depth(&hdr.meta);
let sequence_header_obu_compliance =
crate::derived::audit_sequence_header_obu(&hdr.meta, hdr.file);
let avif_profile_compliance = crate::derived::audit_avif_profile_compliance(&hdr.meta, &brands);
let overlay_resolutions = crate::derived::resolve_overlays(&hdr.meta, hdr.file, idat);
let iden_resolutions = crate::derived::resolve_iden_derivations(&hdr.meta, hdr.file, idat);
let tone_map_resolutions = crate::derived::resolve_tone_maps(&hdr.meta, hdr.file, idat);
let grid_resolutions = crate::derived::resolve_grids(&hdr.meta, hdr.file, idat);
Ok(AvifInfo {
width,
height,
bits_per_channel,
pasp,
av1c,
obu_bytes: Vec::new(),
is_grid: false,
has_alpha,
brands,
colour,
mdcv,
clli,
cclv,
amve,
bit_depth,
monochrome,
chroma_subsampling,
thumbnail_item_ids,
exif_item_id,
xmp_item_id,
premultiplied_alpha,
aux_items,
alpha_aux_kind,
depth_map_item_id,
hdr_gain_map_item_id,
entity_group_count,
mif1_compliance,
operating_point,
layered_index,
sato_item_ids,
tmap_item_ids,
tone_map_compliance,
grid_derivation_compliance,
iden_item_ids,
iden_compliance,
alpha_bit_depth_compliance,
sequence_header_obu_compliance,
avif_profile_compliance,
overlay_resolutions,
iden_resolutions,
tone_map_resolutions,
grid_resolutions,
})
}
pub fn transforms_for(meta: &Meta, item_id: u32) -> Vec<&Property> {
let mut out = Vec::new();
let Some(assoc) = meta.assoc_by_id(item_id) else {
return out;
};
for pa in &assoc.entries {
let Some(prop) = meta.properties.get(pa.index as usize) else {
continue;
};
match prop {
Property::Clap(_) | Property::Irot(_) | Property::Imir(_) => out.push(prop),
_ => {}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/monochrome.avif");
#[test]
fn inspect_extracts_primary_item() {
let info = inspect(FIXTURE).expect("inspect");
assert!(info.width > 0 && info.height > 0);
assert_eq!(info.av1c[0], 0x81);
assert!(!info.is_grid);
assert!(!info.has_alpha);
}
#[test]
fn entity_groups_empty_for_grpl_free_fixture() {
let groups = entity_groups(FIXTURE).expect("entity_groups");
assert!(groups.is_empty());
}
#[test]
fn inspect_av1c_flags_decoded() {
let info = inspect(FIXTURE).expect("inspect");
let bd = info
.bit_depth
.expect("bit_depth should be populated from av1C");
assert_eq!(bd, 8, "monochrome fixture should be 8-bit");
assert!(
info.monochrome,
"monochrome fixture should set monochrome=true"
);
assert!(
info.chroma_subsampling.is_none(),
"monochrome stream has no chroma planes"
);
}
#[test]
fn inspect_sdr_fixture_has_no_hdr_metadata() {
let info = inspect(FIXTURE).expect("inspect");
assert!(
!info.has_hdr_metadata(),
"SDR monochrome fixture must not have HDR metadata"
);
assert!(info.mdcv.is_none(), "mdcv should be None for SDR fixture");
assert!(info.clli.is_none(), "clli should be None for SDR fixture");
assert!(info.cclv.is_none(), "cclv should be None for SDR fixture");
assert!(info.max_cll().is_none(), "max_cll() should be None for SDR");
assert!(
info.max_fall().is_none(),
"max_fall() should be None for SDR"
);
}
#[test]
fn decode_av1c_flags_hdr_bit_depths() {
let av1c_10bit = [0x81, 0x00, 0x4c, 0x00];
let (bd, mono, sub) = decode_av1c_flags(&av1c_10bit);
assert_eq!(bd, Some(10));
assert!(!mono);
assert_eq!(sub, Some((true, true)));
let av1c_12bit = [0x81, 0x00, 0x6c, 0x00];
let (bd, mono, sub) = decode_av1c_flags(&av1c_12bit);
assert_eq!(bd, Some(12));
assert!(!mono);
assert!(sub.is_some());
let av1c_mono = [0x81, 0x00, 0x10, 0x00];
let (bd, mono, sub) = decode_av1c_flags(&av1c_mono);
assert_eq!(bd, Some(8));
assert!(mono);
assert!(sub.is_none());
let av1c_444 = [0x81, 0x00, 0x00, 0x00];
let (bd, mono, sub) = decode_av1c_flags(&av1c_444);
assert_eq!(bd, Some(8));
assert!(!mono);
assert_eq!(sub, Some((false, false)));
let (bd, mono, sub) = decode_av1c_flags(&[0x81]);
assert!(bd.is_none());
assert!(!mono);
assert!(sub.is_none());
}
#[test]
fn inspect_fixture_resolves_native_exif_metadata_item() {
let info = inspect(FIXTURE).expect("inspect");
assert!(info.thumbnail_item_ids.is_empty());
assert_eq!(
info.exif_item_id,
Some(2),
"monochrome.avif fixture must surface its Exif metadata item via cdsc"
);
assert!(
info.xmp_item_id.is_none(),
"monochrome.avif ships no XMP item"
);
assert!(!info.premultiplied_alpha);
assert!(!info.has_thumbnails());
assert!(
info.has_descriptive_metadata(),
"Exif item presence implies has_descriptive_metadata() is true"
);
let exif_bytes = item_payload_bytes(FIXTURE, info.exif_item_id.unwrap())
.expect("extract exif item bytes");
assert!(
exif_bytes.len() > 4,
"exif payload at least carries the 4-byte tiff_header_offset"
);
let off = u32::from_be_bytes(exif_bytes[0..4].try_into().unwrap()) as usize;
let tiff_start = 4 + off;
assert!(
tiff_start + 2 <= exif_bytes.len(),
"tiff offset {off} fits inside payload of {} bytes",
exif_bytes.len()
);
let bom = &exif_bytes[tiff_start..tiff_start + 2];
assert!(
bom == b"II" || bom == b"MM",
"TIFF header BOM must be II or MM, got {bom:?}"
);
}
use crate::meta::{IrefEntry, ItemInfo};
fn make_item(id: u32, item_type: &[u8; 4]) -> ItemInfo {
ItemInfo {
id,
item_type: *item_type,
name: String::new(),
content_type: None,
content_encoding: None,
item_uri_type: None,
flags: 0,
}
}
fn make_mime_item(id: u32, content_type: &str) -> ItemInfo {
ItemInfo {
id,
item_type: *b"mime",
name: String::new(),
content_type: Some(content_type.to_string()),
content_encoding: None,
item_uri_type: None,
flags: 0,
}
}
#[test]
fn resolve_metadata_picks_native_exif_item() {
let meta = Meta {
items: vec![make_item(2, b"Exif")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
let (exif, xmp) = resolve_metadata_items(&meta, 1);
assert_eq!(exif, Some(2));
assert!(xmp.is_none());
}
#[test]
fn resolve_metadata_picks_mime_wrapped_exif() {
let meta = Meta {
items: vec![make_mime_item(3, "application/octet-stream")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 3,
to_ids: vec![1],
}],
..Meta::default()
};
let (exif, xmp) = resolve_metadata_items(&meta, 1);
assert_eq!(exif, Some(3));
assert!(xmp.is_none());
}
#[test]
fn resolve_metadata_picks_xmp_mime_item() {
let meta = Meta {
items: vec![make_mime_item(4, "application/rdf+xml")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 4,
to_ids: vec![1],
}],
..Meta::default()
};
let (exif, xmp) = resolve_metadata_items(&meta, 1);
assert!(exif.is_none());
assert_eq!(xmp, Some(4));
}
#[test]
fn resolve_metadata_picks_both_exif_and_xmp() {
let meta = Meta {
items: vec![
make_item(2, b"Exif"),
make_mime_item(3, "application/rdf+xml"),
],
irefs: vec![
IrefEntry {
reference_type: *b"cdsc",
from_id: 2,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"cdsc",
from_id: 3,
to_ids: vec![1],
},
],
..Meta::default()
};
let (exif, xmp) = resolve_metadata_items(&meta, 1);
assert_eq!(exif, Some(2));
assert_eq!(xmp, Some(3));
}
#[test]
fn resolve_metadata_xmp_match_is_case_insensitive() {
let meta = Meta {
items: vec![make_mime_item(4, "Application/RDF+XML")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 4,
to_ids: vec![1],
}],
..Meta::default()
};
let (_, xmp) = resolve_metadata_items(&meta, 1);
assert_eq!(xmp, Some(4));
}
#[test]
fn resolve_metadata_ignores_items_not_linked_via_cdsc() {
let meta = Meta {
items: vec![make_item(2, b"Exif")],
irefs: vec![],
..Meta::default()
};
let (exif, xmp) = resolve_metadata_items(&meta, 1);
assert!(exif.is_none());
assert!(xmp.is_none());
}
#[test]
fn resolve_metadata_only_targets_the_requested_item() {
let meta = Meta {
items: vec![make_item(2, b"Exif")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 2,
to_ids: vec![5], }],
..Meta::default()
};
let (exif, _) = resolve_metadata_items(&meta, 1);
assert!(exif.is_none());
let (exif5, _) = resolve_metadata_items(&meta, 5);
assert_eq!(exif5, Some(2));
}
#[test]
fn gain_map_metadata_unknown_id_is_invalid_data() {
let err = gain_map_metadata(FIXTURE, 9999).unwrap_err();
match err {
Error::InvalidData(msg) => {
assert!(
msg.contains("missing in iloc"),
"expected iloc-miss error message, got: {msg}"
);
}
other => panic!("expected InvalidData on unknown item id, got {other:?}"),
}
}
use crate::meta::{IlocExtent, ItemLocation};
use crate::region::RegionGeometry;
fn one_rect_region_payload() -> Vec<u8> {
let mut buf = vec![0u8, 0]; buf.extend_from_slice(&100u16.to_be_bytes()); buf.extend_from_slice(&80u16.to_be_bytes()); buf.push(1); buf.push(1); buf.extend_from_slice(&10i16.to_be_bytes());
buf.extend_from_slice(&12i16.to_be_bytes());
buf.extend_from_slice(&30u16.to_be_bytes());
buf.extend_from_slice(&40u16.to_be_bytes());
buf
}
#[test]
fn region_items_resolves_idat_backed_rgan() {
let payload = one_rect_region_payload();
let meta = Meta {
items: vec![make_item(2, b"rgan")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 2,
to_ids: vec![1],
}],
locations: vec![ItemLocation {
id: 2,
construction_method: 1, data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: payload.len() as u64,
extent_index: 0,
}],
}],
idat: Some(payload),
..Meta::default()
};
let items = region_items_for_meta(&[], &meta, 1).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].region_item_id, 2);
assert_eq!(items[0].image_item_id, 1);
assert_eq!(items[0].region.reference_width, 100);
assert_eq!(items[0].region.region_count(), 1);
assert_eq!(
items[0].region.regions[0],
RegionGeometry::Rectangle {
x: 10,
y: 12,
width: 30,
height: 40,
}
);
}
#[test]
fn region_items_skips_non_rgan_cdsc_sources() {
let meta = Meta {
items: vec![make_item(2, b"Exif")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
let items = region_items_for_meta(&[], &meta, 1).unwrap();
assert!(items.is_empty());
}
#[test]
fn region_items_are_target_scoped() {
let payload = one_rect_region_payload();
let plen = payload.len() as u64;
let meta = Meta {
items: vec![make_item(2, b"rgan")],
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 2,
to_ids: vec![5],
}],
locations: vec![ItemLocation {
id: 2,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: plen,
extent_index: 0,
}],
}],
idat: Some(payload),
..Meta::default()
};
assert!(region_items_for_meta(&[], &meta, 1).unwrap().is_empty());
assert_eq!(region_items_for_meta(&[], &meta, 5).unwrap().len(), 1);
}
#[test]
fn text_items_resolves_text_and_font_links() {
let meta = Meta {
items: vec![
make_mime_item(2, "text/html"),
make_mime_item(3, "font/ttf"),
],
irefs: vec![
IrefEntry {
reference_type: *b"text",
from_id: 2,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"font",
from_id: 2,
to_ids: vec![3],
},
],
..Meta::default()
};
let items = text_items_for_meta(&meta, 1);
assert_eq!(items.len(), 1);
assert_eq!(items[0].text_item_id, 2);
assert_eq!(items[0].image_item_id, 1);
assert_eq!(items[0].content_type, "text/html");
assert_eq!(items[0].font_item_ids, vec![3]);
}
#[test]
fn text_items_without_font_has_empty_font_list() {
let meta = Meta {
items: vec![make_mime_item(2, "text/plain")],
irefs: vec![IrefEntry {
reference_type: *b"text",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
let items = text_items_for_meta(&meta, 1);
assert_eq!(items.len(), 1);
assert_eq!(items[0].content_type, "text/plain");
assert!(items[0].font_item_ids.is_empty());
}
#[test]
fn text_items_are_target_scoped() {
let meta = Meta {
items: vec![make_mime_item(2, "text/html")],
irefs: vec![IrefEntry {
reference_type: *b"text",
from_id: 2,
to_ids: vec![9],
}],
..Meta::default()
};
assert!(text_items_for_meta(&meta, 1).is_empty());
assert_eq!(text_items_for_meta(&meta, 9).len(), 1);
}
#[test]
fn is_font_item_matches_font_mime_only() {
assert!(is_font_item(&make_mime_item(1, "font/ttf")));
assert!(is_font_item(&make_mime_item(1, "font/woff")));
assert!(is_font_item(&make_mime_item(1, "FONT/OTF"))); assert!(!is_font_item(&make_mime_item(1, "text/html")));
assert!(!is_font_item(&make_mime_item(1, "application/rdf+xml")));
assert!(!is_font_item(&make_item(1, b"av01")));
}
#[test]
fn coded_deps_detects_predictive_item_with_order() {
let meta = Meta {
irefs: vec![IrefEntry {
reference_type: *b"pred",
from_id: 7,
to_ids: vec![3, 5, 6], }],
..Meta::default()
};
let d = coded_item_dependencies_from_meta(&meta, 7);
assert!(d.is_predictively_coded());
assert!(d.has_dependencies());
assert_eq!(d.pred, vec![3, 5, 6]);
assert!(!d.is_pre_derived());
assert!(!coded_item_dependencies_from_meta(&meta, 3).has_dependencies());
}
#[test]
fn coded_deps_detects_pre_derived_base() {
let meta = Meta {
irefs: vec![IrefEntry {
reference_type: *b"base",
from_id: 1,
to_ids: vec![10, 11, 12],
}],
..Meta::default()
};
let d = coded_item_dependencies_from_meta(&meta, 1);
assert!(d.is_pre_derived());
assert_eq!(d.base, vec![10, 11, 12]);
assert!(!d.is_predictively_coded());
}
#[test]
fn coded_deps_surfaces_exbl_and_tbas() {
let meta = Meta {
irefs: vec![
IrefEntry {
reference_type: *b"exbl",
from_id: 4,
to_ids: vec![2],
},
IrefEntry {
reference_type: *b"tbas",
from_id: 4,
to_ids: vec![8],
},
],
..Meta::default()
};
let d = coded_item_dependencies_from_meta(&meta, 4);
assert_eq!(d.exbl, vec![2]);
assert_eq!(d.tbas, Some(8));
assert!(d.has_dependencies());
assert!(!d.is_predictively_coded() && !d.is_pre_derived());
}
#[test]
fn coded_deps_empty_for_independent_item() {
let meta = Meta {
irefs: vec![IrefEntry {
reference_type: *b"cdsc",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
let d = coded_item_dependencies_from_meta(&meta, 1);
assert_eq!(d, CodedItemDependencies::default());
assert!(!d.has_dependencies());
}
#[test]
fn self_referencing_overlay_with_repeated_inputs_inspects_fast() {
fn bx(t: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut o = ((8 + body.len()) as u32).to_be_bytes().to_vec();
o.extend_from_slice(t);
o.extend_from_slice(body);
o
}
fn fb(t: &[u8; 4], v: u8, flags: u32, body: &[u8]) -> Vec<u8> {
let mut i = vec![v, (flags >> 16) as u8, (flags >> 8) as u8, flags as u8];
i.extend_from_slice(body);
bx(t, &i)
}
let mut desc = vec![0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0];
desc.extend_from_slice(&[0u8; 12]);
let mut hdlr = vec![0u8; 4];
hdlr.extend_from_slice(b"pict");
hdlr.extend_from_slice(&[0u8; 13]);
let mut infe = vec![0, 1, 0, 0];
infe.extend_from_slice(b"iovl");
infe.push(0);
let mut iinf = vec![0, 1];
iinf.extend_from_slice(&fb(b"infe", 2, 0, &infe));
let dimg = bx(b"dimg", &[0, 1, 0, 3, 0, 1, 0, 1, 0, 1]);
let ispe = fb(b"ispe", 0, 0, &[0, 0, 0, 1, 0, 0, 0, 0]);
let mut iprp = bx(b"ipco", &ispe);
iprp.extend_from_slice(&fb(b"ipma", 0, 0, &[0, 0, 0, 1, 0, 1, 1, 1]));
let build = |start: u32| {
let mut iloc = vec![0x44, 0, 0, 1, 0, 1, 0, 0, 0, 1];
iloc.extend_from_slice(&start.to_be_bytes());
iloc.extend_from_slice(&(desc.len() as u32).to_be_bytes());
let mut body = Vec::new();
body.extend_from_slice(&fb(b"hdlr", 0, 0, &hdlr));
body.extend_from_slice(&fb(b"pitm", 0, 0, &[0, 1]));
body.extend_from_slice(&fb(b"iinf", 0, 0, &iinf));
body.extend_from_slice(&fb(b"iref", 0, 0, &dimg));
body.extend_from_slice(&bx(b"iprp", &iprp));
body.extend_from_slice(&fb(b"iloc", 0, 0, &iloc));
fb(b"meta", 0, 0, &body)
};
let mut ftyp = b"avif".to_vec();
ftyp.extend_from_slice(&[0, 0, 0, 0]);
ftyp.extend_from_slice(b"avifmif1miaf");
let ftyp = bx(b"ftyp", &ftyp);
let probe = build(0);
let meta = build((ftyp.len() + probe.len() + 8) as u32);
let mut file = ftyp;
file.extend_from_slice(&meta);
file.extend_from_slice(&bx(b"mdat", &desc));
let started = std::time::Instant::now();
let _ = inspect(&file);
assert!(
started.elapsed().as_secs() < 5,
"inspect took {:?}",
started.elapsed()
);
}
}