use crate::box_parser::{b, iter_boxes, parse_full_box, read_u16, read_u32, type_str, BoxType};
use crate::error::{AvifError as Error, Result};
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OverlayEntry {
pub horizontal_offset: i32,
pub vertical_offset: i32,
}
impl OverlayEntry {
#[allow(clippy::too_many_arguments)]
pub fn new(horizontal_offset: i32, vertical_offset: i32) -> Self {
Self {
horizontal_offset,
vertical_offset,
}
}
pub fn with_horizontal_offset(mut self, horizontal_offset: i32) -> Self {
self.horizontal_offset = horizontal_offset;
self
}
pub fn with_vertical_offset(mut self, vertical_offset: i32) -> Self {
self.vertical_offset = vertical_offset;
self
}
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageOverlay {
pub canvas_fill_value: [u16; 4],
pub output_width: u32,
pub output_height: u32,
pub entries: Vec<OverlayEntry>,
}
impl ImageOverlay {
#[allow(clippy::too_many_arguments)]
pub fn new(
canvas_fill_value: [u16; 4],
output_width: u32,
output_height: u32,
entries: Vec<OverlayEntry>,
) -> Self {
Self {
canvas_fill_value,
output_width,
output_height,
entries,
}
}
pub fn with_canvas_fill_value(mut self, canvas_fill_value: [u16; 4]) -> Self {
self.canvas_fill_value = canvas_fill_value;
self
}
pub fn with_output_width(mut self, output_width: u32) -> Self {
self.output_width = output_width;
self
}
pub fn with_output_height(mut self, output_height: u32) -> Self {
self.output_height = output_height;
self
}
pub fn with_entries(mut self, entries: Vec<OverlayEntry>) -> Self {
self.entries = entries;
self
}
}
impl ImageOverlay {
pub fn parse(payload: &[u8], reference_count: usize) -> Result<Self> {
match payload.first() {
None => return Err(Error::invalid("avif: iovl header too short")),
Some(0) => {}
Some(v) => return Err(Error::invalid(format!("avif: iovl version {v} != 0"))),
}
let desc = oxideav_heif::derived::OverlayDescriptor::parse(payload, reference_count)?;
Ok(ImageOverlay {
canvas_fill_value: desc.canvas_fill,
output_width: desc.output_width,
output_height: desc.output_height,
entries: desc
.offsets
.iter()
.map(|&(h, v)| OverlayEntry {
horizontal_offset: h,
vertical_offset: v,
})
.collect(),
})
}
pub(crate) fn descriptor(&self) -> oxideav_heif::derived::OverlayDescriptor {
oxideav_heif::derived::OverlayDescriptor::new(
self.canvas_fill_value,
self.output_width,
self.output_height,
self.entries
.iter()
.map(|e| (e.horizontal_offset, e.vertical_offset))
.collect(),
)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EntityGroup {
pub grouping_type: BoxType,
pub group_id: u32,
pub entity_ids: Vec<u32>,
pub flags: u32,
}
impl EntityGroup {
#[allow(clippy::too_many_arguments)]
pub fn new(grouping_type: BoxType, group_id: u32, entity_ids: Vec<u32>, flags: u32) -> Self {
Self {
grouping_type,
group_id,
entity_ids,
flags,
}
}
}
impl EntityGroup {
pub fn is_stereo_pair(&self) -> bool {
&self.grouping_type == b"ster"
}
pub fn is_alternates(&self) -> bool {
&self.grouping_type == b"altr"
}
pub fn is_equivalence(&self) -> bool {
&self.grouping_type == b"eqiv"
}
pub fn is_panorama(&self) -> bool {
&self.grouping_type == b"pano"
}
pub fn is_progressive(&self) -> bool {
&self.grouping_type == b"prgr"
}
pub fn is_burst(&self) -> bool {
&self.grouping_type == b"brst"
}
pub fn is_multi_source(&self) -> bool {
&self.grouping_type == b"msrc"
}
pub fn cardinality_ok(&self) -> bool {
match &self.grouping_type {
b"ster" | b"iaug" => self.entity_ids.len() == 2,
_ => true,
}
}
pub fn is_time_synchronized(&self) -> bool {
&self.grouping_type == b"tsyn"
}
pub fn is_audio_to_image(&self) -> bool {
&self.grouping_type == b"iaug"
}
pub fn audio_repeats(&self) -> bool {
self.is_audio_to_image() && (self.flags & 0x1) != 0
}
pub fn is_slideshow(&self) -> bool {
&self.grouping_type == b"slid"
}
pub fn is_album_collection(&self) -> bool {
&self.grouping_type == b"albc"
}
pub fn is_favorites_collection(&self) -> bool {
&self.grouping_type == b"favc"
}
pub fn is_user_collection(&self) -> bool {
self.is_album_collection() || self.is_favorites_collection()
}
pub fn is_bracketed_set(&self) -> bool {
self.bracketing_kind().is_some()
}
pub fn bracketing_kind(&self) -> Option<BracketingKind> {
BracketingKind::from_four_cc(&self.grouping_type)
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BracketingKind {
AutoExposure,
WhiteBalance,
Focus,
FlashExposure,
DepthOfField,
}
impl BracketingKind {
pub fn from_four_cc(fourcc: &[u8; 4]) -> Option<BracketingKind> {
match fourcc {
b"aebr" => Some(BracketingKind::AutoExposure),
b"wbbr" => Some(BracketingKind::WhiteBalance),
b"fobr" => Some(BracketingKind::Focus),
b"afbr" => Some(BracketingKind::FlashExposure),
b"dobr" => Some(BracketingKind::DepthOfField),
_ => None,
}
}
pub fn four_cc(&self) -> &'static [u8; 4] {
match self {
BracketingKind::AutoExposure => b"aebr",
BracketingKind::WhiteBalance => b"wbbr",
BracketingKind::Focus => b"fobr",
BracketingKind::FlashExposure => b"afbr",
BracketingKind::DepthOfField => b"dobr",
}
}
}
pub fn parse_grpl(payload: &[u8]) -> Result<Vec<EntityGroup>> {
let mut out = Vec::new();
for hdr in iter_boxes(payload) {
let hdr = hdr?;
let child = &payload[hdr.payload_start..hdr.end()];
let (_version, flags, body) = parse_full_box(child)?;
if body.len() < 8 {
return Err(Error::invalid(format!(
"avif: EntityToGroupBox '{}' body too short ({} < 8)",
type_str(&hdr.box_type),
body.len()
)));
}
let group_id = read_u32(body, 0)?;
let num_entities = read_u32(body, 4)? as usize;
let need = 8 + num_entities * 4;
if body.len() < need {
return Err(Error::invalid(format!(
"avif: EntityToGroupBox '{}' truncated entity list ({} < {need})",
type_str(&hdr.box_type),
body.len()
)));
}
let mut entity_ids = Vec::with_capacity(num_entities);
for i in 0..num_entities {
entity_ids.push(read_u32(body, 8 + i * 4)?);
}
out.push(EntityGroup {
grouping_type: hdr.box_type,
group_id,
entity_ids,
flags,
});
}
Ok(out)
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Mif1Compliance {
pub has_hdlr: bool,
pub has_pitm: bool,
pub has_iinf: bool,
pub has_iloc: bool,
pub has_iprp: bool,
pub infe_count: usize,
pub claims_mif1: bool,
}
impl Mif1Compliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
has_hdlr: bool,
has_pitm: bool,
has_iinf: bool,
has_iloc: bool,
has_iprp: bool,
infe_count: usize,
claims_mif1: bool,
) -> Self {
Self {
has_hdlr,
has_pitm,
has_iinf,
has_iloc,
has_iprp,
infe_count,
claims_mif1,
}
}
}
impl Mif1Compliance {
pub fn is_compliant(&self) -> bool {
self.has_hdlr
&& self.has_pitm
&& self.has_iinf
&& self.has_iloc
&& self.has_iprp
&& self.infe_count > 0
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if !self.has_hdlr {
out.push("hdlr");
}
if !self.has_pitm {
out.push("pitm");
}
if !self.has_iinf {
out.push("iinf");
}
if self.infe_count == 0 {
out.push("infe");
}
if !self.has_iloc {
out.push("iloc");
}
if !self.has_iprp {
out.push("iprp");
}
out
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Token {
Constant(i64),
Sample(u8),
Unary(u8),
Binary(u8),
Reserved(u8),
}
impl Token {
pub fn is_operand(&self) -> bool {
matches!(self, Token::Constant(_) | Token::Sample(_))
}
pub fn is_unary(&self) -> bool {
matches!(self, Token::Unary(_))
}
pub fn is_binary(&self) -> bool {
matches!(self, Token::Binary(_))
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SampleTransform {
pub version: u8,
pub bit_depth: u8,
pub tokens: Vec<Token>,
}
impl SampleTransform {
#[allow(clippy::too_many_arguments)]
pub fn new(version: u8, bit_depth: u8, tokens: Vec<Token>) -> Self {
Self {
version,
bit_depth,
tokens,
}
}
}
impl SampleTransform {
pub fn num_bits(&self) -> u32 {
bit_depth_to_num_bits(self.bit_depth)
}
pub fn min_value(&self) -> i64 {
let n = self.num_bits();
if n >= 64 {
i64::MIN
} else {
-(1i64 << (n - 1))
}
}
pub fn max_value(&self) -> i64 {
let n = self.num_bits();
if n >= 64 {
i64::MAX
} else {
(1i64 << (n - 1)) - 1
}
}
pub fn parse(payload: &[u8], reference_count: u32) -> Result<Self> {
let st = Self::parse_relaxed(payload)?;
if st.version != 0 {
return Err(Error::invalid(format!(
"avif: sato version {} != 0",
st.version
)));
}
if st.bit_depth > 3 {
return Err(Error::invalid(format!(
"avif: sato bit_depth {} out of 0..=3",
st.bit_depth
)));
}
for (i, t) in st.tokens.iter().enumerate() {
if let Token::Reserved(raw) = *t {
return Err(Error::invalid(format!(
"avif: sato token[{i}] = {raw} is reserved"
)));
}
}
for (i, t) in st.tokens.iter().enumerate() {
if let Token::Sample(n) = *t {
if u32::from(n) > reference_count {
return Err(Error::invalid(format!(
"avif: sato token[{i}] sample index {n} > reference_count {reference_count}"
)));
}
}
}
let mut depth: i64 = 0;
for (i, t) in st.tokens.iter().enumerate() {
match t {
Token::Constant(_) | Token::Sample(_) => depth += 1,
Token::Unary(_) => {
if depth < 1 {
return Err(Error::invalid(format!(
"avif: sato token[{i}] unary op underflows stack"
)));
}
}
Token::Binary(_) => {
if depth < 2 {
return Err(Error::invalid(format!(
"avif: sato token[{i}] binary op underflows stack"
)));
}
depth -= 1; }
Token::Reserved(_) => unreachable!(),
}
}
if depth != 1 {
return Err(Error::invalid(format!(
"avif: sato expression leaves {depth} elements on stack, expected 1"
)));
}
Ok(st)
}
pub fn parse_relaxed(payload: &[u8]) -> Result<Self> {
if payload.len() < 2 {
return Err(Error::invalid("avif: sato header too short"));
}
let header = payload[0];
let version = (header >> 6) & 0x03;
let bit_depth = header & 0x03;
let token_count = payload[1] as usize;
if token_count == 0 {
return Err(Error::invalid("avif: sato token_count = 0"));
}
let num_bits = bit_depth_to_num_bits(bit_depth);
let const_bytes = (num_bits / 8) as usize; let mut tokens = Vec::with_capacity(token_count);
let mut cursor = 2usize;
for i in 0..token_count {
if cursor >= payload.len() {
return Err(Error::invalid(format!(
"avif: sato truncated before token {i} (cursor {cursor} >= {})",
payload.len()
)));
}
let raw = payload[cursor];
cursor += 1;
let token = if raw == 0 {
let end = cursor
.checked_add(const_bytes)
.ok_or_else(|| Error::invalid("avif: sato constant offset overflow"))?;
if end > payload.len() {
return Err(Error::invalid(format!(
"avif: sato truncated constant payload for token {i} (need {const_bytes} bytes at {cursor})"
)));
}
let value = read_sint_be(&payload[cursor..end])?;
cursor = end;
Token::Constant(value)
} else if raw <= 32 {
Token::Sample(raw)
} else if (64..=67).contains(&raw) {
Token::Unary(raw)
} else if (128..=137).contains(&raw) {
Token::Binary(raw)
} else {
Token::Reserved(raw)
};
tokens.push(token);
}
Ok(SampleTransform {
version,
bit_depth,
tokens,
})
}
pub fn evaluate(&self, inputs: &[i64]) -> Result<i64> {
let min = self.min_value();
let max = self.max_value();
let clamp = |v: i64| v.clamp(min, max);
let mut stack: Vec<i64> = Vec::with_capacity(self.tokens.len());
for (i, t) in self.tokens.iter().enumerate() {
match t {
Token::Constant(value) => stack.push(clamp(*value)),
Token::Sample(n) => {
let idx = usize::from(*n).saturating_sub(1);
let v = *inputs.get(idx).ok_or_else(|| {
Error::invalid(format!(
"avif: sato token[{i}] sample {n} out of range (inputs.len()={})",
inputs.len()
))
})?;
stack.push(clamp(v));
}
Token::Unary(raw) => {
let l = stack
.pop()
.ok_or_else(|| Error::invalid("avif: sato unary stack underflow"))?;
let r = match *raw {
64 => l.checked_neg().unwrap_or(i64::MAX), 65 => l.checked_abs().unwrap_or(i64::MAX), 66 => !l, 67 => {
if l > 0 {
(63 - (l as u64).leading_zeros()) as i64
} else {
0
}
}
_ => {
return Err(Error::invalid(format!(
"avif: sato unary token {raw} not implemented",
)));
}
};
stack.push(clamp(r));
}
Token::Binary(raw) => {
let r = stack
.pop()
.ok_or_else(|| Error::invalid("avif: sato binary right underflow"))?;
let l = stack
.pop()
.ok_or_else(|| Error::invalid("avif: sato binary left underflow"))?;
let out = match *raw {
128 => l.saturating_add(r),
129 => l.saturating_sub(r),
130 => l.saturating_mul(r),
131 => {
if r == 0 {
l
} else {
l.checked_div(r).unwrap_or(i64::MAX)
}
}
132 => l & r,
133 => l | r,
134 => l ^ r,
135 => {
if l == 0 {
0
} else {
pow_truncated(l, r)
}
}
136 => l.min(r),
137 => l.max(r),
_ => {
return Err(Error::invalid(format!(
"avif: sato binary token {raw} not implemented",
)));
}
};
stack.push(clamp(out));
}
Token::Reserved(raw) => {
return Err(Error::invalid(format!(
"avif: sato token[{i}] = {raw} reserved (cannot evaluate)",
)));
}
}
}
if stack.len() != 1 {
return Err(Error::invalid(format!(
"avif: sato expression leaves {} elements on stack, expected 1",
stack.len()
)));
}
Ok(stack[0])
}
}
fn bit_depth_to_num_bits(bit_depth: u8) -> u32 {
match bit_depth & 0x03 {
0 => 8,
1 => 16,
2 => 32,
3 => 64,
_ => unreachable!(),
}
}
fn read_sint_be(bytes: &[u8]) -> Result<i64> {
Ok(match bytes.len() {
1 => bytes[0] as i8 as i64,
2 => i16::from_be_bytes([bytes[0], bytes[1]]) as i64,
4 => i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64,
8 => i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]),
n => {
return Err(Error::invalid(format!(
"avif: sato constant width {n} not in {{1,2,4,8}}"
)))
}
})
}
fn pow_truncated(base: i64, exp: i64) -> i64 {
if exp < 0 {
return match base.abs() {
1 => {
if base == 1 || exp % 2 == 0 {
1
} else {
-1
}
}
_ => 0,
};
}
if exp == 0 {
return 1;
}
let mut result: i64 = 1;
let mut base_acc = base;
let mut e = exp as u64;
while e > 0 {
if e & 1 == 1 {
result = match result.checked_mul(base_acc) {
Some(v) => v,
None => {
return if (result >= 0) == (base_acc >= 0) {
i64::MAX
} else {
i64::MIN
};
}
};
}
e >>= 1;
if e > 0 {
base_acc = match base_acc.checked_mul(base_acc) {
Some(v) => v,
None => {
return if base_acc >= 0 || (exp & 1 == 0) {
i64::MAX
} else {
i64::MIN
};
}
};
}
}
result
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ToneMapCompliance {
pub tmap_item_id: u32,
pub base_item_id: Option<u32>,
pub gain_map_item_ids: Vec<u32>,
pub paired_in_altr: bool,
pub gain_maps_hidden: bool,
}
impl ToneMapCompliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
tmap_item_id: u32,
base_item_id: Option<u32>,
gain_map_item_ids: Vec<u32>,
paired_in_altr: bool,
gain_maps_hidden: bool,
) -> Self {
Self {
tmap_item_id,
base_item_id,
gain_map_item_ids,
paired_in_altr,
gain_maps_hidden,
}
}
}
impl ToneMapCompliance {
pub fn is_compliant(&self) -> bool {
self.paired_in_altr && self.gain_maps_hidden
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if !self.paired_in_altr {
out.push("altr-pairs-base-and-tmap");
}
if !self.gain_maps_hidden {
out.push("gain-map-hidden");
}
out
}
}
pub fn audit_tone_map(meta: &crate::meta::Meta) -> Vec<ToneMapCompliance> {
let tmap_type = crate::meta::ITEM_TYPE_TMAP;
let dimg = b(b"dimg");
let groups = meta.groups().unwrap_or_default();
meta.item_ids_of_type(&tmap_type)
.into_iter()
.map(|tmap_id| audit_one_tone_map(meta, &groups, &dimg, tmap_id))
.collect()
}
fn audit_one_tone_map(
meta: &crate::meta::Meta,
groups: &[EntityGroup],
dimg: &BoxType,
tmap_id: u32,
) -> ToneMapCompliance {
let inputs = meta.iref_targets(dimg, tmap_id);
let base_item_id = inputs.first().copied();
let gain_map_item_ids: Vec<u32> = inputs.iter().skip(1).copied().collect();
let paired_in_altr = match base_item_id {
Some(base) => groups.iter().any(|g| {
g.is_alternates() && g.entity_ids.contains(&tmap_id) && g.entity_ids.contains(&base)
}),
None => false,
};
let gain_maps_hidden = gain_map_item_ids
.iter()
.all(|id| meta.item_by_id(*id).is_some_and(|info| info.is_hidden()));
ToneMapCompliance {
tmap_item_id: tmap_id,
base_item_id,
gain_map_item_ids,
paired_in_altr,
gain_maps_hidden,
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct GainMapRational {
pub numerator: i32,
pub denominator: u32,
}
impl GainMapRational {
#[allow(clippy::too_many_arguments)]
pub fn new(numerator: i32, denominator: u32) -> Self {
Self {
numerator,
denominator,
}
}
}
impl GainMapRational {
pub fn as_f64(&self) -> f64 {
self.numerator as f64 / self.denominator as f64
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct GainMapChannel {
pub gain_map_min: GainMapRational,
pub gain_map_max: GainMapRational,
pub gamma: GainMapRational,
pub base_offset: GainMapRational,
pub alternate_offset: GainMapRational,
}
impl GainMapChannel {
#[allow(clippy::too_many_arguments)]
pub fn new(
gain_map_min: GainMapRational,
gain_map_max: GainMapRational,
gamma: GainMapRational,
base_offset: GainMapRational,
alternate_offset: GainMapRational,
) -> Self {
Self {
gain_map_min,
gain_map_max,
gamma,
base_offset,
alternate_offset,
}
}
}
impl GainMapChannel {
pub fn unnormalize_log2_gain(&self, normalized: f64) -> f64 {
let min = self.gain_map_min.as_f64();
let max = self.gain_map_max.as_f64();
let gamma = self.gamma.as_f64();
let g_norm = normalized.clamp(0.0, 1.0);
let degammad = g_norm.powf(1.0 / gamma);
(max - min) * degammad + min
}
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct GainMapMetadata {
pub minimum_version: u16,
pub writer_version: u16,
pub is_multichannel: bool,
pub use_base_colour_space: bool,
pub base_hdr_headroom: GainMapRational,
pub alternate_hdr_headroom: GainMapRational,
pub channels: Vec<GainMapChannel>,
}
impl GainMapMetadata {
#[allow(clippy::too_many_arguments)]
pub fn new(
minimum_version: u16,
writer_version: u16,
is_multichannel: bool,
use_base_colour_space: bool,
base_hdr_headroom: GainMapRational,
alternate_hdr_headroom: GainMapRational,
channels: Vec<GainMapChannel>,
) -> Self {
Self {
minimum_version,
writer_version,
is_multichannel,
use_base_colour_space,
base_hdr_headroom,
alternate_hdr_headroom,
channels,
}
}
}
impl GainMapMetadata {
pub fn parse_tone_map_image(body: &[u8]) -> Result<Self> {
match body.split_first() {
Some((0, metadata)) => Self::parse(metadata),
Some((version, _)) => Err(Error::unsupported(format!(
"avif: ToneMapImage version {version} — readers shall not process a version other \
than 0 (HEIF Amd 1:2025 §6.6.2.4.3); display the base image"
))),
None => Err(Error::invalid("avif: empty tmap item body")),
}
}
pub fn parse(payload: &[u8]) -> Result<Self> {
let minimum_version = read_u16(payload, 0)?;
let writer_version = read_u16(payload, 2)?;
if minimum_version != 0 {
return Err(Error::unsupported(format!(
"avif: gain map metadata minimum_version {minimum_version} not understood \
(ISO 21496-1 C.2.3 requires 0); caller should display the base image"
)));
}
if writer_version < minimum_version {
return Err(Error::invalid(format!(
"avif: gain map metadata writer_version {writer_version} < minimum_version \
{minimum_version} (ISO 21496-1 C.2.3)"
)));
}
let flags = *payload
.get(4)
.ok_or_else(|| Error::invalid("avif: gain map metadata truncated at flags byte"))?;
let is_multichannel = flags & 0x80 != 0;
let use_base_colour_space = flags & 0x40 != 0;
let channel_count = if is_multichannel { 3 } else { 1 };
let base_hdr_headroom = read_unsigned_rational(payload, 5)?;
let alternate_hdr_headroom = read_unsigned_rational(payload, 13)?;
if !rationals_differ(&base_hdr_headroom, &alternate_hdr_headroom) {
return Err(Error::invalid(
"avif: gain map metadata alternate_hdr_headroom equals base_hdr_headroom \
(ISO 21496-1 §5.2.7)",
));
}
let mut channels = Vec::with_capacity(channel_count);
let mut at = 21usize;
for _ in 0..channel_count {
let gain_map_min = read_signed_rational(payload, at)?;
let gain_map_max = read_signed_rational(payload, at + 8)?;
if !rational_ge(&gain_map_max, &gain_map_min) {
return Err(Error::invalid(
"avif: gain map metadata gain_map_max < gain_map_min \
(ISO 21496-1 §5.2.5.3)",
));
}
let gamma = read_unsigned_rational(payload, at + 16)?;
if gamma.numerator == 0 {
return Err(Error::invalid(
"avif: gain map metadata gamma_numerator is 0 (ISO 21496-1 C.2.3)",
));
}
let base_offset = read_signed_rational(payload, at + 24)?;
let alternate_offset = read_signed_rational(payload, at + 32)?;
channels.push(GainMapChannel {
gain_map_min,
gain_map_max,
gamma,
base_offset,
alternate_offset,
});
at += 40;
}
Ok(GainMapMetadata {
minimum_version,
writer_version,
is_multichannel,
use_base_colour_space,
base_hdr_headroom,
alternate_hdr_headroom,
channels,
})
}
pub fn channel_count(&self) -> usize {
self.channels.len()
}
pub fn weight_factor(&self, target_headroom: f64) -> f64 {
let h_base = self.base_hdr_headroom.as_f64();
let h_alt = self.alternate_hdr_headroom.as_f64();
let span = h_alt - h_base;
let frac = ((target_headroom - h_base) / span).clamp(0.0, 1.0);
span.signum() * frac
}
fn channel_for(&self, component: usize) -> Option<&GainMapChannel> {
if component > 2 {
return None;
}
match self.channels.len() {
1 => self.channels.first(),
_ => self.channels.get(component),
}
}
pub fn apply_component(
&self,
baseline: f64,
normalized: f64,
target_headroom: f64,
component: usize,
) -> Option<f64> {
let ch = self.channel_for(component)?;
let g = ch.unnormalize_log2_gain(normalized);
let w = self.weight_factor(target_headroom);
let k_base = ch.base_offset.as_f64();
let k_alt = ch.alternate_offset.as_f64();
Some((baseline + k_base) * (w * g).exp2() - k_alt)
}
pub fn apply_rgb(
&self,
baseline: [f64; 3],
gain: [f64; 3],
target_headroom: f64,
) -> Option<[f64; 3]> {
Some([
self.apply_component(baseline[0], gain[0], target_headroom, 0)?,
self.apply_component(baseline[1], gain[1], target_headroom, 1)?,
self.apply_component(baseline[2], gain[2], target_headroom, 2)?,
])
}
pub fn apply_plane_rgb(
&self,
baseline: &[f64],
gain: &[f64],
width: u32,
height: u32,
target_headroom: f64,
) -> Result<Vec<f64>> {
if self.channels.is_empty() {
return Err(Error::invalid(
"avif: gain map metadata has no channel records to apply",
));
}
let px = (width as usize)
.checked_mul(height as usize)
.ok_or_else(|| Error::invalid("avif: gain map plane dimensions overflow usize"))?;
let rgb_len = px
.checked_mul(3)
.ok_or_else(|| Error::invalid("avif: gain map plane dimensions overflow usize"))?;
if baseline.len() != rgb_len {
return Err(Error::invalid(format!(
"avif: baseline plane length {} != width*height*3 = {rgb_len}",
baseline.len()
)));
}
let achromatic = match gain.len() {
n if n == px => true,
n if n == rgb_len => false,
other => {
return Err(Error::invalid(format!(
"avif: gain plane length {other} is neither width*height = {px} \
(achromatic) nor width*height*3 = {rgb_len} (RGB)"
)));
}
};
let w = self.weight_factor(target_headroom);
let mut out = vec![0.0f64; rgb_len];
for i in 0..px {
for c in 0..3 {
let base = baseline[i * 3 + c];
let g_sample = if achromatic { gain[i] } else { gain[i * 3 + c] };
let ch = self
.channel_for(c)
.expect("channel_for is Some for c<3 with non-empty channels");
let g = ch.unnormalize_log2_gain(g_sample);
let k_base = ch.base_offset.as_f64();
let k_alt = ch.alternate_offset.as_f64();
out[i * 3 + c] = (base + k_base) * (w * g).exp2() - k_alt;
}
}
Ok(out)
}
pub fn apply_plane_rgb_coded(
&self,
baseline: &[f64],
gain_coded: &[u16],
gain_bit_depth: u8,
width: u32,
height: u32,
target_headroom: f64,
) -> Result<Vec<f64>> {
let gain = normalize_full_range_plane(gain_coded, gain_bit_depth)?;
self.apply_plane_rgb(baseline, &gain, width, height, target_headroom)
}
}
pub fn normalize_full_range_plane(samples: &[u16], bit_depth: u8) -> Result<Vec<f64>> {
if !matches!(bit_depth, 8 | 10 | 12 | 16) {
return Err(Error::invalid(format!(
"avif: bit depth {bit_depth} is not one of 8/10/12/16"
)));
}
let max = ((1u32 << bit_depth) - 1) as f64;
let ceil = 1u32 << bit_depth;
let mut out = Vec::with_capacity(samples.len());
for (i, &s) in samples.iter().enumerate() {
if (s as u32) >= ceil && bit_depth < 16 {
return Err(Error::invalid(format!(
"avif: sample {i} = {s} exceeds the {bit_depth}-bit code range"
)));
}
out.push(s as f64 / max);
}
Ok(out)
}
fn read_signed_rational(buf: &[u8], at: usize) -> Result<GainMapRational> {
let numerator = read_u32(buf, at)? as i32;
let denominator = read_u32(buf, at + 4)?;
if denominator == 0 {
return Err(Error::invalid(
"avif: gain map metadata rational denominator is 0 (ISO 21496-1 C.2.3)",
));
}
Ok(GainMapRational {
numerator,
denominator,
})
}
fn read_unsigned_rational(buf: &[u8], at: usize) -> Result<GainMapRational> {
read_signed_rational(buf, at)
}
fn rational_ge(a: &GainMapRational, b: &GainMapRational) -> bool {
let lhs = (a.numerator as i64) * (b.denominator as i64);
let rhs = (b.numerator as i64) * (a.denominator as i64);
lhs >= rhs
}
fn rationals_differ(a: &GainMapRational, b: &GainMapRational) -> bool {
let lhs = (a.numerator as i64) * (b.denominator as i64);
let rhs = (b.numerator as i64) * (a.denominator as i64);
lhs != rhs
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct GridDerivationAudit {
pub grid_item_id: u32,
pub tile_item_ids: Vec<u32>,
pub offenders: Vec<(u32, BoxType)>,
}
impl GridDerivationAudit {
#[allow(clippy::too_many_arguments)]
pub fn new(grid_item_id: u32, tile_item_ids: Vec<u32>, offenders: Vec<(u32, BoxType)>) -> Self {
Self {
grid_item_id,
tile_item_ids,
offenders,
}
}
}
impl GridDerivationAudit {
pub fn is_compliant(&self) -> bool {
self.offenders.is_empty()
}
pub fn offending_tile_ids(&self) -> Vec<u32> {
let mut out: Vec<u32> = self.offenders.iter().map(|(id, _)| *id).collect();
out.sort_unstable();
out.dedup();
out
}
}
pub fn audit_grid_derivations(meta: &crate::meta::Meta) -> Vec<GridDerivationAudit> {
let grid_type = crate::parser::ITEM_TYPE_GRID;
let dimg = b(b"dimg");
let irot = b(b"irot");
let imir = b(b"imir");
let clap = b(b"clap");
let iscl = b(b"iscl");
meta.item_ids_of_type(&grid_type)
.into_iter()
.map(|grid_id| {
let tile_item_ids = meta.iref_targets(&dimg, grid_id);
let mut offenders = Vec::new();
for tile_id in &tile_item_ids {
for kind in [clap, irot, imir, iscl] {
if meta.property_for(*tile_id, &kind).is_some() {
offenders.push((*tile_id, kind));
}
}
}
GridDerivationAudit {
grid_item_id: grid_id,
tile_item_ids,
offenders,
}
})
.collect()
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct IdenCompliance {
pub iden_item_id: u32,
pub dimg_reference_count: usize,
pub dimg_iref_count: usize,
pub has_item_body: bool,
pub source_item_id: Option<u32>,
}
impl IdenCompliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
iden_item_id: u32,
dimg_reference_count: usize,
dimg_iref_count: usize,
has_item_body: bool,
source_item_id: Option<u32>,
) -> Self {
Self {
iden_item_id,
dimg_reference_count,
dimg_iref_count,
has_item_body,
source_item_id,
}
}
}
impl IdenCompliance {
pub fn is_compliant(&self) -> bool {
self.dimg_reference_count == 1 && self.dimg_iref_count == 1 && !self.has_item_body
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if self.dimg_reference_count != 1 {
out.push("dimg-reference-count-eq-1");
}
if self.dimg_iref_count != 1 {
out.push("dimg-iref-count-eq-1");
}
if self.has_item_body {
out.push("no-item-body");
}
out
}
}
pub fn audit_iden_derivations(meta: &crate::meta::Meta) -> Vec<IdenCompliance> {
let iden_type = crate::meta::ITEM_TYPE_IDEN;
let dimg = b(b"dimg");
meta.item_ids_of_type(&iden_type)
.into_iter()
.map(|iden_id| audit_one_iden(meta, &dimg, iden_id))
.collect()
}
fn audit_one_iden(meta: &crate::meta::Meta, dimg: &BoxType, iden_id: u32) -> IdenCompliance {
let dimg_iref_count = meta
.irefs
.iter()
.filter(|e| &e.reference_type == dimg && e.from_id == iden_id)
.count();
let inputs = meta.iref_targets(dimg, iden_id);
let dimg_reference_count = inputs.len();
let source_item_id = if dimg_reference_count == 1 {
Some(inputs[0])
} else {
None
};
let has_item_body = meta
.location_by_id(iden_id)
.map(|loc| loc.extents.iter().any(|x| x.length > 0))
.unwrap_or(false);
IdenCompliance {
iden_item_id: iden_id,
dimg_reference_count,
dimg_iref_count,
has_item_body,
source_item_id,
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PredBrandCompliance {
pub claims_pred: bool,
pub claims_mif1: bool,
pub predictive_item_count: usize,
pub missing_dependency_ids: Vec<u32>,
pub primary_not_independent: bool,
}
impl PredBrandCompliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
claims_pred: bool,
claims_mif1: bool,
predictive_item_count: usize,
missing_dependency_ids: Vec<u32>,
primary_not_independent: bool,
) -> Self {
Self {
claims_pred,
claims_mif1,
predictive_item_count,
missing_dependency_ids,
primary_not_independent,
}
}
}
impl PredBrandCompliance {
pub fn is_compliant(&self) -> bool {
if !self.claims_pred {
return true;
}
self.missing_dependency_ids.is_empty() && !self.primary_not_independent
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if !self.claims_pred {
return out;
}
if !self.missing_dependency_ids.is_empty() {
out.push("pred-dependency-closure");
}
if self.primary_not_independent {
out.push("mif1-primary-independent");
}
out
}
}
pub fn audit_pred_brand(meta: &crate::meta::Meta, brands: &[BoxType]) -> PredBrandCompliance {
let pred = b(b"pred");
let mif1 = b(b"mif1");
let pred_brand = b(b"pred");
let mut out = PredBrandCompliance {
claims_pred: brands.contains(&pred_brand),
claims_mif1: brands.contains(&mif1),
..PredBrandCompliance::default()
};
let present: std::collections::BTreeSet<u32> = meta.items.iter().map(|i| i.id).collect();
let mut missing: Vec<u32> = Vec::new();
for item in &meta.items {
let deps = meta.iref_targets_of(&pred, item.id);
if deps.is_empty() {
continue;
}
out.predictive_item_count += 1;
for dep in deps {
if !present.contains(&dep) && !missing.contains(&dep) {
missing.push(dep);
}
}
}
out.missing_dependency_ids = missing;
if out.claims_mif1 {
if let Some(primary) = meta.primary_item_id {
out.primary_not_independent = !meta.iref_targets_of(&pred, primary).is_empty();
}
}
out
}
fn decode_av1c_bit_depth(av1c: &[u8]) -> Option<u8> {
if av1c.len() < 3 {
return None;
}
let b2 = av1c[2];
let high_bitdepth = ((b2 >> 6) & 1) != 0;
let twelve_bit = ((b2 >> 5) & 1) != 0;
Some(if twelve_bit {
12
} else if high_bitdepth {
10
} else {
8
})
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct AlphaBitDepthAudit {
pub alpha_item_id: u32,
pub master_item_id: u32,
pub alpha_bit_depth: Option<u8>,
pub master_bit_depth: Option<u8>,
pub alpha_missing_av1c: bool,
pub master_missing_av1c: bool,
}
impl AlphaBitDepthAudit {
#[allow(clippy::too_many_arguments)]
pub fn new(
alpha_item_id: u32,
master_item_id: u32,
alpha_bit_depth: Option<u8>,
master_bit_depth: Option<u8>,
alpha_missing_av1c: bool,
master_missing_av1c: bool,
) -> Self {
Self {
alpha_item_id,
master_item_id,
alpha_bit_depth,
master_bit_depth,
alpha_missing_av1c,
master_missing_av1c,
}
}
}
impl AlphaBitDepthAudit {
pub fn is_compliant(&self) -> bool {
match (self.alpha_bit_depth, self.master_bit_depth) {
(Some(a), Some(m)) => a == m,
_ => false,
}
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if self.alpha_missing_av1c {
out.push("alpha-item-missing-av1C");
} else if self.alpha_bit_depth.is_none() {
out.push("alpha-item-av1C-truncated");
}
if self.master_missing_av1c {
out.push("master-item-missing-av1C");
} else if self.master_bit_depth.is_none() {
out.push("master-item-av1C-truncated");
}
if let (Some(a), Some(m)) = (self.alpha_bit_depth, self.master_bit_depth) {
if a != m {
out.push("alpha-master-bit-depth-mismatch");
}
}
out
}
}
pub fn audit_alpha_bit_depth(meta: &crate::meta::Meta) -> Vec<AlphaBitDepthAudit> {
let auxl = b(b"auxl");
let auxc = b(b"auxC");
let mut out = Vec::new();
for entry in &meta.irefs {
if entry.reference_type != auxl {
continue;
}
let alpha_id = entry.from_id;
let is_alpha = match meta.property_for(alpha_id, &auxc) {
Some(crate::meta::Property::AuxC(aux)) => {
aux.aux_type.starts_with(crate::alpha::ALPHA_URN_PREFIX)
}
_ => false,
};
if !is_alpha {
continue;
}
let (alpha_bit_depth, alpha_missing_av1c) = match effective_av1c(meta, alpha_id, 0) {
Some(bytes) => (decode_av1c_bit_depth(bytes), false),
None => (None, true),
};
for &master_id in &entry.to_ids {
let (master_bit_depth, master_missing_av1c) = match effective_av1c(meta, master_id, 0) {
Some(bytes) => (decode_av1c_bit_depth(bytes), false),
None => (None, true),
};
out.push(AlphaBitDepthAudit {
alpha_item_id: alpha_id,
master_item_id: master_id,
alpha_bit_depth,
master_bit_depth,
alpha_missing_av1c,
master_missing_av1c,
});
}
}
out
}
fn effective_av1c(meta: &crate::meta::Meta, item_id: u32, depth: u32) -> Option<&[u8]> {
if depth > MAX_DERIVATION_DEPTH {
return None;
}
if let Some(crate::meta::Property::Av1C(bytes)) = meta.property_for(item_id, &b(b"av1C")) {
return Some(bytes);
}
let inputs = meta.iref_targets(&b(b"dimg"), item_id);
let &first = inputs.first()?;
if first == item_id {
return None;
}
effective_av1c(meta, first, depth + 1)
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct SequenceHeaderObuAudit {
pub item_id: u32,
pub sequence_header_count: u32,
pub total_obu_count: u32,
pub missing_iloc: bool,
pub truncated_obu: bool,
pub has_size_field_zero: bool,
}
impl SequenceHeaderObuAudit {
#[allow(clippy::too_many_arguments)]
pub fn new(
item_id: u32,
sequence_header_count: u32,
total_obu_count: u32,
missing_iloc: bool,
truncated_obu: bool,
has_size_field_zero: bool,
) -> Self {
Self {
item_id,
sequence_header_count,
total_obu_count,
missing_iloc,
truncated_obu,
has_size_field_zero,
}
}
}
impl SequenceHeaderObuAudit {
pub fn is_compliant(&self) -> bool {
!self.missing_iloc
&& !self.truncated_obu
&& !self.has_size_field_zero
&& self.sequence_header_count == 1
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if self.missing_iloc {
out.push("av01-item-missing-iloc");
}
if self.has_size_field_zero {
out.push("av01-item-obu-has-size-field-zero");
}
if self.truncated_obu {
out.push("av01-item-obu-stream-truncated");
}
if !self.missing_iloc && !self.has_size_field_zero && !self.truncated_obu {
match self.sequence_header_count {
0 => out.push("av01-item-missing-sequence-header-obu"),
1 => {} _ => out.push("av01-item-multiple-sequence-header-obus"),
}
}
out
}
}
const AV1_OBU_TYPE_SEQUENCE_HEADER: u8 = 1;
fn read_leb128(bytes: &[u8]) -> core::result::Result<(u32, usize), ()> {
let mut value: u64 = 0;
for i in 0..8 {
if i >= bytes.len() {
return Err(());
}
let byte = bytes[i];
value |= u64::from(byte & 0x7f) << (i * 7);
if byte & 0x80 == 0 {
if value > u64::from(u32::MAX) {
return Err(());
}
return Ok((value as u32, i + 1));
}
}
Err(())
}
fn walk_obu_stream(item_id: u32, payload: &[u8]) -> SequenceHeaderObuAudit {
let mut out = SequenceHeaderObuAudit {
item_id,
..SequenceHeaderObuAudit::default()
};
let mut cursor = 0usize;
while cursor < payload.len() {
let header = payload[cursor];
cursor += 1;
let obu_type = (header >> 3) & 0x0f;
let obu_extension_flag = (header >> 2) & 0x01;
let obu_has_size_field = (header >> 1) & 0x01;
if obu_extension_flag == 1 {
if cursor >= payload.len() {
out.truncated_obu = true;
return out;
}
cursor += 1;
}
if obu_has_size_field == 0 {
out.has_size_field_zero = true;
if obu_type == AV1_OBU_TYPE_SEQUENCE_HEADER {
out.sequence_header_count = out.sequence_header_count.saturating_add(1);
}
out.total_obu_count = out.total_obu_count.saturating_add(1);
return out;
}
let (obu_size, leb_len) = match read_leb128(&payload[cursor..]) {
Ok(v) => v,
Err(()) => {
out.truncated_obu = true;
return out;
}
};
cursor += leb_len;
let obu_size = obu_size as usize;
if cursor
.checked_add(obu_size)
.map(|end| end > payload.len())
.unwrap_or(true)
{
out.truncated_obu = true;
if obu_type == AV1_OBU_TYPE_SEQUENCE_HEADER {
out.sequence_header_count = out.sequence_header_count.saturating_add(1);
}
out.total_obu_count = out.total_obu_count.saturating_add(1);
return out;
}
cursor += obu_size;
if obu_type == AV1_OBU_TYPE_SEQUENCE_HEADER {
out.sequence_header_count = out.sequence_header_count.saturating_add(1);
}
out.total_obu_count = out.total_obu_count.saturating_add(1);
}
out
}
pub fn audit_sequence_header_obu(
meta: &crate::meta::Meta,
file: &[u8],
) -> Vec<SequenceHeaderObuAudit> {
let av01 = b(b"av01");
let mut out = Vec::new();
for item_id in meta.item_ids_of_type(&av01) {
let Some(loc) = meta.location_by_id(item_id) else {
out.push(SequenceHeaderObuAudit {
item_id,
missing_iloc: true,
..SequenceHeaderObuAudit::default()
});
continue;
};
let Ok(payload) = crate::parser::item_bytes(file, loc) else {
out.push(SequenceHeaderObuAudit {
item_id,
missing_iloc: true,
..SequenceHeaderObuAudit::default()
});
continue;
};
out.push(walk_obu_stream(item_id, payload));
}
out
}
pub(crate) fn decode_av1c_seq_level_idx_0(av1c: &[u8]) -> Option<u8> {
if av1c.len() < 2 {
return None;
}
Some(av1c[1] & 0x1F)
}
pub(crate) fn decode_av1c_seq_profile(av1c: &[u8]) -> Option<u8> {
if av1c.len() < 2 {
return None;
}
Some((av1c[1] >> 5) & 0x7)
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AvifProfile {
Baseline,
Advanced,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AvifProfileCompliance {
pub profile: AvifProfile,
pub item_id: u32,
pub seq_profile: Option<u8>,
pub seq_level_idx_0: Option<u8>,
pub missing_av1c: bool,
}
impl AvifProfileCompliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
profile: AvifProfile,
item_id: u32,
seq_profile: Option<u8>,
seq_level_idx_0: Option<u8>,
missing_av1c: bool,
) -> Self {
Self {
profile,
item_id,
seq_profile,
seq_level_idx_0,
missing_av1c,
}
}
}
impl AvifProfileCompliance {
pub fn is_compliant(&self) -> bool {
match (self.seq_profile, self.seq_level_idx_0) {
(Some(p), Some(l)) => match self.profile {
AvifProfile::Baseline => p == 0 && l <= 13,
AvifProfile::Advanced => p <= 1 && l <= 16,
},
_ => false,
}
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if self.missing_av1c {
out.push("item-missing-av1C");
return out;
}
if self.seq_profile.is_none() || self.seq_level_idx_0.is_none() {
out.push("item-av1C-truncated");
return out;
}
let p = self.seq_profile.unwrap();
let l = self.seq_level_idx_0.unwrap();
let (max_p, max_l) = match self.profile {
AvifProfile::Baseline => (0u8, 13u8),
AvifProfile::Advanced => (1u8, 16u8),
};
if p > max_p {
out.push("seq-profile-out-of-range");
}
if l > max_l {
out.push("seq-level-idx-out-of-range");
}
out
}
}
pub fn audit_avif_profile_compliance(
meta: &crate::meta::Meta,
brands: &crate::parser::BrandClass,
) -> Vec<AvifProfileCompliance> {
let av01 = b(b"av01");
let av1c_kind = b(b"av1C");
let mut out = Vec::new();
if !brands.is_baseline_profile && !brands.is_advanced_profile {
return out;
}
for item_id in meta.item_ids_of_type(&av01) {
let (seq_profile, seq_level_idx_0, missing_av1c) =
match meta.property_for(item_id, &av1c_kind) {
Some(crate::meta::Property::Av1C(bytes)) => (
decode_av1c_seq_profile(bytes),
decode_av1c_seq_level_idx_0(bytes),
false,
),
_ => (None, None, true),
};
if brands.is_baseline_profile {
out.push(AvifProfileCompliance {
profile: AvifProfile::Baseline,
item_id,
seq_profile,
seq_level_idx_0,
missing_av1c,
});
}
if brands.is_advanced_profile {
out.push(AvifProfileCompliance {
profile: AvifProfile::Advanced,
item_id,
seq_profile,
seq_level_idx_0,
missing_av1c,
});
}
}
out
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DimTransform {
Rotate { angle: u8 },
Mirror { axis: u8 },
Crop {
width_n: i32,
width_d: i32,
height_n: i32,
height_d: i32,
},
Scale {
target_width_numerator: u16,
target_width_denominator: u16,
target_height_numerator: u16,
target_height_denominator: u16,
},
}
impl DimTransform {
pub fn apply_dims(&self, w: u32, h: u32) -> (u32, u32) {
match *self {
DimTransform::Rotate { angle } => {
if angle % 2 == 1 {
(h, w)
} else {
(w, h)
}
}
DimTransform::Mirror { .. } => (w, h),
DimTransform::Crop {
width_n,
width_d,
height_n,
height_d,
} => {
if width_d == 0 || height_d == 0 {
return (w, h);
}
let cw = (width_n as i64 + width_d as i64 / 2) / width_d as i64;
let ch = (height_n as i64 + height_d as i64 / 2) / height_d as i64;
if cw <= 0 || ch <= 0 || cw > w as i64 || ch > h as i64 {
return (w, h);
}
(cw as u32, ch as u32)
}
DimTransform::Scale {
target_width_numerator,
target_width_denominator,
target_height_numerator,
target_height_denominator,
} => {
let iscl = crate::meta::Iscl {
target_width_numerator,
target_width_denominator,
target_height_numerator,
target_height_denominator,
};
iscl.scaled_dims(w, h).unwrap_or((w, h))
}
}
}
}
pub fn transform_chain(meta: &crate::meta::Meta, item_id: u32) -> Vec<DimTransform> {
use crate::meta::Property;
let Some(assoc) = meta.assoc_by_id(item_id) else {
return Vec::new();
};
let mut out = Vec::new();
for pa in &assoc.entries {
let Some(prop) = meta.properties.get(pa.index as usize) else {
continue;
};
match prop {
Property::Irot(r) => out.push(DimTransform::Rotate { angle: r.angle }),
Property::Imir(m) => out.push(DimTransform::Mirror { axis: m.axis }),
Property::Clap(c) => out.push(DimTransform::Crop {
width_n: c.clean_aperture_width_n,
width_d: c.clean_aperture_width_d,
height_n: c.clean_aperture_height_n,
height_d: c.clean_aperture_height_d,
}),
Property::Iscl(s) => out.push(DimTransform::Scale {
target_width_numerator: s.target_width_numerator,
target_width_denominator: s.target_width_denominator,
target_height_numerator: s.target_height_numerator,
target_height_denominator: s.target_height_denominator,
}),
_ => {}
}
}
out
}
pub fn output_dims_from_reconstructed(
meta: &crate::meta::Meta,
item_id: u32,
reconstructed_w: u32,
reconstructed_h: u32,
) -> (u32, u32) {
let mut dims = (reconstructed_w, reconstructed_h);
for t in transform_chain(meta, item_id) {
dims = t.apply_dims(dims.0, dims.1);
}
dims
}
pub fn reconstructed_dims(
meta: &crate::meta::Meta,
item_id: u32,
file_bytes: &[u8],
idat: Option<&[u8]>,
) -> Option<(u32, u32)> {
reconstructed_dims_inner(meta, item_id, file_bytes, idat, 0)
}
fn resolve_descriptor_bytes(
meta: &crate::meta::Meta,
item_id: u32,
file_bytes: &[u8],
idat: Option<&[u8]>,
) -> Option<Vec<u8>> {
let loc = meta.location_by_id(item_id)?;
let backing: &[u8] = match loc.construction_method {
0 => file_bytes,
1 => idat?,
2 => return crate::parser::item_bytes_owned_full(file_bytes, meta, item_id).ok(),
_ => return None,
};
if loc.extents.is_empty() {
return None;
}
let mut out = Vec::new();
for e in &loc.extents {
let start = loc.base_offset.checked_add(e.offset)?;
let end = start.checked_add(e.length)?;
let (start, end) = (usize::try_from(start).ok()?, usize::try_from(end).ok()?);
if end > backing.len() || start > end {
return None;
}
out.extend_from_slice(&backing[start..end]);
}
Some(out)
}
pub const MAX_DERIVATION_DEPTH: u32 = 16;
fn reconstructed_dims_inner(
meta: &crate::meta::Meta,
item_id: u32,
file_bytes: &[u8],
idat: Option<&[u8]>,
depth: u32,
) -> Option<(u32, u32)> {
if depth > MAX_DERIVATION_DEPTH {
return None;
}
let item = meta.item_by_id(item_id)?;
let grid = b(b"grid");
let dimg = b(b"dimg");
if item.item_type == grid {
let bytes = resolve_descriptor_bytes(meta, item_id, file_bytes, idat)?;
let g = crate::grid::ImageGrid::parse(&bytes).ok()?;
return Some((g.output_width, g.output_height));
}
if item.item_type == crate::meta::ITEM_TYPE_IOVL {
let bytes = resolve_descriptor_bytes(meta, item_id, file_bytes, idat)?;
let refs = meta.iref_targets(&dimg, item_id).len();
let o = ImageOverlay::parse(&bytes, refs).ok()?;
return Some((o.output_width, o.output_height));
}
if item.item_type == crate::meta::ITEM_TYPE_IDEN {
let inputs = meta.iref_targets(&dimg, item_id);
if inputs.len() != 1 {
return None;
}
let (iw, ih) = reconstructed_dims_inner(meta, inputs[0], file_bytes, idat, depth + 1)?;
return Some(output_dims_from_reconstructed(meta, inputs[0], iw, ih));
}
if item.item_type == crate::meta::ITEM_TYPE_TMAP {
let inputs = meta.iref_targets(&dimg, item_id);
let base = *inputs.first()?;
let (iw, ih) = reconstructed_dims_inner(meta, base, file_bytes, idat, depth + 1)?;
return Some(output_dims_from_reconstructed(meta, base, iw, ih));
}
if item.item_type == crate::meta::ITEM_TYPE_SATO {
if let Some(crate::meta::Property::Ispe(ispe)) = meta.property_for(item_id, &b(b"ispe")) {
return Some((ispe.width, ispe.height));
}
let inputs = meta.iref_targets(&dimg, item_id);
let base = *inputs.first()?;
let (iw, ih) = reconstructed_dims_inner(meta, base, file_bytes, idat, depth + 1)?;
return Some(output_dims_from_reconstructed(meta, base, iw, ih));
}
match meta.property_for(item_id, &b(b"ispe")) {
Some(crate::meta::Property::Ispe(ispe)) => Some((ispe.width, ispe.height)),
_ => None,
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OverlayPlacement {
pub source_item_id: u32,
pub offset_x: i64,
pub offset_y: i64,
pub input_width: u32,
pub input_height: u32,
}
impl OverlayPlacement {
#[allow(clippy::too_many_arguments)]
pub fn new(
source_item_id: u32,
offset_x: i64,
offset_y: i64,
input_width: u32,
input_height: u32,
) -> Self {
Self {
source_item_id,
offset_x,
offset_y,
input_width,
input_height,
}
}
}
impl OverlayPlacement {
pub fn visible(&self, canvas_w: u32, canvas_h: u32) -> Option<(u32, u32, u32, u32)> {
let left = self.offset_x.max(0);
let top = self.offset_y.max(0);
let right = (self.offset_x + i64::from(self.input_width)).min(i64::from(canvas_w));
let bottom = (self.offset_y + i64::from(self.input_height)).min(i64::from(canvas_h));
if right <= left || bottom <= top {
return None;
}
Some((
left as u32,
top as u32,
(right - left) as u32,
(bottom - top) as u32,
))
}
pub fn fully_visible(&self, canvas_w: u32, canvas_h: u32) -> bool {
self.offset_x >= 0
&& self.offset_y >= 0
&& self.offset_x + i64::from(self.input_width) <= i64::from(canvas_w)
&& self.offset_y + i64::from(self.input_height) <= i64::from(canvas_h)
}
pub fn off_canvas(&self, canvas_w: u32, canvas_h: u32) -> bool {
self.visible(canvas_w, canvas_h).is_none()
}
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OverlayResolution {
pub iovl_item_id: u32,
pub descriptor: ImageOverlay,
pub placements: Vec<OverlayPlacement>,
}
impl OverlayResolution {
#[allow(clippy::too_many_arguments)]
pub fn new(
iovl_item_id: u32,
descriptor: ImageOverlay,
placements: Vec<OverlayPlacement>,
) -> Self {
Self {
iovl_item_id,
descriptor,
placements,
}
}
}
impl OverlayResolution {
pub fn canvas(&self) -> (u32, u32) {
(self.descriptor.output_width, self.descriptor.output_height)
}
pub fn canvas_partially_filled(&self) -> bool {
let (cw, ch) = self.canvas();
let area = u64::from(cw) * u64::from(ch);
if area == 0 {
return false;
}
if area > 4_000_000 {
return true;
}
let mut covered = vec![false; area as usize];
for p in &self.placements {
if let Some((x, y, w, h)) = p.visible(cw, ch) {
for row in y..y + h {
let base = (u64::from(row) * u64::from(cw)) as usize;
for col in x..x + w {
covered[base + col as usize] = true;
}
}
}
}
covered.iter().any(|&c| !c)
}
}
pub fn resolve_overlays(
meta: &crate::meta::Meta,
file_bytes: &[u8],
idat: Option<&[u8]>,
) -> Vec<OverlayResolution> {
let dimg = b(b"dimg");
let mut out = Vec::new();
for iovl_id in meta.item_ids_of_type(&crate::meta::ITEM_TYPE_IOVL) {
let Some(bytes) = resolve_descriptor_bytes(meta, iovl_id, file_bytes, idat) else {
continue;
};
let sources = meta.iref_targets(&dimg, iovl_id);
let Ok(descriptor) = ImageOverlay::parse(&bytes, sources.len()) else {
continue;
};
let mut placements = Vec::with_capacity(descriptor.entries.len());
for (entry, &src) in descriptor.entries.iter().zip(sources.iter()) {
let (iw, ih) = reconstructed_dims(meta, src, file_bytes, idat)
.map(|(w, h)| output_dims_from_reconstructed(meta, src, w, h))
.unwrap_or((0, 0));
placements.push(OverlayPlacement {
source_item_id: src,
offset_x: i64::from(entry.horizontal_offset),
offset_y: i64::from(entry.vertical_offset),
input_width: iw,
input_height: ih,
});
}
out.push(OverlayResolution {
iovl_item_id: iovl_id,
descriptor,
placements,
});
}
out
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IdenResolution {
pub iden_item_id: u32,
pub source_item_id: Option<u32>,
pub source_dims: Option<(u32, u32)>,
pub transforms: Vec<DimTransform>,
pub output_dims: Option<(u32, u32)>,
}
impl IdenResolution {
#[allow(clippy::too_many_arguments)]
pub fn new(
iden_item_id: u32,
source_item_id: Option<u32>,
source_dims: Option<(u32, u32)>,
transforms: Vec<DimTransform>,
output_dims: Option<(u32, u32)>,
) -> Self {
Self {
iden_item_id,
source_item_id,
source_dims,
transforms,
output_dims,
}
}
}
pub fn resolve_iden_derivations(
meta: &crate::meta::Meta,
file_bytes: &[u8],
idat: Option<&[u8]>,
) -> Vec<IdenResolution> {
let dimg = b(b"dimg");
let mut out = Vec::new();
for iden_id in meta.item_ids_of_type(&crate::meta::ITEM_TYPE_IDEN) {
let inputs = meta.iref_targets(&dimg, iden_id);
let source_item_id = if inputs.len() == 1 {
Some(inputs[0])
} else {
None
};
let source_dims = source_item_id.and_then(|src| {
reconstructed_dims(meta, src, file_bytes, idat)
.map(|(w, h)| output_dims_from_reconstructed(meta, src, w, h))
});
let transforms = transform_chain(meta, iden_id);
let output_dims = source_dims.map(|(w, h)| {
let mut d = (w, h);
for t in &transforms {
d = t.apply_dims(d.0, d.1);
}
d
});
out.push(IdenResolution {
iden_item_id: iden_id,
source_item_id,
source_dims,
transforms,
output_dims,
});
}
out
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToneMapResolution {
pub tmap_item_id: u32,
pub base_item_id: Option<u32>,
pub gain_map_item_ids: Vec<u32>,
pub rendered_dims: Option<(u32, u32)>,
pub gain_map_dims: Vec<(u32, Option<(u32, u32)>)>,
}
impl ToneMapResolution {
#[allow(clippy::too_many_arguments)]
pub fn new(
tmap_item_id: u32,
base_item_id: Option<u32>,
gain_map_item_ids: Vec<u32>,
rendered_dims: Option<(u32, u32)>,
gain_map_dims: Vec<(u32, Option<(u32, u32)>)>,
) -> Self {
Self {
tmap_item_id,
base_item_id,
gain_map_item_ids,
rendered_dims,
gain_map_dims,
}
}
}
impl ToneMapResolution {
pub fn upsamples_gain_map(&self) -> bool {
let Some((bw, bh)) = self.rendered_dims else {
return false;
};
self.gain_map_dims.iter().any(|&(_, dims)| match dims {
Some((gw, gh)) => gw < bw || gh < bh,
None => false,
})
}
}
pub fn resolve_tone_maps(
meta: &crate::meta::Meta,
file_bytes: &[u8],
idat: Option<&[u8]>,
) -> Vec<ToneMapResolution> {
let dimg = b(b"dimg");
let mut out = Vec::new();
for tmap_id in meta.item_ids_of_type(&crate::meta::ITEM_TYPE_TMAP) {
let inputs = meta.iref_targets(&dimg, tmap_id);
let base_item_id = inputs.first().copied();
let gain_map_item_ids: Vec<u32> = inputs.iter().skip(1).copied().collect();
let rendered_dims = base_item_id.and_then(|base| {
reconstructed_dims(meta, base, file_bytes, idat)
.map(|(w, h)| output_dims_from_reconstructed(meta, base, w, h))
});
let gain_map_dims = gain_map_item_ids
.iter()
.map(|&gid| {
let dims = reconstructed_dims(meta, gid, file_bytes, idat)
.map(|(w, h)| output_dims_from_reconstructed(meta, gid, w, h));
(gid, dims)
})
.collect::<Vec<_>>();
out.push(ToneMapResolution {
tmap_item_id: tmap_id,
base_item_id,
gain_map_item_ids,
rendered_dims,
gain_map_dims,
});
}
out
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GridTilePlacement {
pub source_item_id: u32,
pub row: u16,
pub col: u16,
pub origin_x: u32,
pub origin_y: u32,
}
impl GridTilePlacement {
#[allow(clippy::too_many_arguments)]
pub fn new(source_item_id: u32, row: u16, col: u16, origin_x: u32, origin_y: u32) -> Self {
Self {
source_item_id,
row,
col,
origin_x,
origin_y,
}
}
}
impl GridTilePlacement {
pub fn visible(
&self,
tile_w: u32,
tile_h: u32,
output_width: u32,
output_height: u32,
) -> Option<(u32, u32, u32, u32)> {
let right = (u64::from(self.origin_x) + u64::from(tile_w)).min(u64::from(output_width));
let bottom = (u64::from(self.origin_y) + u64::from(tile_h)).min(u64::from(output_height));
if right <= u64::from(self.origin_x) || bottom <= u64::from(self.origin_y) {
return None;
}
Some((
self.origin_x,
self.origin_y,
(right - u64::from(self.origin_x)) as u32,
(bottom - u64::from(self.origin_y)) as u32,
))
}
pub fn is_trimmed(
&self,
tile_w: u32,
tile_h: u32,
output_width: u32,
output_height: u32,
) -> bool {
u64::from(self.origin_x) + u64::from(tile_w) > u64::from(output_width)
|| u64::from(self.origin_y) + u64::from(tile_h) > u64::from(output_height)
}
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GridResolution {
pub grid_item_id: u32,
pub descriptor: crate::grid::ImageGrid,
pub tile_dims: Option<(u32, u32)>,
pub placements: Vec<GridTilePlacement>,
}
impl GridResolution {
#[allow(clippy::too_many_arguments)]
pub fn new(
grid_item_id: u32,
descriptor: crate::grid::ImageGrid,
tile_dims: Option<(u32, u32)>,
placements: Vec<GridTilePlacement>,
) -> Self {
Self {
grid_item_id,
descriptor,
tile_dims,
placements,
}
}
}
impl GridResolution {
pub fn canvas(&self) -> (u32, u32) {
(self.descriptor.output_width, self.descriptor.output_height)
}
pub fn covers_canvas(&self) -> bool {
let Some((tw, th)) = self.tile_dims else {
return false;
};
u64::from(tw) * u64::from(self.descriptor.columns)
>= u64::from(self.descriptor.output_width)
&& u64::from(th) * u64::from(self.descriptor.rows)
>= u64::from(self.descriptor.output_height)
}
pub fn trimmed_tile_count(&self) -> usize {
let Some((tw, th)) = self.tile_dims else {
return 0;
};
let (ow, oh) = self.canvas();
self.placements
.iter()
.filter(|p| p.is_trimmed(tw, th, ow, oh))
.count()
}
}
pub fn resolve_grids(
meta: &crate::meta::Meta,
file_bytes: &[u8],
idat: Option<&[u8]>,
) -> Vec<GridResolution> {
let grid_type = crate::parser::ITEM_TYPE_GRID;
let dimg = b(b"dimg");
let mut out = Vec::new();
for grid_id in meta.item_ids_of_type(&grid_type) {
let Some(bytes) = resolve_descriptor_bytes(meta, grid_id, file_bytes, idat) else {
continue;
};
let Ok(descriptor) = crate::grid::ImageGrid::parse(&bytes) else {
continue;
};
let tiles = meta.iref_targets(&dimg, grid_id);
let tile_dims = tiles.first().and_then(|&first| {
reconstructed_dims(meta, first, file_bytes, idat)
.map(|(w, h)| output_dims_from_reconstructed(meta, first, w, h))
});
let cols = descriptor.columns;
let (tw, th) = tile_dims.unwrap_or((0, 0));
let mut placements = Vec::with_capacity(tiles.len());
for (idx, &tile_id) in tiles.iter().enumerate() {
let row = (idx as u32 / cols.max(1) as u32) as u16;
let col = (idx as u32 % cols.max(1) as u32) as u16;
placements.push(GridTilePlacement {
source_item_id: tile_id,
row,
col,
origin_x: u32::from(col) * tw,
origin_y: u32::from(row) * th,
});
}
out.push(GridResolution {
grid_item_id: grid_id,
descriptor,
tile_dims,
placements,
});
}
out
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DerivationKind {
Coded,
Grid,
Overlay,
Identity,
ToneMap,
SampleTransform,
Unknown,
}
impl DerivationKind {
fn classify(item_type: &BoxType, is_derived: bool) -> Self {
if item_type == &crate::parser::ITEM_TYPE_AV01 {
DerivationKind::Coded
} else if item_type == &crate::parser::ITEM_TYPE_GRID {
DerivationKind::Grid
} else if item_type == &crate::meta::ITEM_TYPE_IOVL {
DerivationKind::Overlay
} else if item_type == &crate::meta::ITEM_TYPE_IDEN {
DerivationKind::Identity
} else if item_type == &crate::meta::ITEM_TYPE_TMAP {
DerivationKind::ToneMap
} else if item_type == &crate::meta::ITEM_TYPE_SATO {
DerivationKind::SampleTransform
} else if is_derived {
DerivationKind::Unknown
} else {
DerivationKind::Coded
}
}
pub fn is_coded(self) -> bool {
matches!(self, DerivationKind::Coded)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DerivationNode {
pub item_id: u32,
pub kind: DerivationKind,
pub inputs: Vec<u32>,
pub reconstructed_dims: Option<(u32, u32)>,
pub output_dims: Option<(u32, u32)>,
pub depth: u32,
}
impl DerivationNode {
#[allow(clippy::too_many_arguments)]
pub fn new(
item_id: u32,
kind: DerivationKind,
inputs: Vec<u32>,
reconstructed_dims: Option<(u32, u32)>,
output_dims: Option<(u32, u32)>,
depth: u32,
) -> Self {
Self {
item_id,
kind,
inputs,
reconstructed_dims,
output_dims,
depth,
}
}
}
impl DerivationNode {
pub fn transform_chain(&self, meta: &crate::meta::Meta) -> Vec<DimTransform> {
transform_chain(meta, self.item_id)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DerivationGraph {
pub root_item_id: u32,
pub nodes: Vec<DerivationNode>,
pub coded_leaf_ids: Vec<u32>,
pub truncated: bool,
}
impl DerivationGraph {
#[allow(clippy::too_many_arguments)]
pub fn new(
root_item_id: u32,
nodes: Vec<DerivationNode>,
coded_leaf_ids: Vec<u32>,
truncated: bool,
) -> Self {
Self {
root_item_id,
nodes,
coded_leaf_ids,
truncated,
}
}
}
impl DerivationGraph {
pub fn output_dims(&self) -> Option<(u32, u32)> {
self.nodes.first().and_then(|n| n.output_dims)
}
pub fn root_is_coded(&self) -> bool {
self.nodes
.first()
.is_some_and(|n| n.kind == DerivationKind::Coded)
}
pub fn node(&self, item_id: u32) -> Option<&DerivationNode> {
self.nodes.iter().find(|n| n.item_id == item_id)
}
pub fn derived_node_count(&self) -> usize {
self.nodes.iter().filter(|n| !n.kind.is_coded()).count()
}
pub fn coded_leaf_dims(&self) -> Vec<(u32, Option<(u32, u32)>)> {
self.coded_leaf_ids
.iter()
.map(|&id| (id, self.node(id).and_then(|n| n.reconstructed_dims)))
.collect()
}
pub fn nodes_at_depth(&self, depth: u32) -> Vec<u32> {
self.nodes
.iter()
.filter(|n| n.depth == depth)
.map(|n| n.item_id)
.collect()
}
}
pub fn build_derivation_graph(
meta: &crate::meta::Meta,
root_item_id: u32,
file_bytes: &[u8],
idat: Option<&[u8]>,
) -> DerivationGraph {
let dimg = b(b"dimg");
let mut nodes: Vec<DerivationNode> = Vec::new();
let mut visited: Vec<u32> = Vec::new();
let mut truncated = false;
let mut stack: Vec<(u32, u32)> = vec![(root_item_id, 0)];
while let Some((item_id, depth)) = stack.pop() {
if visited.contains(&item_id) {
continue;
}
if depth > MAX_DERIVATION_DEPTH {
truncated = true;
continue;
}
visited.push(item_id);
let Some(item) = meta.item_by_id(item_id) else {
continue;
};
let inputs = meta.iref_targets(&dimg, item_id);
let is_derived = !inputs.is_empty();
let kind = DerivationKind::classify(&item.item_type, is_derived);
let reconstructed = reconstructed_dims_inner(meta, item_id, file_bytes, idat, 0);
let output =
reconstructed.map(|(w, h)| output_dims_from_reconstructed(meta, item_id, w, h));
if !kind.is_coded() {
for &child in inputs.iter().rev() {
stack.push((child, depth + 1));
}
}
nodes.push(DerivationNode {
item_id,
kind,
inputs,
reconstructed_dims: reconstructed,
output_dims: output,
depth,
});
}
let visit_rank = |id: u32| visited.iter().position(|&v| v == id).unwrap_or(usize::MAX);
nodes.sort_by_key(|n| (n.depth, visit_rank(n.item_id)));
let coded_leaf_ids: Vec<u32> = nodes
.iter()
.filter(|n| n.kind.is_coded())
.map(|n| n.item_id)
.collect();
DerivationGraph {
root_item_id,
nodes,
coded_leaf_ids,
truncated,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iovl_parses_two_entries_16bit_fields() {
let mut buf = Vec::new();
buf.push(0); buf.push(0); for v in [65535u16, 65535, 65535, 65535] {
buf.extend_from_slice(&v.to_be_bytes());
}
buf.extend_from_slice(&256u16.to_be_bytes()); buf.extend_from_slice(&256u16.to_be_bytes()); buf.extend_from_slice(&10i16.to_be_bytes()); buf.extend_from_slice(&20i16.to_be_bytes()); buf.extend_from_slice(&30i16.to_be_bytes()); buf.extend_from_slice(&40i16.to_be_bytes()); let o = ImageOverlay::parse(&buf, 2).unwrap();
assert_eq!(o.canvas_fill_value, [65535, 65535, 65535, 65535]);
assert_eq!(o.output_width, 256);
assert_eq!(o.output_height, 256);
assert_eq!(
o.entries,
vec![
OverlayEntry {
horizontal_offset: 10,
vertical_offset: 20
},
OverlayEntry {
horizontal_offset: 30,
vertical_offset: 40
}
]
);
}
#[test]
fn iovl_parses_32bit_fields() {
let mut buf = Vec::new();
buf.push(0); buf.push(1); for v in [0u16, 0, 0, 0] {
buf.extend_from_slice(&v.to_be_bytes());
}
buf.extend_from_slice(&100_000u32.to_be_bytes()); buf.extend_from_slice(&8_000u32.to_be_bytes()); buf.extend_from_slice(&(-5i32).to_be_bytes()); buf.extend_from_slice(&10_000i32.to_be_bytes()); let o = ImageOverlay::parse(&buf, 1).unwrap();
assert_eq!(o.output_width, 100_000);
assert_eq!(o.output_height, 8_000);
assert_eq!(o.entries[0].horizontal_offset, -5);
assert_eq!(o.entries[0].vertical_offset, 10_000);
}
#[test]
fn iovl_negative_offset_signed_round_trip() {
let mut buf = Vec::new();
buf.push(0);
buf.push(0);
for _ in 0..4 {
buf.extend_from_slice(&0u16.to_be_bytes());
}
buf.extend_from_slice(&64u16.to_be_bytes());
buf.extend_from_slice(&64u16.to_be_bytes());
buf.extend_from_slice(&(-3i16).to_be_bytes());
buf.extend_from_slice(&(-4i16).to_be_bytes());
let o = ImageOverlay::parse(&buf, 1).unwrap();
assert_eq!(o.entries[0].horizontal_offset, -3);
assert_eq!(o.entries[0].vertical_offset, -4);
}
#[test]
fn iovl_rejects_oversized_reference_count() {
let mut buf = Vec::new();
buf.push(0);
buf.push(0);
for _ in 0..4 {
buf.extend_from_slice(&0u16.to_be_bytes());
}
buf.extend_from_slice(&16u16.to_be_bytes());
buf.extend_from_slice(&16u16.to_be_bytes());
assert!(ImageOverlay::parse(&buf, 100).is_err());
}
#[test]
fn iovl_rejects_nonzero_version() {
let buf = vec![1u8, 0]; assert!(ImageOverlay::parse(&buf, 0).is_err());
}
fn build_grpl_altr() -> Vec<u8> {
let mut buf = Vec::new();
let mut child = vec![0u8; 4]; child.extend_from_slice(&42u32.to_be_bytes()); child.extend_from_slice(&3u32.to_be_bytes()); child.extend_from_slice(&1u32.to_be_bytes());
child.extend_from_slice(&2u32.to_be_bytes());
child.extend_from_slice(&3u32.to_be_bytes());
let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(b"altr");
buf.extend_from_slice(&child);
buf
}
#[test]
fn grpl_parses_altr_group() {
let grpl = build_grpl_altr();
let groups = parse_grpl(&grpl).unwrap();
assert_eq!(groups.len(), 1);
let g = &groups[0];
assert_eq!(&g.grouping_type, b"altr");
assert!(g.is_alternates());
assert!(!g.is_stereo_pair());
assert!(!g.is_equivalence());
assert!(!g.is_panorama());
assert_eq!(g.group_id, 42);
assert_eq!(g.entity_ids, vec![1, 2, 3]);
}
#[test]
fn grpl_parses_pano_group() {
let mut buf = Vec::new();
let mut child = vec![0u8; 4]; child.extend_from_slice(&5u32.to_be_bytes()); child.extend_from_slice(&3u32.to_be_bytes()); child.extend_from_slice(&21u32.to_be_bytes()); child.extend_from_slice(&22u32.to_be_bytes());
child.extend_from_slice(&23u32.to_be_bytes());
let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(b"pano");
buf.extend_from_slice(&child);
let groups = parse_grpl(&buf).unwrap();
assert_eq!(groups.len(), 1);
let g = &groups[0];
assert!(g.is_panorama());
assert!(!g.is_alternates());
assert!(!g.is_stereo_pair());
assert!(!g.is_equivalence());
assert_eq!(g.entity_ids, vec![21, 22, 23]);
}
#[test]
fn grpl_classifies_prgr_brst_msrc_groups() {
let build = |fourcc: &[u8; 4], ids: &[u32]| {
let mut child = vec![0u8; 4]; child.extend_from_slice(&7u32.to_be_bytes()); child.extend_from_slice(&(ids.len() as u32).to_be_bytes());
for &id in ids {
child.extend_from_slice(&id.to_be_bytes());
}
let mut buf = Vec::new();
let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(fourcc);
buf.extend_from_slice(&child);
buf
};
let prgr = &parse_grpl(&build(b"prgr", &[1, 2, 3])).unwrap()[0];
assert!(prgr.is_progressive());
assert!(!prgr.is_burst() && !prgr.is_multi_source() && !prgr.is_alternates());
assert_eq!(prgr.entity_ids, vec![1, 2, 3]);
let brst = &parse_grpl(&build(b"brst", &[10, 11])).unwrap()[0];
assert!(brst.is_burst());
assert!(!brst.is_progressive() && !brst.is_multi_source());
let msrc = &parse_grpl(&build(b"msrc", &[5, 6])).unwrap()[0];
assert!(msrc.is_multi_source());
assert!(!msrc.is_progressive() && !msrc.is_burst());
}
#[test]
fn grpl_classifies_tsyn_and_iaug_groups() {
let build = |fourcc: &[u8; 4], flags: u32, ids: &[u32]| {
let mut child = Vec::new();
child.push(0u8); child.extend_from_slice(&flags.to_be_bytes()[1..]); child.extend_from_slice(&9u32.to_be_bytes()); child.extend_from_slice(&(ids.len() as u32).to_be_bytes());
for &id in ids {
child.extend_from_slice(&id.to_be_bytes());
}
let mut buf = Vec::new();
let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(fourcc);
buf.extend_from_slice(&child);
buf
};
let tsyn = &parse_grpl(&build(b"tsyn", 0, &[1, 2, 3])).unwrap()[0];
assert!(tsyn.is_time_synchronized());
assert!(!tsyn.is_audio_to_image() && !tsyn.is_burst());
assert_eq!(tsyn.entity_ids, vec![1, 2, 3]);
assert_eq!(tsyn.flags, 0);
let iaug = &parse_grpl(&build(b"iaug", 0, &[20, 21])).unwrap()[0];
assert!(iaug.is_audio_to_image());
assert!(!iaug.audio_repeats());
assert_eq!(iaug.entity_ids, vec![20, 21]);
let iaug_rep = &parse_grpl(&build(b"iaug", 0x1, &[20, 21])).unwrap()[0];
assert!(iaug_rep.is_audio_to_image());
assert!(iaug_rep.audio_repeats());
assert_eq!(iaug_rep.flags, 0x1);
let ster = &parse_grpl(&build(b"ster", 0x1, &[20, 21])).unwrap()[0];
assert!(!ster.audio_repeats());
}
#[test]
fn grpl_cardinality_shall() {
let build = |fourcc: &[u8; 4], ids: &[u32]| {
let mut child = vec![0u8; 4];
child.extend_from_slice(&1u32.to_be_bytes());
child.extend_from_slice(&(ids.len() as u32).to_be_bytes());
for &id in ids {
child.extend_from_slice(&id.to_be_bytes());
}
let mut buf = Vec::new();
let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(fourcc);
buf.extend_from_slice(&child);
buf
};
assert!(parse_grpl(&build(b"ster", &[1, 2])).unwrap()[0].cardinality_ok());
assert!(!parse_grpl(&build(b"ster", &[1])).unwrap()[0].cardinality_ok());
assert!(!parse_grpl(&build(b"ster", &[1, 2, 3])).unwrap()[0].cardinality_ok());
assert!(parse_grpl(&build(b"iaug", &[1, 2])).unwrap()[0].cardinality_ok());
assert!(!parse_grpl(&build(b"iaug", &[1])).unwrap()[0].cardinality_ok());
assert!(parse_grpl(&build(b"altr", &[1, 2, 3, 4])).unwrap()[0].cardinality_ok());
}
#[test]
fn grpl_classifies_bracketed_set_groups() {
let build = |fourcc: &[u8; 4], ids: &[u32]| {
let mut child = vec![0u8; 4]; child.extend_from_slice(&3u32.to_be_bytes()); child.extend_from_slice(&(ids.len() as u32).to_be_bytes());
for &id in ids {
child.extend_from_slice(&id.to_be_bytes());
}
let mut buf = Vec::new();
let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(fourcc);
buf.extend_from_slice(&child);
buf
};
let cases = [
(&b"aebr"[..], BracketingKind::AutoExposure),
(&b"wbbr"[..], BracketingKind::WhiteBalance),
(&b"fobr"[..], BracketingKind::Focus),
(&b"afbr"[..], BracketingKind::FlashExposure),
(&b"dobr"[..], BracketingKind::DepthOfField),
];
for (fourcc, kind) in cases {
let mut fc = [0u8; 4];
fc.copy_from_slice(fourcc);
let g = &parse_grpl(&build(&fc, &[1, 2])).unwrap()[0];
assert!(g.is_bracketed_set());
assert_eq!(g.bracketing_kind(), Some(kind));
assert_eq!(kind.four_cc(), &fc);
}
let altr = &parse_grpl(&build(b"altr", &[1, 2])).unwrap()[0];
assert!(!altr.is_bracketed_set());
assert_eq!(altr.bracketing_kind(), None);
}
#[test]
fn grpl_classifies_slid_and_user_collections() {
let build = |fourcc: &[u8; 4], ids: &[u32]| {
let mut child = vec![0u8; 4]; child.extend_from_slice(&4u32.to_be_bytes()); child.extend_from_slice(&(ids.len() as u32).to_be_bytes());
for &id in ids {
child.extend_from_slice(&id.to_be_bytes());
}
let mut buf = Vec::new();
let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(fourcc);
buf.extend_from_slice(&child);
buf
};
let slid = &parse_grpl(&build(b"slid", &[1, 2, 3])).unwrap()[0];
assert!(slid.is_slideshow());
assert!(!slid.is_user_collection() && !slid.is_bracketed_set());
let albc = &parse_grpl(&build(b"albc", &[4, 5])).unwrap()[0];
assert!(albc.is_album_collection() && albc.is_user_collection());
assert!(!albc.is_favorites_collection() && !albc.is_slideshow());
let favc = &parse_grpl(&build(b"favc", &[6])).unwrap()[0];
assert!(favc.is_favorites_collection() && favc.is_user_collection());
assert!(!favc.is_album_collection());
}
#[test]
fn grpl_parses_ster_pair() {
let mut buf = Vec::new();
let mut child = vec![0u8; 4]; child.extend_from_slice(&7u32.to_be_bytes()); child.extend_from_slice(&2u32.to_be_bytes()); child.extend_from_slice(&10u32.to_be_bytes()); child.extend_from_slice(&11u32.to_be_bytes()); let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(b"ster");
buf.extend_from_slice(&child);
let groups = parse_grpl(&buf).unwrap();
assert_eq!(groups.len(), 1);
assert!(groups[0].is_stereo_pair());
assert_eq!(groups[0].entity_ids, vec![10, 11]);
}
#[test]
fn grpl_parses_multiple_groups() {
let mut buf = Vec::new();
let mut a = vec![0u8; 4];
a.extend_from_slice(&1u32.to_be_bytes());
a.extend_from_slice(&1u32.to_be_bytes());
a.extend_from_slice(&100u32.to_be_bytes());
let asz = (8 + a.len()) as u32;
buf.extend_from_slice(&asz.to_be_bytes());
buf.extend_from_slice(b"altr");
buf.extend_from_slice(&a);
let mut e = vec![0u8; 4];
e.extend_from_slice(&2u32.to_be_bytes());
e.extend_from_slice(&0u32.to_be_bytes()); let esz = (8 + e.len()) as u32;
buf.extend_from_slice(&esz.to_be_bytes());
buf.extend_from_slice(b"eqiv");
buf.extend_from_slice(&e);
let groups = parse_grpl(&buf).unwrap();
assert_eq!(groups.len(), 2);
assert!(groups[0].is_alternates());
assert!(groups[1].is_equivalence());
assert!(groups[1].entity_ids.is_empty());
}
#[test]
fn grpl_rejects_truncated_entity_list() {
let mut buf = Vec::new();
let mut child = vec![0u8; 4];
child.extend_from_slice(&1u32.to_be_bytes()); child.extend_from_slice(&5u32.to_be_bytes()); child.extend_from_slice(&100u32.to_be_bytes()); let size = (8 + child.len()) as u32;
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(b"altr");
buf.extend_from_slice(&child);
assert!(parse_grpl(&buf).is_err());
}
#[test]
fn mif1_compliance_missing_list() {
let bare = Mif1Compliance::default();
assert!(!bare.is_compliant());
let m = bare.missing();
assert!(m.contains(&"hdlr"));
assert!(m.contains(&"pitm"));
assert!(m.contains(&"iinf"));
assert!(m.contains(&"infe"));
assert!(m.contains(&"iloc"));
assert!(m.contains(&"iprp"));
}
#[test]
fn mif1_compliance_full() {
let m = Mif1Compliance {
has_hdlr: true,
has_pitm: true,
has_iinf: true,
has_iloc: true,
has_iprp: true,
infe_count: 1,
claims_mif1: true,
};
assert!(m.is_compliant());
assert!(m.missing().is_empty());
}
fn build_sato_raw(bit_depth: u8, tokens: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(2 + tokens.len());
buf.push(bit_depth & 0x03);
assert!(tokens.iter().filter(|&&t| t != 0).count() <= 255);
buf.push(0);
buf.extend_from_slice(tokens);
buf
}
fn build_sato(bit_depth: u8, tokens: &[Token]) -> Vec<u8> {
let const_bytes = match bit_depth & 0x03 {
0 => 1,
1 => 2,
2 => 4,
3 => 8,
_ => unreachable!(),
};
let mut body = Vec::new();
for t in tokens {
match t {
Token::Constant(v) => {
body.push(0);
match const_bytes {
1 => body.push(*v as i8 as u8),
2 => body.extend_from_slice(&(*v as i16).to_be_bytes()),
4 => body.extend_from_slice(&(*v as i32).to_be_bytes()),
8 => body.extend_from_slice(&v.to_be_bytes()),
_ => unreachable!(),
}
}
Token::Sample(n) => body.push(*n),
Token::Unary(raw) | Token::Binary(raw) | Token::Reserved(raw) => body.push(*raw),
}
}
let mut buf = build_sato_raw(bit_depth, &body);
buf[1] = tokens.len() as u8;
buf
}
#[test]
fn sato_parses_constant_only_each_bit_depth() {
for (bd, expected_bits) in [(0, 8u32), (1, 16), (2, 32), (3, 64)] {
let payload = build_sato(bd, &[Token::Constant(-1)]);
let st = SampleTransform::parse(&payload, 0).unwrap();
assert_eq!(st.version, 0);
assert_eq!(st.bit_depth, bd);
assert_eq!(st.num_bits(), expected_bits);
assert_eq!(st.tokens, vec![Token::Constant(-1)]);
assert_eq!(st.evaluate(&[]).unwrap(), -1);
}
}
#[test]
fn sato_evaluates_sum_of_two_samples() {
let payload = build_sato(0, &[Token::Sample(1), Token::Sample(2), Token::Binary(128)]);
let st = SampleTransform::parse(&payload, 2).unwrap();
assert_eq!(st.tokens.len(), 3);
assert_eq!(st.evaluate(&[40, 25]).unwrap(), 65);
assert_eq!(st.evaluate(&[120, 120]).unwrap(), 127);
}
#[test]
fn sato_evaluates_difference_with_right_pop_first() {
let payload = build_sato(
1, &[Token::Sample(1), Token::Sample(2), Token::Binary(129)],
);
let st = SampleTransform::parse(&payload, 2).unwrap();
assert_eq!(st.evaluate(&[100, 30]).unwrap(), 70);
assert_eq!(st.evaluate(&[30, 100]).unwrap(), -70);
}
#[test]
fn sato_evaluates_msb_residual_recombine() {
let payload = build_sato(
1, &[
Token::Sample(1),
Token::Constant(2),
Token::Constant(8),
Token::Binary(135), Token::Binary(130), Token::Sample(2),
Token::Binary(128), ],
);
let st = SampleTransform::parse(&payload, 2).unwrap();
assert_eq!(st.evaluate(&[0x12, 0x34]).unwrap(), 0x1234);
}
#[test]
fn sato_evaluates_unary_negation() {
let payload = build_sato(1, &[Token::Sample(1), Token::Unary(64)]);
let st = SampleTransform::parse(&payload, 1).unwrap();
assert_eq!(st.evaluate(&[42]).unwrap(), -42);
}
#[test]
fn sato_evaluates_unary_bsr() {
let payload = build_sato(1, &[Token::Sample(1), Token::Unary(67)]);
let st = SampleTransform::parse(&payload, 1).unwrap();
assert_eq!(st.evaluate(&[1]).unwrap(), 0); assert_eq!(st.evaluate(&[2]).unwrap(), 1);
assert_eq!(st.evaluate(&[255]).unwrap(), 7);
assert_eq!(st.evaluate(&[0]).unwrap(), 0); assert_eq!(st.evaluate(&[-5]).unwrap(), 0);
}
#[test]
fn sato_evaluates_quotient_with_zero_divisor_returns_left() {
let payload = build_sato(1, &[Token::Sample(1), Token::Sample(2), Token::Binary(131)]);
let st = SampleTransform::parse(&payload, 2).unwrap();
assert_eq!(st.evaluate(&[10, 3]).unwrap(), 3); assert_eq!(st.evaluate(&[-10, 3]).unwrap(), -3); assert_eq!(st.evaluate(&[10, 0]).unwrap(), 10); }
#[test]
fn sato_evaluates_pow_zero_base() {
let payload = build_sato(1, &[Token::Sample(1), Token::Sample(2), Token::Binary(135)]);
let st = SampleTransform::parse(&payload, 2).unwrap();
assert_eq!(st.evaluate(&[0, 5]).unwrap(), 0);
assert_eq!(st.evaluate(&[2, 3]).unwrap(), 8);
assert_eq!(st.evaluate(&[3, 0]).unwrap(), 1);
}
#[test]
fn sato_evaluates_min_max() {
let min_p = build_sato(1, &[Token::Sample(1), Token::Sample(2), Token::Binary(136)]);
let max_p = build_sato(1, &[Token::Sample(1), Token::Sample(2), Token::Binary(137)]);
let min_st = SampleTransform::parse(&min_p, 2).unwrap();
let max_st = SampleTransform::parse(&max_p, 2).unwrap();
assert_eq!(min_st.evaluate(&[10, 3]).unwrap(), 3);
assert_eq!(max_st.evaluate(&[10, 3]).unwrap(), 10);
assert_eq!(min_st.evaluate(&[-5, -2]).unwrap(), -5);
assert_eq!(max_st.evaluate(&[-5, -2]).unwrap(), -2);
}
#[test]
fn sato_rejects_zero_token_count() {
let buf = vec![0u8, 0]; let err = SampleTransform::parse(&buf, 0).unwrap_err();
assert!(format!("{err:?}").contains("token_count"));
}
#[test]
fn sato_rejects_nonzero_version() {
let mut buf = build_sato(0, &[Token::Constant(0)]);
buf[0] = 1u8 << 6; assert!(SampleTransform::parse(&buf, 0).is_err());
let relaxed = SampleTransform::parse_relaxed(&buf).unwrap();
assert_eq!(relaxed.version, 1);
}
#[test]
fn sato_rejects_sample_index_over_reference_count() {
let payload = build_sato(0, &[Token::Sample(5)]);
assert!(SampleTransform::parse(&payload, 3).is_err());
assert!(SampleTransform::parse(&payload, 5).is_ok());
}
#[test]
fn sato_rejects_reserved_token_values() {
for raw in [33u8, 50, 63, 68, 100, 127, 138, 200, 255] {
let payload = build_sato_raw(0, &[raw]);
let mut p = payload;
p[1] = 1;
assert!(
SampleTransform::parse(&p, 32).is_err(),
"expected error for reserved token {raw}"
);
let relaxed = SampleTransform::parse_relaxed(&p).unwrap();
assert_eq!(relaxed.tokens, vec![Token::Reserved(raw)]);
}
}
#[test]
fn sato_rejects_binary_op_without_enough_operands() {
let payload = build_sato(0, &[Token::Sample(1), Token::Binary(128)]);
assert!(SampleTransform::parse(&payload, 32).is_err());
}
#[test]
fn sato_rejects_expression_with_leftover_stack() {
let payload = build_sato(0, &[Token::Sample(1), Token::Sample(2)]);
let err = SampleTransform::parse(&payload, 32).unwrap_err();
assert!(format!("{err:?}").contains("leaves 2"));
}
#[test]
fn sato_rejects_truncated_token_stream() {
let payload = vec![0u8, 3, 1]; assert!(SampleTransform::parse(&payload, 32).is_err());
assert!(SampleTransform::parse_relaxed(&payload).is_err());
}
#[test]
fn sato_rejects_truncated_constant_payload() {
let mut buf = vec![3u8, 1, 0]; buf.extend_from_slice(&[0u8, 0, 0, 0]); assert!(SampleTransform::parse(&buf, 0).is_err());
}
#[test]
fn sato_num_bits_table_1() {
let mk = |bd: u8| SampleTransform {
version: 0,
bit_depth: bd,
tokens: vec![Token::Constant(0)],
};
assert_eq!(mk(0).num_bits(), 8);
assert_eq!(mk(1).num_bits(), 16);
assert_eq!(mk(2).num_bits(), 32);
assert_eq!(mk(3).num_bits(), 64);
}
#[test]
fn sato_min_max_value_per_bit_depth() {
let mk = |bd: u8| SampleTransform {
version: 0,
bit_depth: bd,
tokens: vec![Token::Constant(0)],
};
assert_eq!(mk(0).min_value(), -128);
assert_eq!(mk(0).max_value(), 127);
assert_eq!(mk(1).min_value(), -32_768);
assert_eq!(mk(1).max_value(), 32_767);
assert_eq!(mk(2).min_value(), i32::MIN as i64);
assert_eq!(mk(2).max_value(), i32::MAX as i64);
assert_eq!(mk(3).min_value(), i64::MIN);
assert_eq!(mk(3).max_value(), i64::MAX);
}
#[test]
fn sato_token_classification_helpers() {
assert!(Token::Constant(0).is_operand());
assert!(Token::Sample(1).is_operand());
assert!(!Token::Unary(64).is_operand());
assert!(!Token::Binary(128).is_operand());
assert!(Token::Unary(64).is_unary());
assert!(!Token::Binary(128).is_unary());
assert!(Token::Binary(128).is_binary());
assert!(!Token::Unary(64).is_binary());
}
#[test]
fn sato_evaluate_errors_when_inputs_short() {
let payload = build_sato(0, &[Token::Sample(3)]);
let st = SampleTransform::parse(&payload, 3).unwrap();
let err = st.evaluate(&[1, 2]).unwrap_err();
assert!(format!("{err:?}").contains("out of range"));
}
use crate::meta::{IrefEntry, ItemInfo, Meta};
fn make_infe(id: u32, item_type: &[u8; 4], flags: u32) -> ItemInfo {
ItemInfo {
id,
item_type: *item_type,
name: String::new(),
content_type: None,
content_encoding: None,
item_uri_type: None,
flags,
}
}
fn build_altr_grpl(group_id: u32, ids: &[u32]) -> Vec<u8> {
let mut child = vec![0u8; 4]; child.extend_from_slice(&group_id.to_be_bytes());
child.extend_from_slice(&(ids.len() as u32).to_be_bytes());
for id in ids {
child.extend_from_slice(&id.to_be_bytes());
}
let size = (8 + child.len()) as u32;
let mut buf = Vec::new();
buf.extend_from_slice(&size.to_be_bytes());
buf.extend_from_slice(b"altr");
buf.extend_from_slice(&child);
buf
}
#[test]
fn audit_tone_map_altr_pairing_compliant() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0), make_infe(2, b"tmap", 0), ],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
}],
grpl: Some(build_altr_grpl(7, &[1, 2])),
..Meta::default()
};
let results = audit_tone_map(&meta);
assert_eq!(results.len(), 1);
let r = &results[0];
assert_eq!(r.tmap_item_id, 2);
assert_eq!(r.base_item_id, Some(1));
assert!(r.gain_map_item_ids.is_empty());
assert!(r.paired_in_altr);
assert!(r.gain_maps_hidden);
assert!(r.is_compliant());
assert!(r.missing().is_empty());
}
#[test]
fn audit_tone_map_hidden_gain_map_compliant() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0), make_infe(2, b"av01", 0x01), make_infe(3, b"tmap", 0), ],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 3,
to_ids: vec![1, 2],
}],
grpl: Some(build_altr_grpl(9, &[1, 3])),
..Meta::default()
};
let r = &audit_tone_map(&meta)[0];
assert_eq!(r.base_item_id, Some(1));
assert_eq!(r.gain_map_item_ids, vec![2]);
assert!(r.paired_in_altr);
assert!(r.gain_maps_hidden);
assert!(r.is_compliant());
}
#[test]
fn audit_tone_map_flags_both_failures() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0), make_infe(2, b"av01", 0), make_infe(3, b"tmap", 0),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 3,
to_ids: vec![1, 2],
}],
grpl: None,
..Meta::default()
};
let r = &audit_tone_map(&meta)[0];
assert!(!r.paired_in_altr);
assert!(!r.gain_maps_hidden);
assert!(!r.is_compliant());
let m = r.missing();
assert!(m.contains(&"altr-pairs-base-and-tmap"));
assert!(m.contains(&"gain-map-hidden"));
}
#[test]
fn audit_tone_map_altr_without_tmap_id_fails_pairing() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"tmap", 0)],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
}],
grpl: Some(build_altr_grpl(11, &[1, 99])), ..Meta::default()
};
let r = &audit_tone_map(&meta)[0];
assert!(!r.paired_in_altr);
assert!(r.gain_maps_hidden);
assert!(!r.is_compliant());
}
#[test]
fn audit_tone_map_no_dimg_iref() {
let meta = Meta {
items: vec![make_infe(2, b"tmap", 0)],
irefs: vec![],
grpl: None,
..Meta::default()
};
let r = &audit_tone_map(&meta)[0];
assert_eq!(r.base_item_id, None);
assert!(r.gain_map_item_ids.is_empty());
assert!(!r.paired_in_altr);
assert!(r.gain_maps_hidden);
}
#[test]
fn audit_tone_map_empty_when_no_tmap_items() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
..Meta::default()
};
assert!(audit_tone_map(&meta).is_empty());
}
#[test]
fn audit_tone_map_reports_each_tmap_item() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0),
make_infe(2, b"tmap", 0),
make_infe(3, b"av01", 0x01), make_infe(4, b"tmap", 0),
],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 4,
to_ids: vec![1, 3],
},
],
grpl: Some(build_altr_grpl(1, &[1, 2, 4])),
..Meta::default()
};
let r = audit_tone_map(&meta);
assert_eq!(r.len(), 2);
assert_eq!(r[0].tmap_item_id, 2);
assert_eq!(r[1].tmap_item_id, 4);
assert!(r[0].is_compliant());
assert!(r[1].is_compliant());
}
#[test]
fn item_info_is_hidden_reads_low_bit_only() {
assert!(!make_infe(1, b"av01", 0).is_hidden());
assert!(make_infe(1, b"av01", 0x01).is_hidden());
assert!(!make_infe(1, b"av01", 0xfffe).is_hidden());
assert!(make_infe(1, b"av01", 0xff03).is_hidden());
}
use crate::meta::{Clap, Imir, Irot, ItemPropertyAssociation, Property, PropertyAssociation};
fn sample_clap() -> Clap {
Clap {
clean_aperture_width_n: 1,
clean_aperture_width_d: 1,
clean_aperture_height_n: 1,
clean_aperture_height_d: 1,
horiz_off_n: 0,
horiz_off_d: 1,
vert_off_n: 0,
vert_off_d: 1,
}
}
fn assoc(item_id: u32, indices: &[u16]) -> ItemPropertyAssociation {
ItemPropertyAssociation {
item_id,
entries: indices
.iter()
.map(|i| PropertyAssociation {
index: *i,
essential: false,
})
.collect(),
}
}
#[test]
fn audit_grid_derivations_clean_chain_compliant() {
let meta = Meta {
items: vec![
make_infe(1, b"grid", 0),
make_infe(2, b"av01", 0),
make_infe(3, b"av01", 0),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
properties: vec![Property::Irot(Irot { angle: 1 })],
associations: vec![assoc(1, &[0])],
..Meta::default()
};
let r = audit_grid_derivations(&meta);
assert_eq!(r.len(), 1);
assert_eq!(r[0].grid_item_id, 1);
assert_eq!(r[0].tile_item_ids, vec![2, 3]);
assert!(
r[0].offenders.is_empty(),
"grid-level transforms must not flag the chain"
);
assert!(r[0].is_compliant());
assert!(r[0].offending_tile_ids().is_empty());
}
#[test]
fn audit_grid_derivations_tile_irot_flagged() {
let meta = Meta {
items: vec![
make_infe(1, b"grid", 0),
make_infe(2, b"av01", 0),
make_infe(3, b"av01", 0),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
properties: vec![Property::Irot(Irot { angle: 2 })],
associations: vec![assoc(3, &[0])],
..Meta::default()
};
let r = &audit_grid_derivations(&meta)[0];
assert!(!r.is_compliant());
assert_eq!(r.offenders, vec![(3, *b"irot")]);
assert_eq!(r.offending_tile_ids(), vec![3]);
}
#[test]
fn audit_grid_derivations_tile_with_all_four_kinds() {
let meta = Meta {
items: vec![make_infe(1, b"grid", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
}],
properties: vec![
Property::Clap(sample_clap()),
Property::Irot(Irot { angle: 1 }),
Property::Imir(Imir { axis: 0 }),
Property::Iscl(crate::meta::Iscl {
target_width_numerator: 1,
target_width_denominator: 2,
target_height_numerator: 1,
target_height_denominator: 2,
}),
],
associations: vec![assoc(2, &[0, 1, 2, 3])],
..Meta::default()
};
let r = &audit_grid_derivations(&meta)[0];
assert_eq!(
r.offenders,
vec![(2, *b"clap"), (2, *b"irot"), (2, *b"imir"), (2, *b"iscl"),]
);
assert_eq!(r.offending_tile_ids(), vec![2]);
}
#[test]
fn audit_grid_derivations_tile_iscl_flagged() {
let meta = Meta {
items: vec![
make_infe(1, b"grid", 0),
make_infe(2, b"av01", 0),
make_infe(3, b"av01", 0),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
properties: vec![Property::Iscl(crate::meta::Iscl {
target_width_numerator: 1,
target_width_denominator: 2,
target_height_numerator: 1,
target_height_denominator: 2,
})],
associations: vec![assoc(2, &[0])],
..Meta::default()
};
let r = &audit_grid_derivations(&meta)[0];
assert!(!r.is_compliant());
assert_eq!(r.offenders, vec![(2, *b"iscl")]);
assert_eq!(r.offending_tile_ids(), vec![2]);
}
#[test]
fn audit_grid_derivations_tile_rref_not_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"grid", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
}],
properties: vec![Property::Rref(crate::meta::Rref {
reference_types: vec![*b"dimg"],
})],
associations: vec![assoc(2, &[0])],
..Meta::default()
};
let r = &audit_grid_derivations(&meta)[0];
assert!(r.is_compliant());
assert!(r.offenders.is_empty());
}
#[test]
fn audit_grid_derivations_grid_level_iscl_permitted() {
let meta = Meta {
items: vec![make_infe(1, b"grid", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
}],
properties: vec![Property::Iscl(crate::meta::Iscl {
target_width_numerator: 1,
target_width_denominator: 2,
target_height_numerator: 1,
target_height_denominator: 2,
})],
associations: vec![assoc(1, &[0])],
..Meta::default()
};
let r = &audit_grid_derivations(&meta)[0];
assert!(r.is_compliant());
assert!(r.offenders.is_empty());
}
#[test]
fn audit_grid_derivations_multiple_offending_tiles() {
let meta = Meta {
items: vec![
make_infe(1, b"grid", 0),
make_infe(2, b"av01", 0),
make_infe(3, b"av01", 0),
make_infe(4, b"av01", 0),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3, 4],
}],
properties: vec![
Property::Imir(Imir { axis: 1 }),
Property::Clap(sample_clap()),
],
associations: vec![
assoc(3, &[0]),
assoc(4, &[1]),
],
..Meta::default()
};
let r = &audit_grid_derivations(&meta)[0];
assert!(!r.is_compliant());
assert_eq!(r.offenders, vec![(3, *b"imir"), (4, *b"clap")]);
assert_eq!(r.offending_tile_ids(), vec![3, 4]);
}
#[test]
fn audit_grid_derivations_empty_when_no_grid_items() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
..Meta::default()
};
assert!(audit_grid_derivations(&meta).is_empty());
}
#[test]
fn audit_grid_derivations_reports_each_grid_item() {
let meta = Meta {
items: vec![
make_infe(1, b"grid", 0), make_infe(2, b"av01", 0),
make_infe(3, b"av01", 0),
make_infe(4, b"grid", 0), make_infe(5, b"av01", 0),
],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 4,
to_ids: vec![5],
},
],
properties: vec![Property::Irot(Irot { angle: 3 })],
associations: vec![assoc(5, &[0])],
..Meta::default()
};
let r = audit_grid_derivations(&meta);
assert_eq!(r.len(), 2);
assert_eq!(r[0].grid_item_id, 1);
assert!(r[0].is_compliant());
assert_eq!(r[1].grid_item_id, 4);
assert!(!r[1].is_compliant());
assert_eq!(r[1].offenders, vec![(5, *b"irot")]);
}
#[test]
fn audit_grid_derivations_grid_without_dimg_is_compliant() {
let meta = Meta {
items: vec![make_infe(1, b"grid", 0)],
..Meta::default()
};
let r = &audit_grid_derivations(&meta)[0];
assert_eq!(r.grid_item_id, 1);
assert!(r.tile_item_ids.is_empty());
assert!(r.is_compliant());
}
use crate::meta::{IlocExtent, ItemLocation};
fn make_iloc_with_body(id: u32) -> ItemLocation {
ItemLocation {
id,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 1000,
length: 16,
extent_index: 0,
}],
}
}
fn make_iloc_zero_length(id: u32) -> ItemLocation {
ItemLocation {
id,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: 0,
extent_index: 0,
}],
}
}
#[test]
fn audit_iden_compliant_no_iloc_entry() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0), make_infe(2, b"iden", 0), ],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
let r = audit_iden_derivations(&meta);
assert_eq!(r.len(), 1);
assert_eq!(r[0].iden_item_id, 2);
assert_eq!(r[0].dimg_reference_count, 1);
assert_eq!(r[0].dimg_iref_count, 1);
assert!(!r[0].has_item_body);
assert_eq!(r[0].source_item_id, Some(1));
assert!(r[0].is_compliant());
assert!(r[0].missing().is_empty());
}
#[test]
fn audit_iden_compliant_iloc_with_zero_length_extent() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"iden", 0)],
locations: vec![make_iloc_zero_length(2)],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
let r = &audit_iden_derivations(&meta)[0];
assert!(!r.has_item_body);
assert!(r.is_compliant());
}
#[test]
fn audit_iden_flags_non_empty_item_body() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"iden", 0)],
locations: vec![make_iloc_with_body(2)],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
let r = &audit_iden_derivations(&meta)[0];
assert!(r.has_item_body);
assert!(!r.is_compliant());
assert!(r.missing().contains(&"no-item-body"));
}
#[test]
fn audit_iden_flags_zero_dimg_inputs() {
let meta = Meta {
items: vec![make_infe(2, b"iden", 0)],
irefs: vec![],
..Meta::default()
};
let r = &audit_iden_derivations(&meta)[0];
assert_eq!(r.dimg_reference_count, 0);
assert_eq!(r.dimg_iref_count, 0);
assert_eq!(r.source_item_id, None);
assert!(!r.is_compliant());
let m = r.missing();
assert!(m.contains(&"dimg-reference-count-eq-1"));
assert!(m.contains(&"dimg-iref-count-eq-1"));
}
#[test]
fn audit_iden_flags_two_dimg_inputs_in_single_iref() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0),
make_infe(2, b"av01", 0),
make_infe(3, b"iden", 0),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 3,
to_ids: vec![1, 2],
}],
..Meta::default()
};
let r = &audit_iden_derivations(&meta)[0];
assert_eq!(r.dimg_reference_count, 2);
assert_eq!(r.dimg_iref_count, 1);
assert_eq!(r.source_item_id, None);
assert!(!r.is_compliant());
assert!(r.missing().contains(&"dimg-reference-count-eq-1"));
assert!(!r.missing().contains(&"dimg-iref-count-eq-1"));
}
#[test]
fn audit_iden_flags_multiple_dimg_iref_entries() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0),
make_infe(2, b"av01", 0),
make_infe(3, b"iden", 0),
],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 3,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 3,
to_ids: vec![2],
},
],
..Meta::default()
};
let r = &audit_iden_derivations(&meta)[0];
assert_eq!(r.dimg_iref_count, 2);
assert_eq!(r.dimg_reference_count, 1);
assert!(!r.is_compliant());
assert!(r.missing().contains(&"dimg-iref-count-eq-1"));
}
#[test]
fn audit_iden_empty_when_no_iden_items() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0),
make_infe(2, b"grid", 0),
make_infe(3, b"tmap", 0),
make_infe(4, b"sato", 0),
make_infe(5, b"iovl", 0),
],
..Meta::default()
};
assert!(audit_iden_derivations(&meta).is_empty());
}
#[test]
fn audit_iden_reports_each_iden_item_in_iinf_order() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0),
make_infe(2, b"iden", 0), make_infe(3, b"av01", 0),
make_infe(4, b"iden", 0), ],
locations: vec![make_iloc_with_body(4)],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 4,
to_ids: vec![3],
},
],
..Meta::default()
};
let r = audit_iden_derivations(&meta);
assert_eq!(r.len(), 2);
assert_eq!(r[0].iden_item_id, 2);
assert!(r[0].is_compliant());
assert_eq!(r[1].iden_item_id, 4);
assert!(!r[1].is_compliant());
assert!(r[1].has_item_body);
}
#[test]
fn audit_iden_ignores_non_dimg_irefs() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0),
make_infe(2, b"iden", 0),
make_infe(9, b"Exif", 0),
],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"cdsc",
from_id: 9,
to_ids: vec![2],
},
],
..Meta::default()
};
let r = &audit_iden_derivations(&meta)[0];
assert_eq!(r.dimg_iref_count, 1);
assert_eq!(r.dimg_reference_count, 1);
assert!(r.is_compliant());
}
use crate::meta::{AuxC, ItemPropertyAssociation as Ipa, PropertyAssociation as Pa};
fn av1c_with_bit_depth(bit_depth: u8) -> Vec<u8> {
let (high, twelve) = match bit_depth {
8 => (0u8, 0u8),
10 => (1, 0),
12 => (1, 1),
_ => panic!("test helper supports 8/10/12 only"),
};
let b2 = (high << 6) | (twelve << 5);
vec![0x81, 0x00, b2]
}
fn ipa(item_id: u32, indices: &[u16]) -> Ipa {
Ipa {
item_id,
entries: indices
.iter()
.map(|i| Pa {
index: *i,
essential: false,
})
.collect(),
}
}
fn make_alpha_auxc() -> AuxC {
AuxC {
aux_type: crate::alpha::ALPHA_URN_PREFIX.to_string(),
aux_subtype: Vec::new(),
}
}
fn make_depth_auxc() -> AuxC {
AuxC {
aux_type: crate::meta::AUX_URN_DEPTH_MPEG.to_string(),
aux_subtype: Vec::new(),
}
}
#[test]
fn decode_av1c_bit_depth_recognises_8_10_12() {
assert_eq!(decode_av1c_bit_depth(&av1c_with_bit_depth(8)), Some(8));
assert_eq!(decode_av1c_bit_depth(&av1c_with_bit_depth(10)), Some(10));
assert_eq!(decode_av1c_bit_depth(&av1c_with_bit_depth(12)), Some(12));
}
#[test]
fn decode_av1c_bit_depth_handles_truncation() {
assert_eq!(decode_av1c_bit_depth(&[]), None);
assert_eq!(decode_av1c_bit_depth(&[0x81]), None);
assert_eq!(decode_av1c_bit_depth(&[0x81, 0x00]), None);
}
#[test]
fn audit_alpha_bit_depth_match_compliant() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0), make_infe(2, b"av01", 0), ],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 2,
to_ids: vec![1],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(8)), Property::Av1C(av1c_with_bit_depth(8)), Property::AuxC(make_alpha_auxc()), ],
associations: vec![ipa(1, &[0]), ipa(2, &[1, 2])],
..Meta::default()
};
let r = audit_alpha_bit_depth(&meta);
assert_eq!(r.len(), 1);
let rec = &r[0];
assert_eq!(rec.alpha_item_id, 2);
assert_eq!(rec.master_item_id, 1);
assert_eq!(rec.alpha_bit_depth, Some(8));
assert_eq!(rec.master_bit_depth, Some(8));
assert!(!rec.alpha_missing_av1c);
assert!(!rec.master_missing_av1c);
assert!(rec.is_compliant());
assert!(rec.missing().is_empty());
}
#[test]
fn audit_alpha_bit_depth_mismatch_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 2,
to_ids: vec![1],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(10)), Property::Av1C(av1c_with_bit_depth(8)), Property::AuxC(make_alpha_auxc()),
],
associations: vec![ipa(1, &[0]), ipa(2, &[1, 2])],
..Meta::default()
};
let rec = &audit_alpha_bit_depth(&meta)[0];
assert_eq!(rec.alpha_bit_depth, Some(8));
assert_eq!(rec.master_bit_depth, Some(10));
assert!(!rec.is_compliant());
assert_eq!(rec.missing(), vec!["alpha-master-bit-depth-mismatch"]);
}
#[test]
fn audit_alpha_bit_depth_12_bit_compliant() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 2,
to_ids: vec![1],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(12)),
Property::Av1C(av1c_with_bit_depth(12)),
Property::AuxC(make_alpha_auxc()),
],
associations: vec![ipa(1, &[0]), ipa(2, &[1, 2])],
..Meta::default()
};
let rec = &audit_alpha_bit_depth(&meta)[0];
assert!(rec.is_compliant());
}
#[test]
fn audit_alpha_bit_depth_alpha_missing_av1c() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 2,
to_ids: vec![1],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(10)),
Property::AuxC(make_alpha_auxc()),
],
associations: vec![ipa(1, &[0]), ipa(2, &[1])],
..Meta::default()
};
let rec = &audit_alpha_bit_depth(&meta)[0];
assert!(rec.alpha_missing_av1c);
assert_eq!(rec.alpha_bit_depth, None);
assert!(!rec.is_compliant());
assert_eq!(rec.missing(), vec!["alpha-item-missing-av1C"]);
}
#[test]
fn audit_alpha_bit_depth_master_missing_av1c() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 2,
to_ids: vec![1],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(10)),
Property::AuxC(make_alpha_auxc()),
],
associations: vec![ipa(2, &[0, 1])],
..Meta::default()
};
let rec = &audit_alpha_bit_depth(&meta)[0];
assert!(rec.master_missing_av1c);
assert_eq!(rec.master_bit_depth, None);
assert!(!rec.is_compliant());
assert_eq!(rec.missing(), vec!["master-item-missing-av1C"]);
}
#[test]
fn audit_alpha_bit_depth_truncated_av1c() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 2,
to_ids: vec![1],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(10)),
Property::Av1C(vec![0x81, 0x00]),
Property::AuxC(make_alpha_auxc()),
],
associations: vec![ipa(1, &[0]), ipa(2, &[1, 2])],
..Meta::default()
};
let rec = &audit_alpha_bit_depth(&meta)[0];
assert!(!rec.alpha_missing_av1c);
assert_eq!(rec.alpha_bit_depth, None);
assert!(!rec.is_compliant());
assert_eq!(rec.missing(), vec!["alpha-item-av1C-truncated"]);
}
#[test]
fn audit_alpha_bit_depth_skips_depth_map_auxiliary() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 2,
to_ids: vec![1],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(10)),
Property::Av1C(av1c_with_bit_depth(8)),
Property::AuxC(make_depth_auxc()), ],
associations: vec![ipa(1, &[0]), ipa(2, &[1, 2])],
..Meta::default()
};
assert!(audit_alpha_bit_depth(&meta).is_empty());
}
#[test]
fn audit_alpha_bit_depth_one_record_per_master() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0), make_infe(2, b"av01", 0), make_infe(3, b"av01", 0), ],
irefs: vec![IrefEntry {
reference_type: *b"auxl",
from_id: 3,
to_ids: vec![1, 2],
}],
properties: vec![
Property::Av1C(av1c_with_bit_depth(8)), Property::Av1C(av1c_with_bit_depth(10)), Property::Av1C(av1c_with_bit_depth(10)), Property::AuxC(make_alpha_auxc()),
],
associations: vec![ipa(1, &[0]), ipa(2, &[1]), ipa(3, &[2, 3])],
..Meta::default()
};
let r = audit_alpha_bit_depth(&meta);
assert_eq!(r.len(), 2);
assert_eq!(r[0].master_item_id, 1);
assert!(!r[0].is_compliant()); assert_eq!(r[1].master_item_id, 2);
assert!(r[1].is_compliant()); }
#[test]
fn audit_alpha_bit_depth_empty_when_no_alpha() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
irefs: Vec::new(),
properties: vec![Property::Av1C(av1c_with_bit_depth(10))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
assert!(audit_alpha_bit_depth(&meta).is_empty());
}
#[test]
fn audit_alpha_bit_depth_multiple_alpha_items() {
let meta = Meta {
items: vec![
make_infe(1, b"av01", 0),
make_infe(2, b"av01", 0),
make_infe(3, b"av01", 0),
make_infe(4, b"av01", 0),
],
irefs: vec![
IrefEntry {
reference_type: *b"auxl",
from_id: 3,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"auxl",
from_id: 4,
to_ids: vec![2],
},
],
properties: vec![
Property::Av1C(av1c_with_bit_depth(8)),
Property::Av1C(av1c_with_bit_depth(10)),
Property::Av1C(av1c_with_bit_depth(8)),
Property::Av1C(av1c_with_bit_depth(10)),
Property::AuxC(make_alpha_auxc()),
],
associations: vec![ipa(1, &[0]), ipa(2, &[1]), ipa(3, &[2, 4]), ipa(4, &[3, 4])],
..Meta::default()
};
let r = audit_alpha_bit_depth(&meta);
assert_eq!(r.len(), 2);
assert_eq!(r[0].alpha_item_id, 3);
assert_eq!(r[0].master_item_id, 1);
assert!(r[0].is_compliant());
assert_eq!(r[1].alpha_item_id, 4);
assert_eq!(r[1].master_item_id, 2);
assert!(r[1].is_compliant());
}
fn obu_with_size(obu_type: u8, payload: &[u8]) -> Vec<u8> {
let hdr = ((obu_type & 0x0f) << 3) | 0b10;
let mut out = vec![hdr];
let mut v = payload.len() as u32;
loop {
let mut b = (v & 0x7f) as u8;
v >>= 7;
if v != 0 {
b |= 0x80;
out.push(b);
} else {
out.push(b);
break;
}
}
out.extend_from_slice(payload);
out
}
fn obu_no_size(obu_type: u8) -> Vec<u8> {
vec![(obu_type & 0x0f) << 3]
}
fn synth_av01_item(payload: &[u8]) -> (Vec<u8>, Meta) {
use crate::meta::{IlocExtent, ItemLocation};
let file = payload.to_vec();
let length = file.len() as u64;
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
locations: vec![ItemLocation {
id: 1,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length,
extent_index: 0,
}],
}],
..Meta::default()
};
(file, meta)
}
#[test]
fn read_leb128_decodes_single_and_multi_byte_values() {
assert_eq!(read_leb128(&[0x05, 0xff]), Ok((5, 1)));
assert_eq!(read_leb128(&[0x80, 0x01, 0xff]), Ok((128, 2)));
assert_eq!(
read_leb128(&[0xff, 0xff, 0xff, 0xff, 0x0f]),
Ok((0xffff_ffff, 5))
);
}
#[test]
fn read_leb128_rejects_truncated_continuation() {
assert!(read_leb128(&[0x80]).is_err());
assert!(read_leb128(&[0x80, 0x80, 0x80]).is_err());
}
#[test]
fn read_leb128_rejects_overlong_sequence() {
assert!(read_leb128(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]).is_err());
}
#[test]
fn audit_sequence_header_obu_single_count_compliant() {
let mut stream = Vec::new();
stream.extend(obu_with_size(2, &[])); stream.extend(obu_with_size(1, &[0x00, 0x01, 0x02])); stream.extend(obu_with_size(6, &[0xab; 7])); let (file, meta) = synth_av01_item(&stream);
let r = audit_sequence_header_obu(&meta, &file);
assert_eq!(r.len(), 1);
let rec = &r[0];
assert_eq!(rec.item_id, 1);
assert_eq!(rec.sequence_header_count, 1);
assert_eq!(rec.total_obu_count, 3);
assert!(rec.is_compliant());
assert!(rec.missing().is_empty());
}
#[test]
fn audit_sequence_header_obu_two_count_flagged() {
let mut stream = Vec::new();
stream.extend(obu_with_size(1, &[0x00, 0x01])); stream.extend(obu_with_size(1, &[0x00, 0x01])); let (file, meta) = synth_av01_item(&stream);
let rec = &audit_sequence_header_obu(&meta, &file)[0];
assert_eq!(rec.sequence_header_count, 2);
assert!(!rec.is_compliant());
assert_eq!(
rec.missing(),
vec!["av01-item-multiple-sequence-header-obus"]
);
}
#[test]
fn audit_sequence_header_obu_zero_count_flagged() {
let mut stream = Vec::new();
stream.extend(obu_with_size(2, &[])); stream.extend(obu_with_size(6, &[0xab; 4])); let (file, meta) = synth_av01_item(&stream);
let rec = &audit_sequence_header_obu(&meta, &file)[0];
assert_eq!(rec.sequence_header_count, 0);
assert_eq!(rec.total_obu_count, 2);
assert!(!rec.is_compliant());
assert_eq!(rec.missing(), vec!["av01-item-missing-sequence-header-obu"]);
}
#[test]
fn audit_sequence_header_obu_size_field_zero_flagged() {
let mut stream = Vec::new();
stream.extend(obu_no_size(1)); stream.extend([0xff, 0xff, 0xff]);
let (file, meta) = synth_av01_item(&stream);
let rec = &audit_sequence_header_obu(&meta, &file)[0];
assert!(rec.has_size_field_zero);
assert!(!rec.is_compliant());
assert_eq!(rec.sequence_header_count, 1);
assert!(rec.missing().contains(&"av01-item-obu-has-size-field-zero"));
}
#[test]
fn audit_sequence_header_obu_truncated_payload_flagged() {
let mut stream = vec![(1u8 << 3) | 0b10]; stream.push(100); stream.extend([0xde, 0xad, 0xbe]); let (file, meta) = synth_av01_item(&stream);
let rec = &audit_sequence_header_obu(&meta, &file)[0];
assert!(rec.truncated_obu);
assert!(!rec.is_compliant());
assert!(rec.missing().contains(&"av01-item-obu-stream-truncated"));
}
#[test]
fn audit_sequence_header_obu_truncated_leb128_flagged() {
let stream = vec![(1u8 << 3) | 0b10, 0x80];
let (file, meta) = synth_av01_item(&stream);
let rec = &audit_sequence_header_obu(&meta, &file)[0];
assert!(rec.truncated_obu);
assert!(!rec.is_compliant());
assert_eq!(rec.sequence_header_count, 0);
assert_eq!(rec.total_obu_count, 0);
}
#[test]
fn audit_sequence_header_obu_missing_iloc_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
..Meta::default()
};
let r = audit_sequence_header_obu(&meta, &[]);
assert_eq!(r.len(), 1);
let rec = &r[0];
assert!(rec.missing_iloc);
assert!(!rec.is_compliant());
assert_eq!(rec.missing(), vec!["av01-item-missing-iloc"]);
}
#[test]
fn audit_sequence_header_obu_ignores_non_av01_items() {
let meta = Meta {
items: vec![
make_infe(1, b"Exif", 0),
make_infe(2, b"iden", 0),
make_infe(3, b"grid", 0),
],
..Meta::default()
};
assert!(audit_sequence_header_obu(&meta, &[]).is_empty());
}
#[test]
fn audit_sequence_header_obu_handles_extension_header() {
let hdr = (1u8 << 3) | 0b110;
let mut stream = vec![hdr];
stream.push(0x00); stream.push(2); stream.extend([0x11, 0x22]); let (file, meta) = synth_av01_item(&stream);
let rec = &audit_sequence_header_obu(&meta, &file)[0];
assert_eq!(rec.sequence_header_count, 1);
assert_eq!(rec.total_obu_count, 1);
assert!(rec.is_compliant());
}
#[test]
fn audit_sequence_header_obu_empty_payload_flagged_missing() {
let (file, meta) = synth_av01_item(&[]);
let rec = &audit_sequence_header_obu(&meta, &file)[0];
assert_eq!(rec.sequence_header_count, 0);
assert_eq!(rec.total_obu_count, 0);
assert!(!rec.missing_iloc);
assert!(!rec.truncated_obu);
assert!(!rec.is_compliant());
assert_eq!(rec.missing(), vec!["av01-item-missing-sequence-header-obu"]);
}
#[test]
fn audit_sequence_header_obu_one_record_per_av01_item() {
use crate::meta::{IlocExtent, ItemLocation};
let mut s1 = Vec::new();
s1.extend(obu_with_size(2, &[])); s1.extend(obu_with_size(1, &[0x00; 3])); let mut s2 = Vec::new();
s2.extend(obu_with_size(2, &[])); let mut file = Vec::new();
let off1 = file.len() as u64;
file.extend(&s1);
let off2 = file.len() as u64;
file.extend(&s2);
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
locations: vec![
ItemLocation {
id: 1,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: off1,
length: s1.len() as u64,
extent_index: 0,
}],
},
ItemLocation {
id: 2,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: off2,
length: s2.len() as u64,
extent_index: 0,
}],
},
],
..Meta::default()
};
let r = audit_sequence_header_obu(&meta, &file);
assert_eq!(r.len(), 2);
assert_eq!(r[0].item_id, 1);
assert!(r[0].is_compliant());
assert_eq!(r[1].item_id, 2);
assert!(!r[1].is_compliant());
assert_eq!(r[1].sequence_header_count, 0);
}
fn push_rational(buf: &mut Vec<u8>, num: i32, den: u32) {
buf.extend_from_slice(&(num as u32).to_be_bytes());
buf.extend_from_slice(&den.to_be_bytes());
}
fn build_singlechannel_gain_map() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.push(0x40); push_rational(&mut buf, 5, 2); push_rational(&mut buf, 8, 1); push_rational(&mut buf, -1, 4); push_rational(&mut buf, 3, 4); push_rational(&mut buf, 1, 1); push_rational(&mut buf, -2, 16); push_rational(&mut buf, 7, 16); buf
}
#[test]
fn gain_map_metadata_parses_single_channel() {
let buf = build_singlechannel_gain_map();
let m = GainMapMetadata::parse(&buf).unwrap();
assert_eq!(m.minimum_version, 0);
assert_eq!(m.writer_version, 0);
assert!(!m.is_multichannel);
assert!(m.use_base_colour_space);
assert_eq!(m.channel_count(), 1);
assert_eq!(
m.base_hdr_headroom,
GainMapRational {
numerator: 5,
denominator: 2
}
);
assert_eq!(
m.alternate_hdr_headroom,
GainMapRational {
numerator: 8,
denominator: 1
}
);
let c = m.channels[0];
assert_eq!(
c.gain_map_min,
GainMapRational {
numerator: -1,
denominator: 4
}
);
assert_eq!(c.gain_map_max.as_f64(), 0.75);
assert_eq!(
c.gamma,
GainMapRational {
numerator: 1,
denominator: 1
}
);
assert_eq!(c.base_offset.as_f64(), -0.125);
assert_eq!(c.alternate_offset.numerator, 7);
}
#[test]
fn gain_map_metadata_parses_three_channels_when_multichannel() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.push(0x80); push_rational(&mut buf, 1, 1); push_rational(&mut buf, 4, 1); for ch in 0..3i32 {
push_rational(&mut buf, ch, 1); push_rational(&mut buf, 10, 1); push_rational(&mut buf, 2, 1); push_rational(&mut buf, 0, 1); push_rational(&mut buf, 0, 1); }
let m = GainMapMetadata::parse(&buf).unwrap();
assert!(m.is_multichannel);
assert!(!m.use_base_colour_space);
assert_eq!(m.channel_count(), 3);
assert_eq!(m.channels[0].gain_map_min.numerator, 0);
assert_eq!(m.channels[1].gain_map_min.numerator, 1);
assert_eq!(m.channels[2].gain_map_min.numerator, 2);
}
#[test]
fn gain_map_metadata_ignores_trailing_bytes() {
let mut buf = build_singlechannel_gain_map();
buf.extend_from_slice(&[0xAA; 32]); let m = GainMapMetadata::parse(&buf).unwrap();
assert_eq!(m.channel_count(), 1);
}
#[test]
fn gain_map_metadata_unknown_min_version_is_unsupported() {
let mut buf = build_singlechannel_gain_map();
buf[0] = 0;
buf[1] = 1; let err = GainMapMetadata::parse(&buf).unwrap_err();
assert!(matches!(err, Error::Unsupported(_)));
}
#[test]
fn gain_map_metadata_rejects_zero_denominator() {
let mut buf = build_singlechannel_gain_map();
buf[9] = 0;
buf[10] = 0;
buf[11] = 0;
buf[12] = 0;
let err = GainMapMetadata::parse(&buf).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn gain_map_metadata_rejects_zero_gamma_numerator() {
let mut buf = build_singlechannel_gain_map();
for b in buf.iter_mut().skip(37).take(4) {
*b = 0;
}
let err = GainMapMetadata::parse(&buf).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn gain_map_metadata_rejects_truncated_channel() {
let mut buf = build_singlechannel_gain_map();
buf.truncate(buf.len() - 1); let err = GainMapMetadata::parse(&buf).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn gain_map_metadata_rejects_max_below_min() {
let mut buf = build_singlechannel_gain_map();
let orig_min = i32::from_be_bytes(buf[21..25].try_into().unwrap());
let orig_max = i32::from_be_bytes(buf[29..33].try_into().unwrap());
buf[21..25].copy_from_slice(&orig_max.to_be_bytes());
buf[29..33].copy_from_slice(&orig_min.to_be_bytes());
let err = GainMapMetadata::parse(&buf).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn gain_map_metadata_accepts_max_equal_to_min() {
let mut buf = build_singlechannel_gain_map();
buf[21..25].copy_from_slice(&0i32.to_be_bytes()); buf[25..29].copy_from_slice(&1u32.to_be_bytes()); buf[29..33].copy_from_slice(&0i32.to_be_bytes()); buf[33..37].copy_from_slice(&1u32.to_be_bytes()); let m = GainMapMetadata::parse(&buf).expect("max==min is permitted");
assert_eq!(m.channels[0].gain_map_min.numerator, 0);
assert_eq!(m.channels[0].gain_map_max.numerator, 0);
}
#[test]
fn gain_map_metadata_rejects_equal_hdr_headrooms() {
let mut buf = build_singlechannel_gain_map();
let base = buf[5..13].to_vec();
buf[13..21].copy_from_slice(&base);
let err = GainMapMetadata::parse(&buf).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn gain_map_metadata_rejects_value_equal_hdr_headrooms() {
let mut buf = build_singlechannel_gain_map();
buf[5..9].copy_from_slice(&1u32.to_be_bytes()); buf[9..13].copy_from_slice(&1u32.to_be_bytes()); buf[13..17].copy_from_slice(&2u32.to_be_bytes()); buf[17..21].copy_from_slice(&2u32.to_be_bytes()); let err = GainMapMetadata::parse(&buf).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-12, "expected {b}, got {a}");
}
#[test]
fn weight_factor_spans_baseline_to_alternate() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
approx(m.weight_factor(2.5), 0.0); approx(m.weight_factor(8.0), 1.0); approx(m.weight_factor(5.25), 0.5); approx(m.weight_factor(-100.0), 0.0);
approx(m.weight_factor(100.0), 1.0);
}
#[test]
fn weight_factor_is_negative_when_alternate_below_baseline() {
let mut buf = build_singlechannel_gain_map();
buf[5..9].copy_from_slice(&5u32.to_be_bytes()); buf[9..13].copy_from_slice(&1u32.to_be_bytes()); buf[13..17].copy_from_slice(&1u32.to_be_bytes()); buf[17..21].copy_from_slice(&1u32.to_be_bytes()); let m = GainMapMetadata::parse(&buf).unwrap();
approx(m.weight_factor(5.0), 0.0); approx(m.weight_factor(1.0), -1.0); approx(m.weight_factor(3.0), -0.5); }
#[test]
fn unnormalize_log2_gain_inverts_normalization() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let ch = m.channels[0];
approx(ch.unnormalize_log2_gain(1.0), 0.75); approx(ch.unnormalize_log2_gain(0.0), -0.25); approx(ch.unnormalize_log2_gain(0.5), 0.25); approx(ch.unnormalize_log2_gain(2.0), 0.75);
approx(ch.unnormalize_log2_gain(-1.0), -0.25);
}
#[test]
fn unnormalize_log2_gain_applies_gamma_inverse() {
let mut buf = build_singlechannel_gain_map();
buf[37..41].copy_from_slice(&2u32.to_be_bytes()); buf[41..45].copy_from_slice(&1u32.to_be_bytes()); let m = GainMapMetadata::parse(&buf).unwrap();
let ch = m.channels[0];
approx(ch.unnormalize_log2_gain(0.25), 0.25);
approx(ch.unnormalize_log2_gain(1.0), 0.75); approx(ch.unnormalize_log2_gain(0.0), -0.25); }
#[test]
fn apply_component_at_baseline_headroom_is_offset_only() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let out = m.apply_component(1.0, 1.0, 2.5, 0).unwrap();
approx(out, 1.0 + (-0.125) - 0.4375);
}
#[test]
fn apply_component_at_full_application() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let out = m.apply_component(1.0, 1.0, 8.0, 0).unwrap();
let expected = (1.0 + (-0.125)) * 0.75f64.exp2() - 0.4375;
approx(out, expected);
}
#[test]
fn apply_rgb_broadcasts_single_channel_metadata() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let baseline = [0.5, 1.0, 0.25];
let gain = [0.0, 0.5, 1.0];
let rgb = m.apply_rgb(baseline, gain, 8.0).unwrap();
for c in 0..3 {
let want = m.apply_component(baseline[c], gain[c], 8.0, c).unwrap();
approx(rgb[c], want);
}
}
#[test]
fn apply_component_indexes_per_component_metadata() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.push(0x80); push_rational(&mut buf, 1, 1); push_rational(&mut buf, 2, 1); let mins = [(0i32, 1u32), (1, 2), (1, 1)]; for (mn, md) in mins {
push_rational(&mut buf, mn, md); push_rational(&mut buf, 2, 1); push_rational(&mut buf, 1, 1); push_rational(&mut buf, 0, 1); push_rational(&mut buf, 0, 1); }
let m = GainMapMetadata::parse(&buf).unwrap();
assert_eq!(m.channel_count(), 3);
let baseline = 1.0;
approx(
m.apply_component(baseline, 0.0, 2.0, 0).unwrap(),
baseline * 0.0f64.exp2(),
);
approx(
m.apply_component(baseline, 0.0, 2.0, 1).unwrap(),
baseline * 0.5f64.exp2(),
);
approx(
m.apply_component(baseline, 0.0, 2.0, 2).unwrap(),
baseline * 1.0f64.exp2(),
);
}
#[test]
fn apply_component_rejects_component_index_above_two() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
assert!(m.apply_component(1.0, 1.0, 8.0, 3).is_none());
}
#[test]
fn apply_component_round_trips_a2_gain() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.push(0x00); push_rational(&mut buf, 1, 1); push_rational(&mut buf, 2, 1); push_rational(&mut buf, 2, 1); push_rational(&mut buf, 2, 1); push_rational(&mut buf, 1, 1); push_rational(&mut buf, 0, 1); push_rational(&mut buf, 0, 1); let m = GainMapMetadata::parse(&buf).unwrap();
approx(m.apply_component(2.0, 0.5, 2.0, 0).unwrap(), 8.0);
}
#[test]
fn apply_plane_rgb_achromatic_matches_per_pixel() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let baseline = [0.25, 0.5, 0.75, 1.0, 0.125, 0.6];
let gain = [0.3, 0.9]; let out = m.apply_plane_rgb(&baseline, &gain, 2, 1, 6.0).unwrap();
assert_eq!(out.len(), 6);
for p in 0..2 {
let base = [baseline[p * 3], baseline[p * 3 + 1], baseline[p * 3 + 2]];
let want = m.apply_rgb(base, [gain[p]; 3], 6.0).unwrap();
for c in 0..3 {
approx(out[p * 3 + c], want[c]);
}
}
}
#[test]
fn apply_plane_rgb_interleaved_uses_per_component_gain() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let baseline = [0.25, 0.5, 0.75]; let gain = [0.0, 0.5, 1.0]; let out = m.apply_plane_rgb(&baseline, &gain, 1, 1, 8.0).unwrap();
for c in 0..3 {
let want = m.apply_component(baseline[c], gain[c], 8.0, c).unwrap();
approx(out[c], want);
}
}
#[test]
fn apply_plane_rgb_rejects_mismatched_lengths() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let err = m
.apply_plane_rgb(&[0.0, 0.0, 0.0], &[0.0, 0.0], 2, 1, 6.0)
.unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
let baseline = [0.0; 6];
let err = m
.apply_plane_rgb(&baseline, &[0.0; 5], 2, 1, 6.0)
.unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn apply_plane_rgb_zero_area_is_empty() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let out = m.apply_plane_rgb(&[], &[], 0, 4, 8.0).unwrap();
assert!(out.is_empty());
}
#[test]
fn normalize_full_range_plane_endpoints_and_guards() {
for depth in [8u8, 10, 12, 16] {
let max = ((1u32 << depth) - 1) as u16;
let out = normalize_full_range_plane(&[0, max, max / 2], depth).unwrap();
approx(out[0], 0.0);
approx(out[1], 1.0);
approx(out[2], (max / 2) as f64 / max as f64);
}
assert!(matches!(
normalize_full_range_plane(&[1024], 10).unwrap_err(),
Error::InvalidData(_)
));
assert!(matches!(
normalize_full_range_plane(&[0], 9).unwrap_err(),
Error::InvalidData(_)
));
}
#[test]
fn apply_plane_rgb_coded_matches_manual_normalization() {
let m = GainMapMetadata::parse(&build_singlechannel_gain_map()).unwrap();
let baseline = [0.25, 0.5, 0.75, 1.0, 0.125, 0.6]; let coded: [u16; 2] = [307, 921]; let manual: Vec<f64> = coded.iter().map(|&s| s as f64 / 1023.0).collect();
let want = m.apply_plane_rgb(&baseline, &manual, 2, 1, 6.0).unwrap();
let got = m
.apply_plane_rgb_coded(&baseline, &coded, 10, 2, 1, 6.0)
.unwrap();
assert_eq!(got.len(), want.len());
for (g, w) in got.iter().zip(&want) {
approx(*g, *w);
}
}
fn av1c_with_profile_level(seq_profile: u8, seq_level_idx_0: u8) -> Vec<u8> {
assert!(seq_profile <= 7);
assert!(seq_level_idx_0 <= 31);
let b1 = (seq_profile << 5) | (seq_level_idx_0 & 0x1F);
vec![0x81, b1, 0x00, 0x00]
}
fn brands_with(baseline: bool, advanced: bool) -> crate::parser::BrandClass {
crate::parser::BrandClass {
is_image: true,
is_miaf: true,
is_baseline_profile: baseline,
is_advanced_profile: advanced,
..crate::parser::BrandClass::default()
}
}
#[test]
fn decode_av1c_byte1_round_trips_profile_and_level() {
let cases = [(0, 13), (0, 14), (1, 16), (1, 17), (2, 0), (7, 31)];
for (p, l) in cases {
let bytes = av1c_with_profile_level(p, l);
assert_eq!(decode_av1c_seq_profile(&bytes), Some(p), "p={p} l={l}");
assert_eq!(decode_av1c_seq_level_idx_0(&bytes), Some(l), "p={p} l={l}");
}
}
#[test]
fn decode_av1c_byte1_handles_truncation() {
assert_eq!(decode_av1c_seq_profile(&[]), None);
assert_eq!(decode_av1c_seq_profile(&[0x81]), None);
assert_eq!(decode_av1c_seq_level_idx_0(&[]), None);
assert_eq!(decode_av1c_seq_level_idx_0(&[0x81]), None);
}
#[test]
fn audit_profile_baseline_main_level_5_1_compliant() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(0, 13))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert_eq!(r[0].profile, AvifProfile::Baseline);
assert_eq!(r[0].seq_profile, Some(0));
assert_eq!(r[0].seq_level_idx_0, Some(13));
assert!(r[0].is_compliant());
assert!(r[0].missing().is_empty());
}
#[test]
fn audit_profile_baseline_level_above_5_1_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(0, 14))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["seq-level-idx-out-of-range"]);
}
#[test]
fn audit_profile_baseline_high_profile_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(1, 13))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["seq-profile-out-of-range"]);
}
#[test]
fn audit_profile_advanced_high_level_6_0_compliant() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(1, 16))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(false, true));
assert_eq!(r.len(), 1);
assert_eq!(r[0].profile, AvifProfile::Advanced);
assert!(r[0].is_compliant());
}
#[test]
fn audit_profile_advanced_main_profile_compliant() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(0, 13))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(false, true));
assert!(r[0].is_compliant());
}
#[test]
fn audit_profile_advanced_professional_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(2, 16))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(false, true));
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["seq-profile-out-of-range"]);
}
#[test]
fn audit_profile_advanced_level_above_6_0_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(1, 17))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(false, true));
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["seq-level-idx-out-of-range"]);
}
#[test]
fn audit_profile_level_31_max_parameters_flagged_for_baseline() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(0, 31))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, false));
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["seq-level-idx-out-of-range"]);
}
#[test]
fn audit_profile_missing_av1c_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![],
associations: vec![],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(r[0].missing_av1c);
assert_eq!(r[0].seq_profile, None);
assert_eq!(r[0].seq_level_idx_0, None);
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["item-missing-av1C"]);
}
#[test]
fn audit_profile_truncated_av1c_flagged() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(vec![0x81])], associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(!r[0].missing_av1c);
assert_eq!(r[0].seq_profile, None);
assert_eq!(r[0].seq_level_idx_0, None);
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["item-av1C-truncated"]);
}
#[test]
fn audit_profile_both_brands_emit_two_records_per_item() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(0, 13))],
associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, true));
assert_eq!(r.len(), 2);
assert_eq!(r[0].profile, AvifProfile::Baseline);
assert_eq!(r[1].profile, AvifProfile::Advanced);
assert!(r[0].is_compliant());
assert!(r[1].is_compliant());
}
#[test]
fn audit_profile_no_brand_claim_returns_empty() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
properties: vec![Property::Av1C(av1c_with_profile_level(2, 31))], associations: vec![ipa(1, &[0])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(false, false));
assert!(r.is_empty());
}
#[test]
fn audit_profile_no_av01_items_returns_empty() {
let meta = Meta {
items: vec![make_infe(1, b"mime", 0)],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, false));
assert!(r.is_empty());
}
#[test]
fn audit_profile_two_items_two_brands_four_records() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
properties: vec![
Property::Av1C(av1c_with_profile_level(0, 13)), Property::Av1C(av1c_with_profile_level(2, 16)), ],
associations: vec![ipa(1, &[0]), ipa(2, &[1])],
..Meta::default()
};
let r = audit_avif_profile_compliance(&meta, &brands_with(true, true));
assert_eq!(r.len(), 4);
assert_eq!(r[0].item_id, 1);
assert_eq!(r[0].profile, AvifProfile::Baseline);
assert!(r[0].is_compliant());
assert_eq!(r[1].item_id, 1);
assert_eq!(r[1].profile, AvifProfile::Advanced);
assert!(r[1].is_compliant());
assert_eq!(r[2].item_id, 2);
assert_eq!(r[2].profile, AvifProfile::Baseline);
assert!(!r[2].is_compliant()); assert_eq!(r[3].item_id, 2);
assert_eq!(r[3].profile, AvifProfile::Advanced);
assert!(!r[3].is_compliant()); }
use crate::meta::{
Clap as MClap, Imir as MImir, Irot as MIrot, Iscl as MIscl, ItemInfo as MII,
ItemPropertyAssociation as MIpa, Meta as MMeta, Property as MProp,
PropertyAssociation as MPa,
};
fn geo_ii(id: u32, item_type: &[u8; 4]) -> MII {
MII {
id,
item_type: *item_type,
name: String::new(),
content_type: None,
content_encoding: None,
item_uri_type: None,
flags: 0,
}
}
fn geo_assoc(item_id: u32, indices: &[u16]) -> MIpa {
MIpa {
item_id,
entries: indices
.iter()
.map(|&index| MPa {
index,
essential: true,
})
.collect(),
}
}
fn ispe_prop(w: u32, h: u32) -> MProp {
MProp::Ispe(crate::meta::Ispe {
width: w,
height: h,
})
}
#[test]
fn dim_transform_rotate_swaps_at_odd_quarter_turns() {
assert_eq!(
DimTransform::Rotate { angle: 0 }.apply_dims(40, 30),
(40, 30)
);
assert_eq!(
DimTransform::Rotate { angle: 1 }.apply_dims(40, 30),
(30, 40)
);
assert_eq!(
DimTransform::Rotate { angle: 2 }.apply_dims(40, 30),
(40, 30)
);
assert_eq!(
DimTransform::Rotate { angle: 3 }.apply_dims(40, 30),
(30, 40)
);
}
#[test]
fn dim_transform_mirror_preserves_dims() {
assert_eq!(
DimTransform::Mirror { axis: 0 }.apply_dims(40, 30),
(40, 30)
);
assert_eq!(
DimTransform::Mirror { axis: 1 }.apply_dims(40, 30),
(40, 30)
);
}
#[test]
fn dim_transform_crop_to_clean_aperture() {
let c = DimTransform::Crop {
width_n: 20,
width_d: 1,
height_n: 10,
height_d: 1,
};
assert_eq!(c.apply_dims(40, 30), (20, 10));
let big = DimTransform::Crop {
width_n: 100,
width_d: 1,
height_n: 10,
height_d: 1,
};
assert_eq!(big.apply_dims(40, 30), (40, 30));
let zero = DimTransform::Crop {
width_n: 20,
width_d: 0,
height_n: 10,
height_d: 1,
};
assert_eq!(zero.apply_dims(40, 30), (40, 30));
}
#[test]
fn dim_transform_scale_ceil() {
let s = DimTransform::Scale {
target_width_numerator: 3,
target_width_denominator: 2,
target_height_numerator: 1,
target_height_denominator: 2,
};
assert_eq!(s.apply_dims(40, 30), (60, 15));
assert_eq!(
DimTransform::Scale {
target_width_numerator: 1,
target_width_denominator: 2,
target_height_numerator: 1,
target_height_denominator: 1,
}
.apply_dims(41, 9),
(21, 9)
);
}
#[test]
fn transform_chain_preserves_ipma_order_skips_descriptive() {
let meta = MMeta {
items: vec![geo_ii(1, b"av01")],
properties: vec![
ispe_prop(100, 50), MProp::Irot(MIrot { angle: 1 }), MProp::Clap(MClap {
clean_aperture_width_n: 40,
clean_aperture_width_d: 1,
clean_aperture_height_n: 30,
clean_aperture_height_d: 1,
horiz_off_n: 0,
horiz_off_d: 1,
vert_off_n: 0,
vert_off_d: 1,
}),
MProp::Imir(MImir { axis: 1 }), ],
associations: vec![geo_assoc(1, &[0, 1, 2, 3])],
..MMeta::default()
};
let chain = transform_chain(&meta, 1);
assert_eq!(chain.len(), 3);
assert_eq!(chain[0], DimTransform::Rotate { angle: 1 });
assert!(matches!(chain[1], DimTransform::Crop { .. }));
assert_eq!(chain[2], DimTransform::Mirror { axis: 1 });
}
#[test]
fn output_dims_folds_chain_in_order() {
let meta = MMeta {
items: vec![geo_ii(1, b"av01")],
properties: vec![
MProp::Irot(MIrot { angle: 1 }),
MProp::Clap(MClap {
clean_aperture_width_n: 40,
clean_aperture_width_d: 1,
clean_aperture_height_n: 30,
clean_aperture_height_d: 1,
horiz_off_n: 0,
horiz_off_d: 1,
vert_off_n: 0,
vert_off_d: 1,
}),
],
associations: vec![geo_assoc(1, &[0, 1])],
..MMeta::default()
};
assert_eq!(output_dims_from_reconstructed(&meta, 1, 100, 50), (40, 30));
let bare = MMeta {
items: vec![geo_ii(9, b"av01")],
..MMeta::default()
};
assert_eq!(output_dims_from_reconstructed(&bare, 9, 100, 50), (100, 50));
}
#[test]
fn reconstructed_dims_coded_uses_ispe() {
let meta = MMeta {
items: vec![geo_ii(1, b"av01")],
properties: vec![ispe_prop(640, 480)],
associations: vec![geo_assoc(1, &[0])],
..MMeta::default()
};
assert_eq!(reconstructed_dims(&meta, 1, &[], None), Some((640, 480)));
let bare = MMeta {
items: vec![geo_ii(2, b"av01")],
..MMeta::default()
};
assert_eq!(reconstructed_dims(&bare, 2, &[], None), None);
}
fn iovl_desc(out_w: u16, out_h: u16, offsets: &[(i16, i16)]) -> Vec<u8> {
let mut buf = vec![0u8, 0u8]; for _ in 0..4 {
buf.extend_from_slice(&0u16.to_be_bytes()); }
buf.extend_from_slice(&out_w.to_be_bytes());
buf.extend_from_slice(&out_h.to_be_bytes());
for &(x, y) in offsets {
buf.extend_from_slice(&x.to_be_bytes());
buf.extend_from_slice(&y.to_be_bytes());
}
buf
}
#[test]
fn resolve_overlays_idat_with_clipping() {
let desc = iovl_desc(200, 200, &[(10, 20), (160, 170)]);
let idat = desc.clone();
let meta = MMeta {
items: vec![geo_ii(1, b"iovl"), geo_ii(2, b"av01"), geo_ii(3, b"av01")],
properties: vec![ispe_prop(80, 60), ispe_prop(80, 60)],
associations: vec![geo_assoc(2, &[0]), geo_assoc(3, &[1])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
locations: vec![ItemLocation {
id: 1,
construction_method: 1, data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: desc.len() as u64,
extent_index: 0,
}],
}],
..MMeta::default()
};
let res = resolve_overlays(&meta, &[], Some(&idat));
assert_eq!(res.len(), 1);
let r = &res[0];
assert_eq!(r.iovl_item_id, 1);
assert_eq!(r.canvas(), (200, 200));
assert_eq!(r.placements.len(), 2);
let p0 = &r.placements[0];
assert_eq!(p0.source_item_id, 2);
assert_eq!((p0.input_width, p0.input_height), (80, 60));
assert!(p0.fully_visible(200, 200));
assert_eq!(p0.visible(200, 200), Some((10, 20, 80, 60)));
let p1 = &r.placements[1];
assert_eq!(p1.source_item_id, 3);
assert!(!p1.fully_visible(200, 200));
assert_eq!(p1.visible(200, 200), Some((160, 170, 40, 30)));
assert!(r.canvas_partially_filled());
}
#[test]
fn overlay_placement_off_canvas() {
let p = OverlayPlacement {
source_item_id: 5,
offset_x: -100,
offset_y: 0,
input_width: 50,
input_height: 50,
};
assert!(p.off_canvas(200, 200));
assert_eq!(p.visible(200, 200), None);
let q = OverlayPlacement { offset_x: -10, ..p };
assert_eq!(q.visible(200, 200), Some((0, 0, 40, 50)));
}
#[test]
fn resolve_iden_applies_iden_transforms() {
let meta = MMeta {
items: vec![geo_ii(1, b"iden"), geo_ii(2, b"av01")],
properties: vec![
ispe_prop(100, 80), MProp::Clap(MClap {
clean_aperture_width_n: 50,
clean_aperture_width_d: 1,
clean_aperture_height_n: 40,
clean_aperture_height_d: 1,
horiz_off_n: 0,
horiz_off_d: 1,
vert_off_n: 0,
vert_off_d: 1,
}),
],
associations: vec![geo_assoc(2, &[0]), geo_assoc(1, &[1])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
}],
..MMeta::default()
};
let res = resolve_iden_derivations(&meta, &[], None);
assert_eq!(res.len(), 1);
let r = &res[0];
assert_eq!(r.iden_item_id, 1);
assert_eq!(r.source_item_id, Some(2));
assert_eq!(r.source_dims, Some((100, 80)));
assert_eq!(r.transforms.len(), 1);
assert_eq!(r.output_dims, Some((50, 40)));
}
#[test]
fn reconstructed_dims_iden_over_grid_with_rotation() {
let mut grid = vec![0u8, 0u8, 0u8, 0u8];
grid.extend_from_slice(&256u16.to_be_bytes());
grid.extend_from_slice(&128u16.to_be_bytes());
let idat = grid.clone();
let meta = MMeta {
items: vec![geo_ii(1, b"iden"), geo_ii(2, b"grid"), geo_ii(3, b"av01")],
properties: vec![
ispe_prop(256, 128), MProp::Irot(MIrot { angle: 1 }), ],
associations: vec![geo_assoc(3, &[0]), geo_assoc(1, &[1])],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![3],
},
],
locations: vec![ItemLocation {
id: 2,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: grid.len() as u64,
extent_index: 0,
}],
}],
..MMeta::default()
};
let res = resolve_iden_derivations(&meta, &[], Some(&idat));
assert_eq!(res.len(), 1);
assert_eq!(res[0].source_dims, Some((256, 128)));
assert_eq!(res[0].output_dims, Some((128, 256)));
}
#[test]
fn reconstructed_dims_cycle_guard() {
let meta = MMeta {
items: vec![geo_ii(1, b"iden"), geo_ii(2, b"iden")],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
},
],
..MMeta::default()
};
assert_eq!(reconstructed_dims(&meta, 1, &[], None), None);
}
#[test]
fn output_dims_with_iscl() {
let meta = MMeta {
items: vec![geo_ii(1, b"av01")],
properties: vec![MProp::Iscl(MIscl {
target_width_numerator: 2,
target_width_denominator: 1,
target_height_numerator: 2,
target_height_denominator: 1,
})],
associations: vec![geo_assoc(1, &[0])],
..MMeta::default()
};
assert_eq!(output_dims_from_reconstructed(&meta, 1, 64, 48), (128, 96));
}
#[test]
fn reconstructed_dims_tmap_takes_base_extents() {
let meta = MMeta {
items: vec![
geo_ii(1, b"tmap"),
geo_ii(2, b"av01"), geo_ii(3, b"av01"), ],
properties: vec![ispe_prop(200, 150), ispe_prop(100, 75)],
associations: vec![geo_assoc(2, &[0]), geo_assoc(3, &[1])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
..MMeta::default()
};
assert_eq!(reconstructed_dims(&meta, 1, &[], None), Some((200, 150)));
}
#[test]
fn reconstructed_dims_tmap_folds_base_transforms() {
let meta = MMeta {
items: vec![geo_ii(1, b"tmap"), geo_ii(2, b"av01"), geo_ii(3, b"av01")],
properties: vec![
ispe_prop(200, 150), MProp::Irot(MIrot { angle: 1 }), ispe_prop(100, 75), ],
associations: vec![geo_assoc(2, &[0, 1]), geo_assoc(3, &[2])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
..MMeta::default()
};
assert_eq!(reconstructed_dims(&meta, 1, &[], None), Some((150, 200)));
}
#[test]
fn resolve_tone_maps_reports_geometry_and_upsampling() {
let meta = MMeta {
items: vec![geo_ii(1, b"tmap"), geo_ii(2, b"av01"), geo_ii(3, b"av01")],
properties: vec![ispe_prop(200, 150), ispe_prop(100, 75)],
associations: vec![geo_assoc(2, &[0]), geo_assoc(3, &[1])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
..MMeta::default()
};
let res = resolve_tone_maps(&meta, &[], None);
assert_eq!(res.len(), 1);
let r = &res[0];
assert_eq!(r.tmap_item_id, 1);
assert_eq!(r.base_item_id, Some(2));
assert_eq!(r.gain_map_item_ids, vec![3]);
assert_eq!(r.rendered_dims, Some((200, 150)));
assert_eq!(r.gain_map_dims, vec![(3, Some((100, 75)))]);
assert!(r.upsamples_gain_map());
}
#[test]
fn resolve_tone_maps_no_upsampling_when_gain_map_matches_base() {
let meta = MMeta {
items: vec![geo_ii(1, b"tmap"), geo_ii(2, b"av01"), geo_ii(3, b"av01")],
properties: vec![ispe_prop(200, 150)],
associations: vec![geo_assoc(2, &[0]), geo_assoc(3, &[0])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
..MMeta::default()
};
let r = &resolve_tone_maps(&meta, &[], None)[0];
assert_eq!(r.rendered_dims, Some((200, 150)));
assert_eq!(r.gain_map_dims, vec![(3, Some((200, 150)))]);
assert!(!r.upsamples_gain_map());
}
#[test]
fn reconstructed_dims_sato_reads_own_ispe() {
let meta = MMeta {
items: vec![geo_ii(1, b"sato"), geo_ii(2, b"av01"), geo_ii(3, b"av01")],
properties: vec![ispe_prop(320, 240)],
associations: vec![geo_assoc(1, &[0])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
..MMeta::default()
};
assert_eq!(reconstructed_dims(&meta, 1, &[], None), Some((320, 240)));
}
#[test]
fn reconstructed_dims_sato_falls_back_to_input_extents() {
let meta = MMeta {
items: vec![geo_ii(1, b"sato"), geo_ii(2, b"av01"), geo_ii(3, b"av01")],
properties: vec![ispe_prop(320, 240)],
associations: vec![geo_assoc(2, &[0]), geo_assoc(3, &[0])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
}],
..MMeta::default()
};
assert_eq!(reconstructed_dims(&meta, 1, &[], None), Some((320, 240)));
}
fn build_grid_desc(rows: u8, cols: u8, out_w: u16, out_h: u16) -> Vec<u8> {
let mut b = vec![0u8, 0u8, rows.saturating_sub(1), cols.saturating_sub(1)];
b.extend_from_slice(&out_w.to_be_bytes());
b.extend_from_slice(&out_h.to_be_bytes());
b
}
#[test]
fn resolve_grids_places_tiles_and_trims_edges() {
let desc = build_grid_desc(2, 2, 180, 180);
let idat = desc.clone();
let meta = MMeta {
items: vec![
geo_ii(1, b"grid"),
geo_ii(2, b"av01"),
geo_ii(3, b"av01"),
geo_ii(4, b"av01"),
geo_ii(5, b"av01"),
],
properties: vec![ispe_prop(100, 100)],
associations: vec![
geo_assoc(2, &[0]),
geo_assoc(3, &[0]),
geo_assoc(4, &[0]),
geo_assoc(5, &[0]),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3, 4, 5],
}],
locations: vec![ItemLocation {
id: 1,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: desc.len() as u64,
extent_index: 0,
}],
}],
..MMeta::default()
};
let res = resolve_grids(&meta, &[], Some(&idat));
assert_eq!(res.len(), 1);
let r = &res[0];
assert_eq!(r.grid_item_id, 1);
assert_eq!(r.canvas(), (180, 180));
assert_eq!(r.tile_dims, Some((100, 100)));
assert!(r.covers_canvas());
assert_eq!(r.placements.len(), 4);
assert_eq!(
r.placements[0],
GridTilePlacement {
source_item_id: 2,
row: 0,
col: 0,
origin_x: 0,
origin_y: 0
}
);
assert_eq!(
r.placements[3],
GridTilePlacement {
source_item_id: 5,
row: 1,
col: 1,
origin_x: 100,
origin_y: 100
}
);
assert_eq!(
r.placements[0].visible(100, 100, 180, 180),
Some((0, 0, 100, 100))
);
assert_eq!(
r.placements[3].visible(100, 100, 180, 180),
Some((100, 100, 80, 80))
);
assert!(!r.placements[0].is_trimmed(100, 100, 180, 180));
assert!(r.placements[3].is_trimmed(100, 100, 180, 180));
assert_eq!(r.trimmed_tile_count(), 3);
}
#[test]
fn resolve_grids_no_trim_on_exact_multiple() {
let desc = build_grid_desc(1, 3, 192, 64);
let idat = desc.clone();
let meta = MMeta {
items: vec![
geo_ii(1, b"grid"),
geo_ii(2, b"av01"),
geo_ii(3, b"av01"),
geo_ii(4, b"av01"),
],
properties: vec![ispe_prop(64, 64)],
associations: vec![geo_assoc(2, &[0]), geo_assoc(3, &[0]), geo_assoc(4, &[0])],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3, 4],
}],
locations: vec![ItemLocation {
id: 1,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: desc.len() as u64,
extent_index: 0,
}],
}],
..MMeta::default()
};
let r = &resolve_grids(&meta, &[], Some(&idat))[0];
assert_eq!(r.tile_dims, Some((64, 64)));
assert!(r.covers_canvas());
assert_eq!(r.trimmed_tile_count(), 0);
assert_eq!(r.placements[2].origin_x, 128);
}
#[test]
fn resolve_grids_resolves_cm2_item_offset_descriptor() {
let desc = build_grid_desc(1, 2, 128, 64);
let mut file = vec![0u8; 8];
file.extend_from_slice(&desc);
let meta = MMeta {
items: vec![
geo_ii(1, b"grid"),
geo_ii(2, b"av01"),
geo_ii(3, b"av01"),
geo_ii(10, b"av01"), ],
properties: vec![ispe_prop(64, 64)],
associations: vec![geo_assoc(2, &[0]), geo_assoc(3, &[0])],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
},
IrefEntry {
reference_type: *b"iloc",
from_id: 1,
to_ids: vec![10],
},
],
locations: vec![
ItemLocation {
id: 1,
construction_method: 2,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: 0,
extent_index: 1,
}],
},
ItemLocation {
id: 10,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 8,
length: desc.len() as u64,
extent_index: 0,
}],
},
],
..MMeta::default()
};
let res = resolve_grids(&meta, &file, None);
assert_eq!(res.len(), 1, "cm=2 grid descriptor must resolve");
let r = &res[0];
assert_eq!(r.canvas(), (128, 64));
assert_eq!(r.placements.len(), 2);
assert_eq!(reconstructed_dims(&meta, 1, &file, None), Some((128, 64)));
}
#[test]
fn derivation_graph_coded_root_single_node() {
let meta = MMeta {
items: vec![geo_ii(1, b"av01")],
properties: vec![ispe_prop(640, 480)],
associations: vec![geo_assoc(1, &[0])],
..MMeta::default()
};
let g = build_derivation_graph(&meta, 1, &[], None);
assert_eq!(g.root_item_id, 1);
assert_eq!(g.nodes.len(), 1);
assert!(g.root_is_coded());
assert_eq!(g.nodes[0].kind, DerivationKind::Coded);
assert_eq!(g.nodes[0].reconstructed_dims, Some((640, 480)));
assert_eq!(g.nodes[0].output_dims, Some((640, 480)));
assert_eq!(g.output_dims(), Some((640, 480)));
assert_eq!(g.coded_leaf_ids, vec![1]);
assert_eq!(g.derived_node_count(), 0);
assert!(!g.truncated);
}
#[test]
fn derivation_graph_grid_root_lists_tiles() {
let desc = build_grid_desc(2, 2, 200, 200);
let idat = desc.clone();
let meta = MMeta {
items: vec![
geo_ii(1, b"grid"),
geo_ii(2, b"av01"),
geo_ii(3, b"av01"),
geo_ii(4, b"av01"),
geo_ii(5, b"av01"),
],
properties: vec![ispe_prop(100, 100)],
associations: vec![
geo_assoc(2, &[0]),
geo_assoc(3, &[0]),
geo_assoc(4, &[0]),
geo_assoc(5, &[0]),
],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3, 4, 5],
}],
locations: vec![ItemLocation {
id: 1,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: desc.len() as u64,
extent_index: 0,
}],
}],
..MMeta::default()
};
let g = build_derivation_graph(&meta, 1, &[], Some(&idat));
assert!(!g.root_is_coded());
assert_eq!(g.nodes[0].kind, DerivationKind::Grid);
assert_eq!(g.nodes[0].inputs, vec![2, 3, 4, 5]);
assert_eq!(g.output_dims(), Some((200, 200)));
assert_eq!(g.coded_leaf_ids, vec![2, 3, 4, 5]);
assert_eq!(g.nodes.len(), 5);
assert_eq!(g.derived_node_count(), 1);
assert_eq!(g.node(1).unwrap().depth, 0);
assert_eq!(g.node(2).unwrap().depth, 1);
}
#[test]
fn derivation_graph_iden_over_grid_nests() {
let desc = build_grid_desc(2, 2, 200, 200);
let idat = desc.clone();
let meta = MMeta {
items: vec![
geo_ii(1, b"iden"),
geo_ii(2, b"grid"),
geo_ii(3, b"av01"),
geo_ii(4, b"av01"),
geo_ii(5, b"av01"),
geo_ii(6, b"av01"),
],
properties: vec![
ispe_prop(100, 100),
MProp::Clap(MClap {
clean_aperture_width_n: 150,
clean_aperture_width_d: 1,
clean_aperture_height_n: 120,
clean_aperture_height_d: 1,
horiz_off_n: 0,
horiz_off_d: 1,
vert_off_n: 0,
vert_off_d: 1,
}),
],
associations: vec![
geo_assoc(1, &[1]), geo_assoc(3, &[0]),
geo_assoc(4, &[0]),
geo_assoc(5, &[0]),
geo_assoc(6, &[0]),
],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![3, 4, 5, 6],
},
],
locations: vec![ItemLocation {
id: 2,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: desc.len() as u64,
extent_index: 0,
}],
}],
..MMeta::default()
};
let g = build_derivation_graph(&meta, 1, &[], Some(&idat));
assert_eq!(g.nodes[0].kind, DerivationKind::Identity);
assert_eq!(g.nodes[0].reconstructed_dims, Some((200, 200)));
assert_eq!(g.nodes[0].output_dims, Some((150, 120)));
assert_eq!(g.output_dims(), Some((150, 120)));
assert_eq!(g.coded_leaf_ids, vec![3, 4, 5, 6]);
assert_eq!(g.node(1).unwrap().depth, 0);
assert_eq!(g.node(2).unwrap().depth, 1);
assert_eq!(g.node(3).unwrap().depth, 2);
assert_eq!(g.derived_node_count(), 2);
}
#[test]
fn derivation_graph_diamond_dedups_shared_leaf() {
let ovl = {
let mut b = vec![0u8, 0u8]; b.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); b.extend_from_slice(&100u16.to_be_bytes()); b.extend_from_slice(&100u16.to_be_bytes()); b.extend_from_slice(&0i16.to_be_bytes()); b.extend_from_slice(&0i16.to_be_bytes()); b.extend_from_slice(&0i16.to_be_bytes()); b.extend_from_slice(&0i16.to_be_bytes()); b
};
let idat = ovl.clone();
let meta = MMeta {
items: vec![
geo_ii(1, b"iovl"),
geo_ii(2, b"iden"),
geo_ii(3, b"iden"),
geo_ii(4, b"av01"),
],
properties: vec![ispe_prop(50, 50)],
associations: vec![geo_assoc(4, &[0])],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2, 3],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![4],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 3,
to_ids: vec![4],
},
],
locations: vec![ItemLocation {
id: 1,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![IlocExtent {
offset: 0,
length: ovl.len() as u64,
extent_index: 0,
}],
}],
..MMeta::default()
};
let g = build_derivation_graph(&meta, 1, &[], Some(&idat));
assert_eq!(g.nodes[0].kind, DerivationKind::Overlay);
assert_eq!(g.coded_leaf_ids, vec![4]);
assert_eq!(g.nodes.len(), 4);
assert_eq!(g.derived_node_count(), 3);
assert!(!g.truncated);
assert_eq!(g.coded_leaf_dims(), vec![(4, Some((50, 50)))]);
assert_eq!(g.nodes_at_depth(0), vec![1]);
assert_eq!(g.nodes_at_depth(1), vec![2, 3]);
assert_eq!(g.nodes_at_depth(2), vec![4]);
}
#[test]
fn derivation_graph_cycle_terminates_and_flags_truncation() {
let meta = MMeta {
items: vec![geo_ii(1, b"iden"), geo_ii(2, b"iden")],
irefs: vec![
IrefEntry {
reference_type: *b"dimg",
from_id: 1,
to_ids: vec![2],
},
IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![1],
},
],
..MMeta::default()
};
let g = build_derivation_graph(&meta, 1, &[], None);
assert_eq!(g.nodes.len(), 2);
assert!(g.coded_leaf_ids.is_empty());
assert_eq!(g.output_dims(), None);
}
#[test]
fn resolve_tone_maps_handles_inputless_tmap() {
let meta = MMeta {
items: vec![geo_ii(1, b"tmap")],
..MMeta::default()
};
let r = &resolve_tone_maps(&meta, &[], None)[0];
assert_eq!(r.base_item_id, None);
assert!(r.gain_map_item_ids.is_empty());
assert_eq!(r.rendered_dims, None);
assert!(!r.upsamples_gain_map());
assert_eq!(reconstructed_dims(&meta, 1, &[], None), None);
}
struct SplitMix64(u64);
impl SplitMix64 {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn bytes(&mut self, max_len: usize) -> Vec<u8> {
let len = (self.next() as usize) % (max_len + 1);
(0..len).map(|_| (self.next() & 0xff) as u8).collect()
}
}
#[test]
fn fuzz_image_overlay_parse_is_total() {
let mut rng = SplitMix64(0x0AF1_2026_0341_0001);
for _ in 0..20_000 {
let buf = rng.bytes(48);
let refs = (rng.next() as usize) % 8;
let _ = ImageOverlay::parse(&buf, refs);
}
}
#[test]
fn fuzz_gain_map_metadata_parse_is_total() {
let mut rng = SplitMix64(0x0AF1_2026_0341_0002);
for _ in 0..20_000 {
let buf = rng.bytes(160);
let _ = GainMapMetadata::parse(&buf);
}
}
#[test]
fn fuzz_sample_transform_parse_is_total() {
let mut rng = SplitMix64(0x0AF1_2026_0341_0003);
for _ in 0..20_000 {
let buf = rng.bytes(64);
let refs = (rng.next() % 8) as u32;
let _ = SampleTransform::parse(&buf, refs);
}
}
#[test]
fn fuzz_parse_grpl_is_total() {
let mut rng = SplitMix64(0x0AF1_2026_0341_0004);
for _ in 0..20_000 {
let buf = rng.bytes(80);
let _ = parse_grpl(&buf);
}
}
#[test]
fn fuzz_geometry_resolver_is_total() {
let mut rng = SplitMix64(0x0AF1_2026_0341_0005);
let types: [&[u8; 4]; 5] = [b"av01", b"grid", b"iovl", b"iden", b"sato"];
for _ in 0..5_000 {
let item_count = 1 + (rng.next() as usize % 6);
let mut items = Vec::new();
let mut properties = Vec::new();
let mut associations = Vec::new();
let mut irefs = Vec::new();
let mut locations = Vec::new();
for i in 0..item_count {
let id = (i + 1) as u32;
let t = types[(rng.next() as usize) % types.len()];
items.push(geo_ii(id, t));
let w = (rng.next() % 600) as u32;
let h = (rng.next() % 600) as u32;
properties.push(ispe_prop(w, h));
associations.push(geo_assoc(id, &[(properties.len() - 1) as u16]));
if rng.next() & 1 == 0 {
let to = 1 + (rng.next() as u32 % item_count as u32);
irefs.push(IrefEntry {
reference_type: *b"dimg",
from_id: id,
to_ids: vec![to],
});
}
if rng.next() & 1 == 0 {
locations.push(ItemLocation {
id,
construction_method: (rng.next() % 3) as u8,
data_reference_index: 0,
base_offset: rng.next() % 64,
extents: vec![IlocExtent {
offset: rng.next() % 64,
length: rng.next() % 64,
extent_index: 0,
}],
});
}
}
let meta = MMeta {
items,
properties,
associations,
irefs,
locations,
..MMeta::default()
};
let file = rng.bytes(128);
let idat = rng.bytes(128);
let _ = resolve_overlays(&meta, &file, Some(&idat));
let _ = resolve_iden_derivations(&meta, &file, Some(&idat));
let _ = resolve_grids(&meta, &file, Some(&idat));
let _ = resolve_tone_maps(&meta, &file, Some(&idat));
for id in 1..=item_count as u32 {
let _ = reconstructed_dims(&meta, id, &file, Some(&idat));
let g = build_derivation_graph(&meta, id, &file, Some(&idat));
let mut seen = std::collections::BTreeSet::new();
for n in &g.nodes {
assert!(seen.insert(n.item_id), "duplicate node {}", n.item_id);
assert!(n.depth <= MAX_DERIVATION_DEPTH);
}
let mut leaves = std::collections::BTreeSet::new();
for &leaf in &g.coded_leaf_ids {
assert!(leaves.insert(leaf), "duplicate coded leaf {leaf}");
}
}
}
}
#[test]
fn pred_audit_no_claim_is_compliant() {
let meta = Meta {
items: vec![make_infe(1, b"av01", 0)],
..Meta::default()
};
let c = audit_pred_brand(&meta, &[b(b"avif"), b(b"mif1")]);
assert!(!c.claims_pred);
assert!(c.is_compliant());
assert!(c.missing().is_empty());
}
#[test]
fn pred_audit_closure_satisfied() {
let meta = Meta {
primary_item_id: Some(1),
items: vec![
make_infe(1, b"av01", 0), make_infe(2, b"av01", 0), make_infe(3, b"av01", 0), ],
irefs: vec![IrefEntry {
reference_type: *b"pred",
from_id: 2,
to_ids: vec![1, 3],
}],
..Meta::default()
};
let c = audit_pred_brand(&meta, &[b(b"avif"), b(b"pred")]);
assert!(c.claims_pred);
assert_eq!(c.predictive_item_count, 1);
assert!(c.missing_dependency_ids.is_empty());
assert!(c.is_compliant());
}
#[test]
fn pred_audit_missing_dependency_flagged() {
let meta = Meta {
primary_item_id: Some(1),
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"pred",
from_id: 2,
to_ids: vec![1, 99], }],
..Meta::default()
};
let c = audit_pred_brand(&meta, &[b(b"pred")]);
assert_eq!(c.missing_dependency_ids, vec![99]);
assert!(!c.is_compliant());
assert!(c.missing().contains(&"pred-dependency-closure"));
}
#[test]
fn pred_audit_mif1_primary_must_be_independent() {
let meta = Meta {
primary_item_id: Some(1),
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"pred",
from_id: 1, to_ids: vec![2],
}],
..Meta::default()
};
let c = audit_pred_brand(&meta, &[b(b"mif1"), b(b"pred")]);
assert!(c.claims_mif1 && c.claims_pred);
assert!(c.primary_not_independent);
assert!(!c.is_compliant());
assert!(c.missing().contains(&"mif1-primary-independent"));
}
#[test]
fn pred_audit_no_mif1_allows_predictive_primary() {
let meta = Meta {
primary_item_id: Some(1),
items: vec![make_infe(1, b"av01", 0), make_infe(2, b"av01", 0)],
irefs: vec![IrefEntry {
reference_type: *b"pred",
from_id: 1,
to_ids: vec![2],
}],
..Meta::default()
};
let c = audit_pred_brand(&meta, &[b(b"pred")]);
assert!(!c.claims_mif1);
assert!(!c.primary_not_independent);
assert!(c.is_compliant());
}
}