use crate::error::{AvifError as Error, Result};
#[cfg(test)]
use crate::box_parser::iter_boxes;
use crate::box_parser::{b, parse_full_box, read_cstr, read_u16, read_u32, type_str, BoxType};
use oxideav_heif::boxes::FourCc;
use oxideav_heif::meta as hmeta;
use oxideav_heif::props as hprops;
pub const ITEM_TYPE_MIME: BoxType = b(b"mime");
pub const ITEM_TYPE_URI: BoxType = b(b"uri ");
pub const ITEM_TYPE_EXIF: BoxType = b(b"Exif");
const AV1C: BoxType = b(b"av1C");
const ISPE: BoxType = b(b"ispe");
const COLR: BoxType = b(b"colr");
const PIXI: BoxType = b(b"pixi");
const PASP: BoxType = b(b"pasp");
const IROT: BoxType = b(b"irot");
const IMIR: BoxType = b(b"imir");
const CLAP: BoxType = b(b"clap");
const AUXC: BoxType = b(b"auxC");
const MDCV: BoxType = b(b"mdcv");
const CLLI: BoxType = b(b"clli");
const CCLV: BoxType = b(b"cclv");
const AMVE: BoxType = b(b"amve");
const RLOC: BoxType = b(b"rloc");
const LSEL: BoxType = b(b"lsel");
const A1OP: BoxType = b(b"a1op");
const A1LX: BoxType = b(b"a1lx");
const ISCL: BoxType = b(b"iscl");
const RREF: BoxType = b(b"rref");
const CRTT: BoxType = b(b"crtt");
const MDFT: BoxType = b(b"mdft");
const UDES: BoxType = b(b"udes");
const ALTT: BoxType = b(b"altt");
const AEBR: BoxType = b(b"aebr");
const WBBR: BoxType = b(b"wbbr");
const FOBR: BoxType = b(b"fobr");
const AFBR: BoxType = b(b"afbr");
const DOBR: BoxType = b(b"dobr");
const PANO: BoxType = b(b"pano");
const SUBS: BoxType = b(b"subs");
const TOLS: BoxType = b(b"tols");
const WIPE: BoxType = b(b"wipe");
const ZOOM: BoxType = b(b"zoom");
const FADE: BoxType = b(b"fade");
const SPLT: BoxType = b(b"splt");
const STPE: BoxType = b(b"stpe");
const SSLD: BoxType = b(b"ssld");
const PRDI: BoxType = b(b"prdi");
const SSTR: BoxType = b(b"sstr");
const CMEX: BoxType = b(b"cmex");
const CMIN: BoxType = b(b"cmin");
const TXLO: BoxType = b(b"txlo");
const ELNG: BoxType = b(b"elng");
const FNCH: BoxType = b(b"fnch");
const MSKC: BoxType = b(b"mskC");
const REVE: BoxType = b(b"reve");
const NDWT: BoxType = b(b"ndwt");
const CEXG: BoxType = b(b"cexg");
const DADJ: BoxType = b(b"dadj");
const STAG: BoxType = b(b"stag");
const TILC: BoxType = b(b"tilC");
pub const ITEM_TYPE_IOVL: BoxType = b(b"iovl");
pub const ITEM_TYPE_IDEN: BoxType = b(b"iden");
pub const ITEM_TYPE_SATO: BoxType = b(b"sato");
pub const ITEM_TYPE_TMAP: BoxType = b(b"tmap");
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AuxKind {
Alpha,
DepthMap,
HdrGainMap,
Other,
}
pub const AUX_URN_ALPHA_MPEG: &str = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha";
pub const AUX_URN_ALPHA_HEVC: &str = "urn:mpeg:hevc:2015:auxid:1";
pub const AUX_URN_DEPTH_MPEG: &str = "urn:mpeg:mpegB:cicp:systems:auxiliary:depth";
pub const AUX_URN_DEPTH_HEVC: &str = "urn:mpeg:hevc:2015:auxid:2";
pub const AUX_URN_HDR_GAINMAP: &str = "urn:com:apple:photo:2020:aux:hdrgainmap";
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct ItemInfo {
pub id: u32,
pub item_type: BoxType,
pub name: String,
pub content_type: Option<String>,
pub content_encoding: Option<String>,
pub item_uri_type: Option<String>,
pub flags: u32,
}
impl ItemInfo {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: u32,
item_type: BoxType,
name: String,
content_type: Option<String>,
content_encoding: Option<String>,
item_uri_type: Option<String>,
flags: u32,
) -> Self {
Self {
id,
item_type,
name,
content_type,
content_encoding,
item_uri_type,
flags,
}
}
}
impl ItemInfo {
pub fn is_hidden(&self) -> bool {
(self.flags & 0x01) == 0x01
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Default)]
pub struct IlocExtent {
pub offset: u64,
pub length: u64,
pub extent_index: u64,
}
impl IlocExtent {
#[allow(clippy::too_many_arguments)]
pub fn new(offset: u64, length: u64, extent_index: u64) -> Self {
Self {
offset,
length,
extent_index,
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct ItemLocation {
pub id: u32,
pub construction_method: u8,
pub data_reference_index: u16,
pub base_offset: u64,
pub extents: Vec<IlocExtent>,
}
impl ItemLocation {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: u32,
construction_method: u8,
data_reference_index: u16,
base_offset: u64,
extents: Vec<IlocExtent>,
) -> Self {
Self {
id,
construction_method,
data_reference_index,
base_offset,
extents,
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct ItemPropertyAssociation {
pub item_id: u32,
pub entries: Vec<PropertyAssociation>,
}
impl ItemPropertyAssociation {
#[allow(clippy::too_many_arguments)]
pub fn new(item_id: u32, entries: Vec<PropertyAssociation>) -> Self {
Self { item_id, entries }
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub struct PropertyAssociation {
pub index: u16,
pub essential: bool,
}
impl PropertyAssociation {
#[allow(clippy::too_many_arguments)]
pub fn new(index: u16, essential: bool) -> Self {
Self { index, essential }
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct Ispe {
pub width: u32,
pub height: u32,
}
impl Ispe {
#[allow(clippy::too_many_arguments)]
pub fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pixi {
pub bits_per_channel: Vec<u8>,
pub channels: Vec<hprops::PixiChannel>,
}
impl Pixi {
#[allow(clippy::too_many_arguments)]
pub fn new(bits_per_channel: Vec<u8>, channels: Vec<hprops::PixiChannel>) -> Self {
Self {
bits_per_channel,
channels,
}
}
}
impl Pixi {
pub fn num_channels(&self) -> usize {
self.bits_per_channel.len()
}
pub fn max_bit_depth(&self) -> u8 {
self.bits_per_channel.iter().copied().max().unwrap_or(0)
}
pub fn is_uniform_depth(&self) -> bool {
match self.bits_per_channel.first() {
None => false,
Some(&first) => self.bits_per_channel.iter().all(|&b| b == first),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Pasp {
pub h_spacing: u32,
pub v_spacing: u32,
}
impl Pasp {
#[allow(clippy::too_many_arguments)]
pub fn new(h_spacing: u32, v_spacing: u32) -> Self {
Self {
h_spacing,
v_spacing,
}
}
}
impl Pasp {
pub fn is_square(&self) -> bool {
self.h_spacing != 0 && self.v_spacing != 0 && self.h_spacing == self.v_spacing
}
pub fn ratio(&self) -> Option<f64> {
if self.v_spacing == 0 {
None
} else {
Some(self.h_spacing as f64 / self.v_spacing as f64)
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum Colr {
Nclx {
colour_primaries: u16,
transfer_characteristics: u16,
matrix_coefficients: u16,
full_range: bool,
},
Icc(Vec<u8>),
Unknown(BoxType),
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct Irot {
pub angle: u8,
}
impl Irot {
#[allow(clippy::too_many_arguments)]
pub fn new(angle: u8) -> Self {
Self { angle }
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct Imir {
pub axis: u8,
}
impl Imir {
#[allow(clippy::too_many_arguments)]
pub fn new(axis: u8) -> Self {
Self { axis }
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct Clap {
pub clean_aperture_width_n: i32,
pub clean_aperture_width_d: i32,
pub clean_aperture_height_n: i32,
pub clean_aperture_height_d: i32,
pub horiz_off_n: i32,
pub horiz_off_d: i32,
pub vert_off_n: i32,
pub vert_off_d: i32,
}
impl Clap {
#[allow(clippy::too_many_arguments)]
pub fn new(
clean_aperture_width_n: i32,
clean_aperture_width_d: i32,
clean_aperture_height_n: i32,
clean_aperture_height_d: i32,
horiz_off_n: i32,
horiz_off_d: i32,
vert_off_n: i32,
vert_off_d: i32,
) -> Self {
Self {
clean_aperture_width_n,
clean_aperture_width_d,
clean_aperture_height_n,
clean_aperture_height_d,
horiz_off_n,
horiz_off_d,
vert_off_n,
vert_off_d,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct AuxC {
pub aux_type: String,
pub aux_subtype: Vec<u8>,
}
impl AuxC {
#[allow(clippy::too_many_arguments)]
pub fn new(aux_type: String, aux_subtype: Vec<u8>) -> Self {
Self {
aux_type,
aux_subtype,
}
}
}
impl AuxC {
pub fn kind(&self) -> AuxKind {
match self.aux_type.as_str() {
AUX_URN_ALPHA_MPEG | AUX_URN_ALPHA_HEVC => AuxKind::Alpha,
AUX_URN_DEPTH_MPEG | AUX_URN_DEPTH_HEVC => AuxKind::DepthMap,
AUX_URN_HDR_GAINMAP => AuxKind::HdrGainMap,
_ => AuxKind::Other,
}
}
pub fn is_alpha(&self) -> bool {
matches!(self.kind(), AuxKind::Alpha)
}
pub fn is_depth_map(&self) -> bool {
matches!(self.kind(), AuxKind::DepthMap)
}
pub fn is_hdr_gain_map(&self) -> bool {
matches!(self.kind(), AuxKind::HdrGainMap)
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rloc {
pub horizontal_offset: u32,
pub vertical_offset: u32,
}
impl Rloc {
#[allow(clippy::too_many_arguments)]
pub fn new(horizontal_offset: u32, vertical_offset: u32) -> Self {
Self {
horizontal_offset,
vertical_offset,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Lsel {
pub layer_id: u16,
}
impl Lsel {
#[allow(clippy::too_many_arguments)]
pub fn new(layer_id: u16) -> Self {
Self { layer_id }
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct A1op {
pub op_index: u8,
}
impl A1op {
#[allow(clippy::too_many_arguments)]
pub fn new(op_index: u8) -> Self {
Self { op_index }
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct A1lx {
pub large_size: bool,
pub layer_size: [u32; 3],
}
impl A1lx {
#[allow(clippy::too_many_arguments)]
pub fn new(large_size: bool, layer_size: [u32; 3]) -> Self {
Self {
large_size,
layer_size,
}
}
}
impl A1lx {
pub fn documented_layers(&self) -> usize {
self.layer_size.iter().take_while(|&&s| s != 0).count()
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Mdcv {
pub display_primaries_xy: [(u16, u16); 3],
pub white_point_xy: (u16, u16),
pub max_display_mastering_luminance: u32,
pub min_display_mastering_luminance: u32,
}
impl Mdcv {
#[allow(clippy::too_many_arguments)]
pub fn new(
display_primaries_xy: [(u16, u16); 3],
white_point_xy: (u16, u16),
max_display_mastering_luminance: u32,
min_display_mastering_luminance: u32,
) -> Self {
Self {
display_primaries_xy,
white_point_xy,
max_display_mastering_luminance,
min_display_mastering_luminance,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Clli {
pub max_content_light_level: u16,
pub max_pic_average_light_level: u16,
}
impl Clli {
#[allow(clippy::too_many_arguments)]
pub fn new(max_content_light_level: u16, max_pic_average_light_level: u16) -> Self {
Self {
max_content_light_level,
max_pic_average_light_level,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cclv {
pub max_content_light_level: u16,
pub max_pic_average_light_level: u16,
}
impl Cclv {
#[allow(clippy::too_many_arguments)]
pub fn new(max_content_light_level: u16, max_pic_average_light_level: u16) -> Self {
Self {
max_content_light_level,
max_pic_average_light_level,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Amve {
pub ambient_illuminance: u32,
pub ambient_light_x: u16,
pub ambient_light_y: u16,
}
impl Amve {
#[allow(clippy::too_many_arguments)]
pub fn new(ambient_illuminance: u32, ambient_light_x: u16, ambient_light_y: u16) -> Self {
Self {
ambient_illuminance,
ambient_light_x,
ambient_light_y,
}
}
}
impl Amve {
#[inline]
pub fn illuminance_lux(&self) -> f64 {
f64::from(self.ambient_illuminance) / 10000.0
}
#[inline]
pub fn light_xy(&self) -> (f64, f64) {
(
f64::from(self.ambient_light_x) * 0.00002,
f64::from(self.ambient_light_y) * 0.00002,
)
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Iscl {
pub target_width_numerator: u16,
pub target_width_denominator: u16,
pub target_height_numerator: u16,
pub target_height_denominator: u16,
}
impl Iscl {
#[allow(clippy::too_many_arguments)]
pub fn new(
target_width_numerator: u16,
target_width_denominator: u16,
target_height_numerator: u16,
target_height_denominator: u16,
) -> Self {
Self {
target_width_numerator,
target_width_denominator,
target_height_numerator,
target_height_denominator,
}
}
}
impl Iscl {
pub fn is_well_formed(&self) -> bool {
self.target_width_numerator != 0
&& self.target_width_denominator != 0
&& self.target_height_numerator != 0
&& self.target_height_denominator != 0
}
pub fn scaled_dims(&self, input_width: u32, input_height: u32) -> Option<(u32, u32)> {
if self.target_width_denominator == 0 || self.target_height_denominator == 0 {
return None;
}
let w = div_ceil_u64(
u64::from(input_width) * u64::from(self.target_width_numerator),
u64::from(self.target_width_denominator),
);
let h = div_ceil_u64(
u64::from(input_height) * u64::from(self.target_height_numerator),
u64::from(self.target_height_denominator),
);
Some((
u32::try_from(w).unwrap_or(u32::MAX),
u32::try_from(h).unwrap_or(u32::MAX),
))
}
}
#[inline]
fn div_ceil_u64(n: u64, d: u64) -> u64 {
if d == 0 {
return 0;
}
n / d + u64::from(n % d != 0)
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rref {
pub reference_types: Vec<BoxType>,
}
impl Rref {
#[allow(clippy::too_many_arguments)]
pub fn new(reference_types: Vec<BoxType>) -> Self {
Self { reference_types }
}
}
impl Rref {
pub fn count(&self) -> usize {
self.reference_types.len()
}
pub fn requires(&self, four_cc: &BoxType) -> bool {
self.reference_types.iter().any(|t| t == four_cc)
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Crtt {
pub creation_time: u64,
}
impl Crtt {
#[allow(clippy::too_many_arguments)]
pub fn new(creation_time: u64) -> Self {
Self { creation_time }
}
}
const HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS: u64 = 2_082_844_800;
impl Crtt {
pub fn seconds_since_unix_epoch(&self) -> Option<u64> {
let secs_since_1904 = self.creation_time / 1_000_000;
secs_since_1904.checked_sub(HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS)
}
pub fn subsecond_micros(&self) -> u32 {
(self.creation_time % 1_000_000) as u32
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Mdft {
pub modification_time: u64,
}
impl Mdft {
#[allow(clippy::too_many_arguments)]
pub fn new(modification_time: u64) -> Self {
Self { modification_time }
}
}
impl Mdft {
pub fn seconds_since_unix_epoch(&self) -> Option<u64> {
let secs_since_1904 = self.modification_time / 1_000_000;
secs_since_1904.checked_sub(HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS)
}
pub fn subsecond_micros(&self) -> u32 {
(self.modification_time % 1_000_000) as u32
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Udes {
pub lang: String,
pub name: String,
pub description: String,
pub tags: String,
}
impl Udes {
#[allow(clippy::too_many_arguments)]
pub fn new(lang: String, name: String, description: String, tags: String) -> Self {
Self {
lang,
name,
description,
tags,
}
}
}
impl Udes {
pub fn lang_opt(&self) -> Option<&str> {
if self.lang.is_empty() {
None
} else {
Some(self.lang.as_str())
}
}
pub fn name_opt(&self) -> Option<&str> {
if self.name.is_empty() {
None
} else {
Some(self.name.as_str())
}
}
pub fn description_opt(&self) -> Option<&str> {
if self.description.is_empty() {
None
} else {
Some(self.description.as_str())
}
}
pub fn tag_list(&self) -> Vec<&str> {
if self.tags.is_empty() {
return Vec::new();
}
self.tags
.split(',')
.map(|t| t.trim())
.filter(|t| !t.is_empty())
.collect()
}
pub fn tags_opt(&self) -> Option<&str> {
if self.tags.is_empty() {
None
} else {
Some(self.tags.as_str())
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Altt {
pub alt_text: String,
pub alt_lang: String,
}
impl Altt {
#[allow(clippy::too_many_arguments)]
pub fn new(alt_text: String, alt_lang: String) -> Self {
Self { alt_text, alt_lang }
}
}
impl Altt {
pub fn alt_text_opt(&self) -> Option<&str> {
if self.alt_text.is_empty() {
None
} else {
Some(self.alt_text.as_str())
}
}
pub fn alt_lang_opt(&self) -> Option<&str> {
if self.alt_lang.is_empty() {
None
} else {
Some(self.alt_lang.as_str())
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Aebr {
pub exposure_step: i8,
pub exposure_numerator: i8,
}
impl Aebr {
#[allow(clippy::too_many_arguments)]
pub fn new(exposure_step: i8, exposure_numerator: i8) -> Self {
Self {
exposure_step,
exposure_numerator,
}
}
}
impl Aebr {
pub const STEP_FULL: i8 = 1;
pub const STEP_HALF: i8 = 2;
pub const STEP_THIRD: i8 = 3;
pub const STEP_QUARTER: i8 = 4;
pub fn is_defined_step(&self) -> bool {
matches!(
self.exposure_step,
Self::STEP_FULL | Self::STEP_HALF | Self::STEP_THIRD | Self::STEP_QUARTER
)
}
pub fn exposure_stops(&self) -> Option<f64> {
if self.exposure_step == 0 {
return None;
}
Some(f64::from(self.exposure_numerator) / f64::from(self.exposure_step))
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Wbbr {
pub blue_amber: u16,
pub green_magenta: i8,
}
impl Wbbr {
#[allow(clippy::too_many_arguments)]
pub fn new(blue_amber: u16, green_magenta: i8) -> Self {
Self {
blue_amber,
green_magenta,
}
}
}
impl Wbbr {
pub const NEUTRAL_GREEN_MAGENTA: i8 = 0;
pub fn green_magenta_duv(&self) -> f64 {
f64::from(self.green_magenta) / 100.0
}
pub fn is_green_magenta_neutral(&self) -> bool {
self.green_magenta == Self::NEUTRAL_GREEN_MAGENTA
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Fobr {
pub focus_distance_numerator: u16,
pub focus_distance_denominator: u16,
}
impl Fobr {
#[allow(clippy::too_many_arguments)]
pub fn new(focus_distance_numerator: u16, focus_distance_denominator: u16) -> Self {
Self {
focus_distance_numerator,
focus_distance_denominator,
}
}
}
impl Fobr {
pub const INFINITY_DENOMINATOR: u16 = 0;
pub fn focus_distance_metres(&self) -> Option<f64> {
if self.focus_distance_denominator == Self::INFINITY_DENOMINATOR {
None
} else {
Some(
f64::from(self.focus_distance_numerator)
/ f64::from(self.focus_distance_denominator),
)
}
}
pub fn is_focus_at_infinity(&self) -> bool {
self.focus_distance_denominator == Self::INFINITY_DENOMINATOR
}
pub fn has_well_formed_infinity_sentinel(&self) -> bool {
self.focus_distance_denominator == Self::INFINITY_DENOMINATOR
&& self.focus_distance_numerator == 0
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Afbr {
pub flash_exposure_numerator: i8,
pub flash_exposure_denominator: i8,
}
impl Afbr {
#[allow(clippy::too_many_arguments)]
pub fn new(flash_exposure_numerator: i8, flash_exposure_denominator: i8) -> Self {
Self {
flash_exposure_numerator,
flash_exposure_denominator,
}
}
}
impl Afbr {
pub fn flash_exposure_stops(&self) -> Option<f64> {
if self.flash_exposure_denominator == 0 {
return None;
}
Some(f64::from(self.flash_exposure_numerator) / f64::from(self.flash_exposure_denominator))
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Dobr {
pub f_stop_numerator: i8,
pub f_stop_denominator: i8,
}
impl Dobr {
#[allow(clippy::too_many_arguments)]
pub fn new(f_stop_numerator: i8, f_stop_denominator: i8) -> Self {
Self {
f_stop_numerator,
f_stop_denominator,
}
}
}
impl Dobr {
pub fn aperture_stops(&self) -> Option<f64> {
if self.f_stop_denominator == 0 {
return None;
}
Some(f64::from(self.f_stop_numerator) / f64::from(self.f_stop_denominator))
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PanoGrid {
pub rows_minus_one: u8,
pub columns_minus_one: u8,
}
impl PanoGrid {
#[allow(clippy::too_many_arguments)]
pub fn new(rows_minus_one: u8, columns_minus_one: u8) -> Self {
Self {
rows_minus_one,
columns_minus_one,
}
}
}
impl PanoGrid {
pub fn rows(&self) -> u16 {
u16::from(self.rows_minus_one) + 1
}
pub fn columns(&self) -> u16 {
u16::from(self.columns_minus_one) + 1
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Pano {
pub panorama_direction: u8,
pub grid: Option<PanoGrid>,
}
impl Pano {
#[allow(clippy::too_many_arguments)]
pub fn new(panorama_direction: u8, grid: Option<PanoGrid>) -> Self {
Self {
panorama_direction,
grid,
}
}
}
impl Pano {
pub const DIRECTION_LEFT_TO_RIGHT: u8 = 0;
pub const DIRECTION_RIGHT_TO_LEFT: u8 = 1;
pub const DIRECTION_BOTTOM_TO_TOP: u8 = 2;
pub const DIRECTION_TOP_TO_BOTTOM: u8 = 3;
pub const DIRECTION_GRID_RASTER: u8 = 4;
pub const DIRECTION_GRID_CONTINUOUS: u8 = 5;
pub fn is_defined_direction(&self) -> bool {
self.panorama_direction <= Self::DIRECTION_GRID_CONTINUOUS
}
pub fn is_grid(&self) -> bool {
self.panorama_direction >= Self::DIRECTION_GRID_RASTER
&& self.panorama_direction <= Self::DIRECTION_GRID_CONTINUOUS
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SubsEntry {
pub subsample_size: u32,
pub subsample_priority: u8,
pub discardable: u8,
pub codec_specific_parameters: u32,
}
impl SubsEntry {
#[allow(clippy::too_many_arguments)]
pub fn new(
subsample_size: u32,
subsample_priority: u8,
discardable: u8,
codec_specific_parameters: u32,
) -> Self {
Self {
subsample_size,
subsample_priority,
discardable,
codec_specific_parameters,
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Subs {
pub flags: u32,
pub entries: Vec<SubsEntry>,
}
impl Subs {
#[allow(clippy::too_many_arguments)]
pub fn new(flags: u32, entries: Vec<SubsEntry>) -> Self {
Self { flags, entries }
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Tols {
pub target_ols_idx: u16,
}
impl Tols {
#[allow(clippy::too_many_arguments)]
pub fn new(target_ols_idx: u16) -> Self {
Self { target_ols_idx }
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Prdi {
pub flags: u32,
pub step_count: Option<u16>,
pub item_counts: Option<Vec<u16>>,
}
impl Prdi {
#[allow(clippy::too_many_arguments)]
pub fn new(flags: u32, step_count: Option<u16>, item_counts: Option<Vec<u16>>) -> Self {
Self {
flags,
step_count,
item_counts,
}
}
}
impl Prdi {
pub const FLAG_ITEM_REFERENCE_ORDER: u32 = 0x0000_0001;
pub const FLAG_ONE_ITEM_PER_STEP: u32 = 0x0000_0002;
pub const FLAG_ALTERNATIVE_IS_CANDIDATE: u32 = 0x0000_0004;
pub fn is_item_reference_order(&self) -> bool {
self.flags & Self::FLAG_ITEM_REFERENCE_ORDER != 0
}
pub fn is_one_item_per_step(&self) -> bool {
self.flags & Self::FLAG_ONE_ITEM_PER_STEP != 0
}
pub fn is_alternative_is_candidate(&self) -> bool {
self.flags & Self::FLAG_ALTERNATIVE_IS_CANDIDATE != 0
}
pub fn step_count_or_inferred(&self, dimg_input_count: u16) -> u16 {
self.step_count.unwrap_or(dimg_input_count)
}
pub fn item_count_for_step(&self, i: usize) -> Option<u16> {
match &self.item_counts {
Some(counts) => counts.get(i).copied(),
None => Some(1),
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Sstr;
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Txlo {
pub flags: u32,
pub reference_width: u32,
pub reference_height: u32,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub font_size: u16,
pub direction: String,
pub writing_mode: String,
}
impl Txlo {
#[allow(clippy::too_many_arguments)]
pub fn new(
flags: u32,
reference_width: u32,
reference_height: u32,
x: i32,
y: i32,
width: u32,
height: u32,
font_size: u16,
direction: String,
writing_mode: String,
) -> Self {
Self {
flags,
reference_width,
reference_height,
x,
y,
width,
height,
font_size,
direction,
writing_mode,
}
}
}
impl Txlo {
pub const FLAG_LARGE_FIELD_SIZE: u32 = 0x0000_0001;
pub fn is_large_field_size(&self) -> bool {
self.flags & Self::FLAG_LARGE_FIELD_SIZE != 0
}
pub fn font_size_percent(&self) -> f64 {
f64::from(self.font_size) / 256.0
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Elng {
pub extended_language: String,
}
impl Elng {
#[allow(clippy::too_many_arguments)]
pub fn new(extended_language: String) -> Self {
Self { extended_language }
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Fnch {
pub font_family: String,
pub font_style: String,
pub font_weight: String,
}
impl Fnch {
#[allow(clippy::too_many_arguments)]
pub fn new(font_family: String, font_style: String, font_weight: String) -> Self {
Self {
font_family,
font_style,
font_weight,
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MaskC {
pub bits_per_pixel: u8,
}
impl MaskC {
#[allow(clippy::too_many_arguments)]
pub fn new(bits_per_pixel: u8) -> Self {
Self { bits_per_pixel }
}
}
impl MaskC {
pub fn is_defined_depth(&self) -> bool {
matches!(self.bits_per_pixel, 1 | 2 | 4 | 8 | 16 | 24)
}
pub fn pixels_per_byte(&self) -> Option<u8> {
match self.bits_per_pixel {
1 => Some(8),
2 => Some(4),
4 => Some(2),
_ => None,
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Cmex {
pub flags: u32,
pub version: u8,
pub pos_x: Option<i32>,
pub pos_y: Option<i32>,
pub pos_z: Option<i32>,
pub quat_x: Option<i32>,
pub quat_y: Option<i32>,
pub quat_z: Option<i32>,
pub id: Option<u32>,
}
impl Cmex {
#[allow(clippy::too_many_arguments)]
pub fn new(
flags: u32,
version: u8,
pos_x: Option<i32>,
pos_y: Option<i32>,
pos_z: Option<i32>,
quat_x: Option<i32>,
quat_y: Option<i32>,
quat_z: Option<i32>,
id: Option<u32>,
) -> Self {
Self {
flags,
version,
pos_x,
pos_y,
pos_z,
quat_x,
quat_y,
quat_z,
id,
}
}
}
impl Cmex {
pub const FLAG_POS_X_PRESENT: u32 = 0x00_0001;
pub const FLAG_POS_Y_PRESENT: u32 = 0x00_0002;
pub const FLAG_POS_Z_PRESENT: u32 = 0x00_0004;
pub const FLAG_ORIENTATION_PRESENT: u32 = 0x00_0008;
pub const FLAG_ROT_LARGE_FIELD_SIZE: u32 = 0x00_0010;
pub const FLAG_ID_PRESENT: u32 = 0x00_0020;
pub fn pos_x_present(&self) -> bool {
self.flags & Self::FLAG_POS_X_PRESENT != 0
}
pub fn pos_y_present(&self) -> bool {
self.flags & Self::FLAG_POS_Y_PRESENT != 0
}
pub fn pos_z_present(&self) -> bool {
self.flags & Self::FLAG_POS_Z_PRESENT != 0
}
pub fn orientation_present(&self) -> bool {
self.flags & Self::FLAG_ORIENTATION_PRESENT != 0
}
pub fn rot_large_field_size(&self) -> bool {
self.flags & Self::FLAG_ROT_LARGE_FIELD_SIZE != 0
}
pub fn id_present(&self) -> bool {
self.flags & Self::FLAG_ID_PRESENT != 0
}
pub fn orientation_precision(&self) -> u32 {
if self.rot_large_field_size() {
16
} else {
0
}
}
pub fn position(&self) -> [i32; 3] {
[
self.pos_x.unwrap_or(0),
self.pos_y.unwrap_or(0),
self.pos_z.unwrap_or(0),
]
}
pub fn quaternion(&self) -> Option<[f64; 4]> {
if self.version != 0 {
return None;
}
let (qx, qy, qz) = (self.quat_x?, self.quat_y?, self.quat_z?);
let scale = f64::from(1u32 << (14 + self.orientation_precision()));
let qx = f64::from(qx) / scale;
let qy = f64::from(qy) / scale;
let qz = f64::from(qz) / scale;
let qw = (1.0 - (qx * qx + qy * qy + qz * qz)).max(0.0).sqrt().abs();
Some([qx, qy, qz, qw])
}
pub fn rotation_matrix(&self) -> Option<[[f64; 3]; 3]> {
let [qx, qy, qz, qw] = self.quaternion()?;
Some([
[
1.0 - 2.0 * (qy * qy + qz * qz),
2.0 * (qx * qy - qz * qw),
2.0 * (qx * qz + qy * qw),
],
[
2.0 * (qx * qy + qz * qw),
1.0 - 2.0 * (qx * qx + qz * qz),
2.0 * (qy * qz - qx * qw),
],
[
2.0 * (qx * qz - qy * qw),
2.0 * (qy * qz + qx * qw),
1.0 - 2.0 * (qx * qx + qy * qy),
],
])
}
pub fn coordinate_system_id(&self) -> u32 {
self.id.unwrap_or(0)
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Cmin {
pub flags: u32,
pub focal_length_x: i32,
pub principal_point_x: i32,
pub principal_point_y: i32,
pub focal_length_y: Option<i32>,
pub skew_factor: Option<i32>,
}
impl Cmin {
#[allow(clippy::too_many_arguments)]
pub fn new(
flags: u32,
focal_length_x: i32,
principal_point_x: i32,
principal_point_y: i32,
focal_length_y: Option<i32>,
skew_factor: Option<i32>,
) -> Self {
Self {
flags,
focal_length_x,
principal_point_x,
principal_point_y,
focal_length_y,
skew_factor,
}
}
}
impl Cmin {
pub const FLAG_FULL_INTRINSICS: u32 = 0x1;
pub fn has_skew(&self) -> bool {
self.flags & Self::FLAG_FULL_INTRINSICS != 0
}
pub fn denominator_shift(&self) -> u32 {
(self.flags & 0x0000_1F00) >> 8
}
pub fn skew_denominator_shift(&self) -> u32 {
(self.flags & 0x001F_0000) >> 16
}
pub fn denominator(&self) -> u64 {
1u64 << self.denominator_shift()
}
pub fn skew_denominator(&self) -> u64 {
1u64 << self.skew_denominator_shift()
}
pub fn focal_length_x_value(&self, image_width: u32) -> f64 {
f64::from(self.focal_length_x) * f64::from(image_width) / self.denominator() as f64
}
pub fn focal_length_y_value(&self, image_width: u32, image_height: u32) -> f64 {
match self.focal_length_y {
Some(fl_y) => f64::from(fl_y) * f64::from(image_height) / self.denominator() as f64,
None => self.focal_length_x_value(image_width),
}
}
pub fn principal_point_x_value(&self, image_width: u32) -> f64 {
f64::from(self.principal_point_x) * f64::from(image_width) / self.denominator() as f64
}
pub fn principal_point_y_value(&self, image_height: u32) -> f64 {
f64::from(self.principal_point_y) * f64::from(image_height) / self.denominator() as f64
}
pub fn skew_value(&self) -> f64 {
match self.skew_factor {
Some(skew) => f64::from(skew) / self.skew_denominator() as f64,
None => 0.0,
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Wipe {
pub transition_direction: u8,
}
impl Wipe {
#[allow(clippy::too_many_arguments)]
pub fn new(transition_direction: u8) -> Self {
Self {
transition_direction,
}
}
}
impl Wipe {
pub const FROM_LEFT: u8 = 0;
pub const FROM_RIGHT: u8 = 1;
pub const FROM_TOP: u8 = 2;
pub const FROM_BOTTOM: u8 = 3;
pub const FROM_LEFT_TOP: u8 = 4;
pub const FROM_RIGHT_TOP: u8 = 5;
pub const FROM_LEFT_BOTTOM: u8 = 6;
pub const FROM_RIGHT_BOTTOM: u8 = 7;
pub fn is_known_direction(&self) -> bool {
self.transition_direction <= Self::FROM_RIGHT_BOTTOM
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Zoom {
pub transition_direction: u8,
pub transition_shape: u8,
}
impl Zoom {
#[allow(clippy::too_many_arguments)]
pub fn new(transition_direction: u8, transition_shape: u8) -> Self {
Self {
transition_direction,
transition_shape,
}
}
}
impl Zoom {
pub const DIRECTION_IN: u8 = 0;
pub const DIRECTION_OUT: u8 = 1;
pub const SHAPE_RECTANGULAR: u8 = 0;
pub const SHAPE_CIRCLE: u8 = 1;
pub const SHAPE_STAR: u8 = 2;
pub fn is_known_shape(&self) -> bool {
self.transition_shape <= Self::SHAPE_STAR
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Fade {
pub transition_direction: u8,
}
impl Fade {
#[allow(clippy::too_many_arguments)]
pub fn new(transition_direction: u8) -> Self {
Self {
transition_direction,
}
}
}
impl Fade {
pub const THROUGH_WHITE: u8 = 0;
pub const THROUGH_BLACK: u8 = 1;
pub const DISSOLVE: u8 = 2;
pub fn is_known_direction(&self) -> bool {
self.transition_direction <= Self::DISSOLVE
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Splt {
pub transition_direction: u8,
}
impl Splt {
#[allow(clippy::too_many_arguments)]
pub fn new(transition_direction: u8) -> Self {
Self {
transition_direction,
}
}
}
impl Splt {
pub const VERTICAL_IN: u8 = 0;
pub const VERTICAL_OUT: u8 = 1;
pub const HORIZONTAL_IN: u8 = 2;
pub const HORIZONTAL_OUT: u8 = 3;
pub fn is_known_direction(&self) -> bool {
self.transition_direction <= Self::HORIZONTAL_OUT
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Stpe {
pub transition_period: u8,
}
impl Stpe {
#[allow(clippy::too_many_arguments)]
pub fn new(transition_period: u8) -> Self {
Self { transition_period }
}
}
impl Stpe {
pub fn seconds(&self) -> f64 {
f64::from(self.transition_period) / 16.0
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Ssld {
pub duration: u16,
}
impl Ssld {
#[allow(clippy::too_many_arguments)]
pub fn new(duration: u16) -> Self {
Self { duration }
}
}
impl Ssld {
pub fn seconds(&self) -> f64 {
f64::from(self.duration) / 16.0
}
pub fn is_reserved(&self) -> bool {
self.duration == 0
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum Property {
Av1C(Vec<u8>),
Ispe(Ispe),
Colr(Colr),
Pixi(Pixi),
Pasp(Pasp),
Irot(Irot),
Imir(Imir),
Clap(Clap),
AuxC(AuxC),
Mdcv(Mdcv),
Clli(Clli),
Cclv(Cclv),
Amve(Amve),
Rloc(Rloc),
Lsel(Lsel),
A1op(A1op),
A1lx(A1lx),
Iscl(Iscl),
Rref(Rref),
Crtt(Crtt),
Mdft(Mdft),
Udes(Udes),
Altt(Altt),
Aebr(Aebr),
Wbbr(Wbbr),
Fobr(Fobr),
Afbr(Afbr),
Dobr(Dobr),
Pano(Pano),
Subs(Subs),
Tols(Tols),
Wipe(Wipe),
Zoom(Zoom),
Fade(Fade),
Splt(Splt),
Stpe(Stpe),
Ssld(Ssld),
Prdi(Prdi),
Sstr(Sstr),
Txlo(Txlo),
Elng(Elng),
Fnch(Fnch),
MaskC(MaskC),
Cmex(Cmex),
Cmin(Cmin),
Container(hprops::Property),
Other(BoxType, Vec<u8>),
}
impl Property {
pub fn kind(&self) -> BoxType {
match self {
Property::Av1C(_) => AV1C,
Property::Ispe(_) => ISPE,
Property::Colr(_) => COLR,
Property::Pixi(_) => PIXI,
Property::Pasp(_) => PASP,
Property::Irot(_) => IROT,
Property::Imir(_) => IMIR,
Property::Clap(_) => CLAP,
Property::AuxC(_) => AUXC,
Property::Mdcv(_) => MDCV,
Property::Clli(_) => CLLI,
Property::Cclv(_) => CCLV,
Property::Amve(_) => AMVE,
Property::Rloc(_) => RLOC,
Property::Lsel(_) => LSEL,
Property::A1op(_) => A1OP,
Property::A1lx(_) => A1LX,
Property::Iscl(_) => ISCL,
Property::Rref(_) => RREF,
Property::Crtt(_) => CRTT,
Property::Mdft(_) => MDFT,
Property::Udes(_) => UDES,
Property::Altt(_) => ALTT,
Property::Aebr(_) => AEBR,
Property::Wbbr(_) => WBBR,
Property::Fobr(_) => FOBR,
Property::Afbr(_) => AFBR,
Property::Dobr(_) => DOBR,
Property::Pano(_) => PANO,
Property::Subs(_) => SUBS,
Property::Tols(_) => TOLS,
Property::Wipe(_) => WIPE,
Property::Zoom(_) => ZOOM,
Property::Fade(_) => FADE,
Property::Splt(_) => SPLT,
Property::Stpe(_) => STPE,
Property::Ssld(_) => SSLD,
Property::Prdi(_) => PRDI,
Property::Sstr(_) => SSTR,
Property::Txlo(_) => TXLO,
Property::Elng(_) => ELNG,
Property::Fnch(_) => FNCH,
Property::MaskC(_) => MSKC,
Property::Cmex(_) => CMEX,
Property::Cmin(_) => CMIN,
Property::Container(p) => p.box_type(),
Property::Other(t, _) => *t,
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct IrefEntry {
pub reference_type: BoxType,
pub from_id: u32,
pub to_ids: Vec<u32>,
}
impl IrefEntry {
#[allow(clippy::too_many_arguments)]
pub fn new(reference_type: BoxType, from_id: u32, to_ids: Vec<u32>) -> Self {
Self {
reference_type,
from_id,
to_ids,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct Meta {
pub handler: Option<BoxType>,
pub primary_item_id: Option<u32>,
pub items: Vec<ItemInfo>,
pub locations: Vec<ItemLocation>,
pub properties: Vec<Property>,
pub associations: Vec<ItemPropertyAssociation>,
pub irefs: Vec<IrefEntry>,
pub grpl: Option<Vec<u8>>,
pub idat: Option<Vec<u8>>,
}
impl Meta {
#[allow(clippy::too_many_arguments)]
pub fn new(
handler: Option<BoxType>,
primary_item_id: Option<u32>,
items: Vec<ItemInfo>,
locations: Vec<ItemLocation>,
properties: Vec<Property>,
associations: Vec<ItemPropertyAssociation>,
irefs: Vec<IrefEntry>,
grpl: Option<Vec<u8>>,
idat: Option<Vec<u8>>,
) -> Self {
Self {
handler,
primary_item_id,
items,
locations,
properties,
associations,
irefs,
grpl,
idat,
}
}
}
impl Meta {
pub fn parse(meta_payload: &[u8]) -> Result<Self> {
let container = hmeta::Meta::parse(meta_payload)?;
let mut me = Meta::from_container(&container)?;
let (_version, _flags, body) = parse_full_box(meta_payload)?;
if let Some((grpl, _)) = crate::box_parser::find_box(body, &b(b"grpl"))? {
me.grpl = Some(grpl.to_vec());
}
Ok(me)
}
pub fn from_container(container: &hmeta::Meta) -> Result<Self> {
let mut properties = Vec::with_capacity(container.properties.len());
for raw in &container.properties {
properties.push(property_from_raw(raw)?);
}
Ok(Meta {
handler: container.handler.as_ref().map(|h| h.handler_type),
primary_item_id: container.primary_item_id,
items: container.items.iter().map(ItemInfo::from).collect(),
locations: container.locations.iter().map(ItemLocation::from).collect(),
properties,
associations: container
.associations
.iter()
.map(|a| ItemPropertyAssociation {
item_id: a.item_id,
entries: a
.entries
.iter()
.map(|e| PropertyAssociation {
index: e.index.saturating_sub(1),
essential: e.essential,
})
.collect(),
})
.collect(),
irefs: container
.references
.iter()
.map(|r| IrefEntry {
reference_type: r.reference_type,
from_id: r.from_item_id,
to_ids: r.to_item_ids.clone(),
})
.collect(),
grpl: None,
idat: container.idat.clone(),
})
}
pub fn iref_targets(&self, reference_type: &BoxType, from_id: u32) -> Vec<u32> {
for e in &self.irefs {
if &e.reference_type == reference_type && e.from_id == from_id {
return e.to_ids.clone();
}
}
Vec::new()
}
pub fn iref_source_of(&self, reference_type: &BoxType, to_id: u32) -> Option<u32> {
for e in &self.irefs {
if &e.reference_type == reference_type && e.to_ids.contains(&to_id) {
return Some(e.from_id);
}
}
None
}
pub fn iref_sources_of(&self, reference_type: &BoxType, to_id: u32) -> Vec<u32> {
let mut out = Vec::new();
for e in &self.irefs {
if &e.reference_type == reference_type && e.to_ids.contains(&to_id) {
out.push(e.from_id);
}
}
out
}
pub fn iref_targets_of(&self, reference_type: &BoxType, from: u32) -> Vec<u32> {
let mut out = Vec::new();
for e in &self.irefs {
if &e.reference_type == reference_type && e.from_id == from {
out.extend_from_slice(&e.to_ids);
}
}
out
}
pub fn is_alpha_premultiplied_for(&self, to_id: u32) -> bool {
const PREM: BoxType = b(b"prem");
const AUXL: BoxType = b(b"auxl");
if self.iref_source_of(&PREM, to_id).is_some() {
return true;
}
self.iref_targets_of(&PREM, to_id)
.iter()
.any(|aux| self.iref_targets_of(&AUXL, *aux).contains(&to_id))
}
pub fn item_by_id(&self, id: u32) -> Option<&ItemInfo> {
self.items.iter().find(|i| i.id == id)
}
pub fn location_by_id(&self, id: u32) -> Option<&ItemLocation> {
self.locations.iter().find(|l| l.id == id)
}
pub fn assoc_by_id(&self, id: u32) -> Option<&ItemPropertyAssociation> {
self.associations.iter().find(|a| a.item_id == id)
}
pub fn properties_for(&self, item_id: u32) -> Vec<&Property> {
let Some(assoc) = self.assoc_by_id(item_id) else {
return Vec::new();
};
assoc
.entries
.iter()
.filter_map(|pa| self.properties.get(pa.index as usize))
.collect()
}
pub fn property_for<'a>(&'a self, item_id: u32, kind: &BoxType) -> Option<&'a Property> {
let assoc = self.assoc_by_id(item_id)?;
for pa in &assoc.entries {
let prop = self.properties.get(pa.index as usize)?;
if &prop.kind() == kind {
return Some(prop);
}
}
None
}
pub fn unsupported_essential_properties(&self, item_id: u32) -> Vec<BoxType> {
let Some(assoc) = self.assoc_by_id(item_id) else {
return Vec::new();
};
let mut out = Vec::new();
for pa in &assoc.entries {
if !pa.essential {
continue;
}
match self.properties.get(pa.index as usize) {
Some(Property::Other(t, _)) => out.push(*t),
Some(Property::Container(p)) => out.push(p.box_type()),
None => out.push([0, 0, 0, 0]),
Some(_) => {}
}
}
out
}
pub fn has_unsupported_essential_property(&self, item_id: u32) -> bool {
!self.unsupported_essential_properties(item_id).is_empty()
}
pub fn groups(&self) -> Result<Vec<crate::derived::EntityGroup>> {
match &self.grpl {
None => Ok(Vec::new()),
Some(bytes) => crate::derived::parse_grpl(bytes),
}
}
pub fn aux_items_for(&self, to_id: u32) -> Vec<(u32, AuxKind)> {
const AUXL: BoxType = b(b"auxl");
const AUXC_KIND: BoxType = b(b"auxC");
let mut out = Vec::new();
for src in self.iref_sources_of(&AUXL, to_id) {
if let Some(Property::AuxC(aux)) = self.property_for(src, &AUXC_KIND) {
out.push((src, aux.kind()));
}
}
out
}
pub fn item_ids_of_type(&self, target: &BoxType) -> Vec<u32> {
self.items
.iter()
.filter(|it| it.item_type == *target)
.map(|it| it.id)
.collect()
}
}
const CONTAINER_TYPED: &[FourCc] = &[
ISPE, COLR, PIXI, PASP, IROT, IMIR, CLAP, AUXC, CLLI, AMVE, RLOC, LSEL, A1OP, A1LX, ISCL, RREF,
CRTT, MDFT, UDES, ALTT,
];
const CONTAINER_OPAQUE: &[FourCc] = &[REVE, NDWT, CEXG, DADJ, STAG, TILC];
const VERSION_ZERO_ONLY: &[FourCc] = &[RLOC, ISCL, RREF, CRTT, MDFT, UDES, ALTT];
fn container_typed(raw: &hmeta::RawProperty) -> Result<hprops::Property> {
let body = raw.body.as_slice();
let tag = &raw.box_type;
if VERSION_ZERO_ONLY.contains(tag) {
let (version, _flags, _rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!(
"avif: {} version {version} != 0",
type_str(tag)
)));
}
}
let strings = if tag == &UDES {
4
} else if tag == &ALTT {
2
} else {
0
};
if strings > 0 {
let (_v, _f, rest) = parse_full_box(body)?;
if rest.iter().filter(|&&c| c == 0).count() < strings {
return Err(Error::invalid(format!(
"avif: {} needs {strings} NUL-terminated strings",
type_str(tag)
)));
}
}
if tag == &AUXC {
let (_v, _f, rest) = parse_full_box(body)?;
if !rest.contains(&0) {
return Err(Error::invalid("avif: auxC aux_type is not NUL-terminated"));
}
}
Ok(hprops::Property::parse(raw)?)
}
fn property_from_raw(raw: &hmeta::RawProperty) -> Result<Property> {
let body = raw.body.as_slice();
if CONTAINER_TYPED.contains(&raw.box_type) {
return Ok(container_property(&container_typed(raw)?));
}
if CONTAINER_OPAQUE.contains(&raw.box_type) {
return Ok(match hprops::Property::parse(raw) {
Ok(hprops::Property::Unknown(_)) | Err(_) => {
Property::Other(raw.box_type, body.to_vec())
}
Ok(p) => Property::Container(p),
});
}
let prop = match &raw.box_type {
x if x == &AV1C => Property::Av1C(body.to_vec()),
x if x == &MDCV => Property::Mdcv(parse_mdcv(body)?),
x if x == &CCLV => Property::Cclv(parse_cclv(body)?),
x if x == &AEBR => Property::Aebr(parse_aebr(body)?),
x if x == &WBBR => Property::Wbbr(parse_wbbr(body)?),
x if x == &FOBR => Property::Fobr(parse_fobr(body)?),
x if x == &AFBR => Property::Afbr(parse_afbr(body)?),
x if x == &DOBR => Property::Dobr(parse_dobr(body)?),
x if x == &PANO => Property::Pano(parse_pano(body)?),
x if x == &SUBS => Property::Subs(parse_subs(body)?),
x if x == &TOLS => Property::Tols(parse_tols(body)?),
x if x == &WIPE => Property::Wipe(parse_wipe(body)?),
x if x == &ZOOM => Property::Zoom(parse_zoom(body)?),
x if x == &FADE => Property::Fade(parse_fade(body)?),
x if x == &SPLT => Property::Splt(parse_splt(body)?),
x if x == &STPE => Property::Stpe(parse_stpe(body)?),
x if x == &SSLD => Property::Ssld(parse_ssld(body)?),
x if x == &PRDI => Property::Prdi(parse_prdi(body)?),
x if x == &SSTR => Property::Sstr(parse_sstr(body)?),
x if x == &TXLO => Property::Txlo(parse_txlo(body)?),
x if x == &ELNG => Property::Elng(parse_elng(body)?),
x if x == &FNCH => Property::Fnch(parse_fnch(body)?),
x if x == &MSKC => Property::MaskC(parse_mskc(body)?),
x if x == &CMEX => Property::Cmex(parse_cmex(body)?),
x if x == &CMIN => Property::Cmin(parse_cmin(body)?),
other => Property::Other(*other, body.to_vec()),
};
Ok(prop)
}
fn container_property(p: &hprops::Property) -> Property {
use hprops::Property as H;
match p {
H::Ispe(v) => Property::Ispe((*v).into()),
H::Pixi(v) => Property::Pixi(v.clone().into()),
H::Colr(v) => Property::Colr(v.clone().into()),
H::Pasp(v) => Property::Pasp((*v).into()),
H::Clap(v) => Property::Clap((*v).into()),
H::Irot(v) => Property::Irot((*v).into()),
H::Imir(v) => Property::Imir((*v).into()),
H::Iscl(v) => Property::Iscl((*v).into()),
H::AuxC(v) => Property::AuxC(v.clone().into()),
H::Clli(v) => Property::Clli((*v).into()),
H::Amve(v) => Property::Amve((*v).into()),
H::Rloc(v) => Property::Rloc((*v).into()),
H::Lsel(v) => Property::Lsel((*v).into()),
H::A1op(v) => Property::A1op((*v).into()),
H::A1lx(v) => Property::A1lx((*v).into()),
H::Rref(v) => Property::Rref(v.clone().into()),
H::Crtt(v) => Property::Crtt(Crtt {
creation_time: v.time,
}),
H::Mdft(v) => Property::Mdft(Mdft {
modification_time: v.time,
}),
H::Udes(v) => Property::Udes(v.clone().into()),
H::Altt(v) => Property::Altt(v.clone().into()),
H::PixiExtended(v) => Property::Pixi(v.clone().into()),
H::Unknown(raw) => Property::Other(raw.box_type, raw.body.clone()),
other => Property::Container(other.clone()),
}
}
#[cfg(test)]
fn container_parse(box_type: &FourCc, body: &[u8]) -> Result<hprops::Property> {
let raw = hmeta::RawProperty::new(*box_type, None, body.to_vec(), body.len() + 8);
container_typed(&raw)
}
macro_rules! container_typed_parser {
($name:ident, $tag:expr, $variant:ident, $ty:ty, $conv:expr) => {
#[cfg(test)]
fn $name(body: &[u8]) -> Result<$ty> {
match container_parse(&$tag, body)? {
hprops::Property::$variant(v) => Ok($conv(v)),
_ => Err(Error::invalid(format!(
"avif: '{}' body did not type as expected",
type_str(&$tag)
))),
}
}
};
}
container_typed_parser!(parse_ispe, ISPE, Ispe, Ispe, Ispe::from);
container_typed_parser!(parse_colr, COLR, Colr, Colr, Colr::from);
container_typed_parser!(parse_pixi, PIXI, Pixi, Pixi, Pixi::from);
container_typed_parser!(parse_pasp, PASP, Pasp, Pasp, Pasp::from);
container_typed_parser!(parse_irot, IROT, Irot, Irot, Irot::from);
container_typed_parser!(parse_imir, IMIR, Imir, Imir, Imir::from);
container_typed_parser!(parse_clap, CLAP, Clap, Clap, Clap::from);
container_typed_parser!(parse_auxc, AUXC, AuxC, AuxC, AuxC::from);
container_typed_parser!(parse_clli, CLLI, Clli, Clli, Clli::from);
container_typed_parser!(parse_amve, AMVE, Amve, Amve, Amve::from);
container_typed_parser!(parse_rloc, RLOC, Rloc, Rloc, Rloc::from);
container_typed_parser!(parse_lsel, LSEL, Lsel, Lsel, Lsel::from);
container_typed_parser!(parse_a1op, A1OP, A1op, A1op, A1op::from);
container_typed_parser!(parse_a1lx, A1LX, A1lx, A1lx, A1lx::from);
container_typed_parser!(parse_iscl, ISCL, Iscl, Iscl, Iscl::from);
container_typed_parser!(parse_rref, RREF, Rref, Rref, Rref::from);
container_typed_parser!(parse_crtt, CRTT, Crtt, Crtt, |v: hprops::TimeInfo| Crtt {
creation_time: v.time
});
container_typed_parser!(parse_mdft, MDFT, Mdft, Mdft, |v: hprops::TimeInfo| Mdft {
modification_time: v.time
});
container_typed_parser!(parse_udes, UDES, Udes, Udes, Udes::from);
container_typed_parser!(parse_altt, ALTT, Altt, Altt, Altt::from);
#[cfg(test)]
fn parse_ipco(payload: &[u8]) -> Result<Vec<Property>> {
let mut out = Vec::new();
for hdr in iter_boxes(payload) {
let hdr = hdr?;
let raw = hmeta::RawProperty::new(
hdr.box_type,
hdr.user_type,
payload[hdr.payload_start..hdr.end()].to_vec(),
hdr.total_len(),
);
out.push(property_from_raw(&raw)?);
}
Ok(out)
}
impl From<&hmeta::ItemInfo> for ItemInfo {
fn from(i: &hmeta::ItemInfo) -> Self {
ItemInfo {
id: i.id,
item_type: i.item_type,
name: i.name.clone(),
content_type: i.content_type.clone(),
content_encoding: i.content_encoding.clone(),
item_uri_type: i.item_uri_type.clone(),
flags: i.flags,
}
}
}
impl From<&hmeta::ItemLocation> for ItemLocation {
fn from(l: &hmeta::ItemLocation) -> Self {
ItemLocation {
id: l.item_id,
construction_method: l.construction_method,
data_reference_index: l.data_reference_index,
base_offset: l.base_offset,
extents: l
.extents
.iter()
.map(|e| IlocExtent {
offset: e.offset,
length: e.length,
extent_index: e.index,
})
.collect(),
}
}
}
impl From<hprops::Ispe> for Ispe {
fn from(v: hprops::Ispe) -> Self {
Ispe {
width: v.width,
height: v.height,
}
}
}
impl From<hprops::Pixi> for Pixi {
fn from(v: hprops::Pixi) -> Self {
Pixi {
bits_per_channel: v.bits_per_channel,
channels: Vec::new(),
}
}
}
impl From<hprops::PixiExtended> for Pixi {
fn from(v: hprops::PixiExtended) -> Self {
Pixi {
bits_per_channel: v.pixi.bits_per_channel,
channels: v.channels,
}
}
}
impl From<hprops::Pasp> for Pasp {
fn from(v: hprops::Pasp) -> Self {
Pasp {
h_spacing: v.h_spacing,
v_spacing: v.v_spacing,
}
}
}
impl From<hprops::Colr> for Colr {
fn from(v: hprops::Colr) -> Self {
match v {
hprops::Colr::Nclx {
primaries,
transfer,
matrix,
full_range,
} => Colr::Nclx {
colour_primaries: primaries,
transfer_characteristics: transfer,
matrix_coefficients: matrix,
full_range,
},
hprops::Colr::Icc { profile, .. } => Colr::Icc(profile),
hprops::Colr::Other { colour_type, .. } => Colr::Unknown(colour_type),
_ => Colr::Unknown(*b"????"),
}
}
}
impl From<&Colr> for hprops::Colr {
fn from(v: &Colr) -> Self {
match v {
Colr::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
} => hprops::Colr::Nclx {
primaries: *colour_primaries,
transfer: *transfer_characteristics,
matrix: *matrix_coefficients,
full_range: *full_range,
},
Colr::Icc(profile) => hprops::Colr::Icc {
restricted: false,
profile: profile.clone(),
},
Colr::Unknown(t) => hprops::Colr::Other {
colour_type: *t,
payload: Vec::new(),
},
}
}
}
impl From<hprops::Irot> for Irot {
fn from(v: hprops::Irot) -> Self {
Irot {
angle: v.angle & 0x03,
}
}
}
impl From<hprops::Imir> for Imir {
fn from(v: hprops::Imir) -> Self {
Imir {
axis: v.axis & 0x01,
}
}
}
impl From<hprops::Clap> for Clap {
fn from(v: hprops::Clap) -> Self {
Clap {
clean_aperture_width_n: v.width_n as i32,
clean_aperture_width_d: v.width_d as i32,
clean_aperture_height_n: v.height_n as i32,
clean_aperture_height_d: v.height_d as i32,
horiz_off_n: v.horiz_off_n,
horiz_off_d: v.horiz_off_d as i32,
vert_off_n: v.vert_off_n,
vert_off_d: v.vert_off_d as i32,
}
}
}
impl From<&Clap> for hprops::Clap {
fn from(v: &Clap) -> Self {
hprops::Clap::new(
v.clean_aperture_width_n as u32,
v.clean_aperture_width_d as u32,
v.clean_aperture_height_n as u32,
v.clean_aperture_height_d as u32,
v.horiz_off_n,
v.horiz_off_d as u32,
v.vert_off_n,
v.vert_off_d as u32,
)
}
}
impl From<hprops::AuxC> for AuxC {
fn from(v: hprops::AuxC) -> Self {
AuxC {
aux_type: v.aux_type,
aux_subtype: v.aux_subtype,
}
}
}
impl From<hprops::Clli> for Clli {
fn from(v: hprops::Clli) -> Self {
Clli {
max_content_light_level: v.max_content_light_level,
max_pic_average_light_level: v.max_pic_average_light_level,
}
}
}
impl From<&Clli> for hprops::Clli {
fn from(v: &Clli) -> Self {
hprops::Clli::new(v.max_content_light_level, v.max_pic_average_light_level)
}
}
impl From<hprops::Amve> for Amve {
fn from(v: hprops::Amve) -> Self {
Amve {
ambient_illuminance: v.ambient_illuminance,
ambient_light_x: v.ambient_light_x,
ambient_light_y: v.ambient_light_y,
}
}
}
impl From<&Amve> for hprops::Amve {
fn from(v: &Amve) -> Self {
hprops::Amve::new(v.ambient_illuminance, v.ambient_light_x, v.ambient_light_y)
}
}
impl From<hprops::Rloc> for Rloc {
fn from(v: hprops::Rloc) -> Self {
Rloc {
horizontal_offset: v.horizontal_offset,
vertical_offset: v.vertical_offset,
}
}
}
impl From<hprops::Lsel> for Lsel {
fn from(v: hprops::Lsel) -> Self {
Lsel {
layer_id: v.layer_id,
}
}
}
impl From<hprops::A1op> for A1op {
fn from(v: hprops::A1op) -> Self {
A1op {
op_index: v.op_index,
}
}
}
impl From<hprops::A1lx> for A1lx {
fn from(v: hprops::A1lx) -> Self {
A1lx {
large_size: v.large_size,
layer_size: v.layer_size,
}
}
}
impl From<hprops::Iscl> for Iscl {
fn from(v: hprops::Iscl) -> Self {
Iscl {
target_width_numerator: v.width_num,
target_width_denominator: v.width_den,
target_height_numerator: v.height_num,
target_height_denominator: v.height_den,
}
}
}
impl From<hprops::Rref> for Rref {
fn from(v: hprops::Rref) -> Self {
Rref {
reference_types: v.reference_types,
}
}
}
impl From<hprops::Udes> for Udes {
fn from(v: hprops::Udes) -> Self {
Udes {
lang: v.lang,
name: v.name,
description: v.description,
tags: v.tags,
}
}
}
impl From<hprops::Altt> for Altt {
fn from(v: hprops::Altt) -> Self {
Altt {
alt_text: v.alt_text,
alt_lang: v.alt_lang,
}
}
}
fn parse_mdcv(body: &[u8]) -> Result<Mdcv> {
if body.len() < 24 {
return Err(Error::invalid(format!(
"avif: mdcv too short ({} < 24)",
body.len()
)));
}
let rx = read_u16(body, 0)?;
let ry = read_u16(body, 2)?;
let gx = read_u16(body, 4)?;
let gy = read_u16(body, 6)?;
let bx = read_u16(body, 8)?;
let by_ = read_u16(body, 10)?;
let wx = read_u16(body, 12)?;
let wy = read_u16(body, 14)?;
let max_lum = read_u32(body, 16)?;
let min_lum = read_u32(body, 20)?;
Ok(Mdcv {
display_primaries_xy: [(rx, ry), (gx, gy), (bx, by_)],
white_point_xy: (wx, wy),
max_display_mastering_luminance: max_lum,
min_display_mastering_luminance: min_lum,
})
}
fn parse_cclv(body: &[u8]) -> Result<Cclv> {
if body.len() < 4 {
return Err(Error::invalid(format!(
"avif: cclv too short ({} < 4)",
body.len()
)));
}
Ok(Cclv {
max_content_light_level: read_u16(body, 0)?,
max_pic_average_light_level: read_u16(body, 2)?,
})
}
fn parse_aebr(body: &[u8]) -> Result<Aebr> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: aebr version {version} != 0")));
}
if rest.len() < 2 {
return Err(Error::invalid(format!(
"avif: aebr too short ({} < 2)",
rest.len()
)));
}
Ok(Aebr {
exposure_step: rest[0] as i8,
exposure_numerator: rest[1] as i8,
})
}
fn parse_wbbr(body: &[u8]) -> Result<Wbbr> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: wbbr version {version} != 0")));
}
if rest.len() < 3 {
return Err(Error::invalid(format!(
"avif: wbbr too short ({} < 3)",
rest.len()
)));
}
Ok(Wbbr {
blue_amber: read_u16(rest, 0)?,
green_magenta: rest[2] as i8,
})
}
fn parse_fobr(body: &[u8]) -> Result<Fobr> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: fobr version {version} != 0")));
}
if rest.len() < 4 {
return Err(Error::invalid(format!(
"avif: fobr too short ({} < 4)",
rest.len()
)));
}
Ok(Fobr {
focus_distance_numerator: read_u16(rest, 0)?,
focus_distance_denominator: read_u16(rest, 2)?,
})
}
fn parse_afbr(body: &[u8]) -> Result<Afbr> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: afbr version {version} != 0")));
}
if rest.len() < 2 {
return Err(Error::invalid(format!(
"avif: afbr too short ({} < 2)",
rest.len()
)));
}
Ok(Afbr {
flash_exposure_numerator: rest[0] as i8,
flash_exposure_denominator: rest[1] as i8,
})
}
fn parse_dobr(body: &[u8]) -> Result<Dobr> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: dobr version {version} != 0")));
}
if rest.len() < 2 {
return Err(Error::invalid(format!(
"avif: dobr too short ({} < 2)",
rest.len()
)));
}
Ok(Dobr {
f_stop_numerator: rest[0] as i8,
f_stop_denominator: rest[1] as i8,
})
}
fn parse_pano(body: &[u8]) -> Result<Pano> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: pano version {version} != 0")));
}
if rest.is_empty() {
return Err(Error::invalid("avif: pano too short (0 < 1)"));
}
let panorama_direction = rest[0];
let grid = if (Pano::DIRECTION_GRID_RASTER..=Pano::DIRECTION_GRID_CONTINUOUS)
.contains(&panorama_direction)
{
if rest.len() < 3 {
return Err(Error::invalid(format!(
"avif: pano grid direction {panorama_direction} but body too short ({} < 3)",
rest.len()
)));
}
Some(PanoGrid {
rows_minus_one: rest[1],
columns_minus_one: rest[2],
})
} else {
None
};
Ok(Pano {
panorama_direction,
grid,
})
}
fn parse_subs(body: &[u8]) -> Result<Subs> {
let (version, flags, rest) = parse_full_box(body)?;
if version != 0 && version != 1 {
return Err(Error::invalid(format!(
"avif: subs version {version} unsupported (expected 0 or 1)"
)));
}
let entry_count = read_u32(rest, 0)?;
if entry_count != 1 {
return Err(Error::invalid(format!(
"avif: subs entry_count {entry_count} != 1 (HEIF §6.5.28)"
)));
}
let sample_delta = read_u32(rest, 4)?;
if sample_delta != 0 {
return Err(Error::invalid(format!(
"avif: subs sample_delta {sample_delta} != 0 (HEIF §6.5.28)"
)));
}
let subsample_count = read_u16(rest, 8)?;
let mut entries = Vec::with_capacity(subsample_count as usize);
let mut off = 10usize;
for _ in 0..subsample_count {
let subsample_size = if version == 1 {
let v = read_u32(rest, off)?;
off += 4;
v
} else {
let v = read_u16(rest, off)? as u32;
off += 2;
v
};
let priority_off = off
.checked_add(2)
.ok_or_else(|| Error::invalid("avif: subs entry offset overflow"))?;
if priority_off > rest.len() {
return Err(Error::invalid("avif: subs truncated sub-sample entry"));
}
let subsample_priority = rest[off];
let discardable = rest[off + 1];
off += 2;
let codec_specific_parameters = read_u32(rest, off)?;
off += 4;
entries.push(SubsEntry {
subsample_size,
subsample_priority,
discardable,
codec_specific_parameters,
});
}
Ok(Subs { flags, entries })
}
fn parse_tols(body: &[u8]) -> Result<Tols> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: tols version {version} != 0")));
}
if rest.len() < 2 {
return Err(Error::invalid(format!(
"avif: tols too short ({} < 2)",
rest.len()
)));
}
Ok(Tols {
target_ols_idx: read_u16(rest, 0)?,
})
}
fn parse_wipe(body: &[u8]) -> Result<Wipe> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: wipe version {version} != 0")));
}
if rest.is_empty() {
return Err(Error::invalid("avif: wipe too short (0 < 1)"));
}
Ok(Wipe {
transition_direction: rest[0],
})
}
fn parse_zoom(body: &[u8]) -> Result<Zoom> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: zoom version {version} != 0")));
}
if rest.is_empty() {
return Err(Error::invalid("avif: zoom too short (0 < 1)"));
}
let packed = rest[0];
Ok(Zoom {
transition_direction: packed >> 7,
transition_shape: packed & 0x7f,
})
}
fn parse_fade(body: &[u8]) -> Result<Fade> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: fade version {version} != 0")));
}
if rest.is_empty() {
return Err(Error::invalid("avif: fade too short (0 < 1)"));
}
Ok(Fade {
transition_direction: rest[0],
})
}
fn parse_splt(body: &[u8]) -> Result<Splt> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: splt version {version} != 0")));
}
if rest.is_empty() {
return Err(Error::invalid("avif: splt too short (0 < 1)"));
}
Ok(Splt {
transition_direction: rest[0],
})
}
fn parse_stpe(body: &[u8]) -> Result<Stpe> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: stpe version {version} != 0")));
}
if rest.is_empty() {
return Err(Error::invalid("avif: stpe too short (0 < 1)"));
}
Ok(Stpe {
transition_period: rest[0],
})
}
fn parse_ssld(body: &[u8]) -> Result<Ssld> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: ssld version {version} != 0")));
}
if rest.len() < 2 {
return Err(Error::invalid(format!(
"avif: ssld too short ({} < 2)",
rest.len()
)));
}
Ok(Ssld {
duration: read_u16(rest, 0)?,
})
}
fn parse_prdi(body: &[u8]) -> Result<Prdi> {
let (version, flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: prdi version {version} != 0")));
}
let one_item_per_step = flags & Prdi::FLAG_ONE_ITEM_PER_STEP != 0;
let alternative_is_candidate = flags & Prdi::FLAG_ALTERNATIVE_IS_CANDIDATE != 0;
let mut off = 0usize;
let step_count = if !one_item_per_step || alternative_is_candidate {
let v = read_u16(rest, off)?;
off += 2;
Some(v)
} else {
None
};
let item_counts = if !one_item_per_step {
let n = step_count.expect("prdi: step_count present when one_item_per_step is clear");
let mut counts = Vec::with_capacity(n as usize);
for _ in 0..n {
counts.push(read_u16(rest, off)?);
off += 2;
}
Some(counts)
} else {
None
};
Ok(Prdi {
flags,
step_count,
item_counts,
})
}
fn parse_sstr(_body: &[u8]) -> Result<Sstr> {
Ok(Sstr)
}
fn parse_txlo(body: &[u8]) -> Result<Txlo> {
let (version, flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: txlo version {version} != 0")));
}
let large = flags & Txlo::FLAG_LARGE_FIELD_SIZE != 0;
let mut off = 0usize;
let reference_width = read_txlo_u(rest, &mut off, large)?;
let reference_height = read_txlo_u(rest, &mut off, large)?;
let x = read_txlo_i(rest, &mut off, large)?;
let y = read_txlo_i(rest, &mut off, large)?;
let width = read_txlo_u(rest, &mut off, large)?;
let height = read_txlo_u(rest, &mut off, large)?;
let font_size = read_u16(rest, off)?;
off += 2;
let (direction, after_dir) = read_cstr(rest, off)?;
let (writing_mode, _after_wm) = read_cstr(rest, after_dir)?;
Ok(Txlo {
flags,
reference_width,
reference_height,
x,
y,
width,
height,
font_size,
direction,
writing_mode,
})
}
fn read_txlo_u(buf: &[u8], off: &mut usize, large: bool) -> Result<u32> {
if large {
let v = read_u32(buf, *off)?;
*off += 4;
Ok(v)
} else {
let v = read_u16(buf, *off)?;
*off += 2;
Ok(u32::from(v))
}
}
fn read_txlo_i(buf: &[u8], off: &mut usize, large: bool) -> Result<i32> {
if large {
let v = read_u32(buf, *off)? as i32;
*off += 4;
Ok(v)
} else {
let v = read_u16(buf, *off)? as i16 as i32;
*off += 2;
Ok(v)
}
}
fn parse_elng(body: &[u8]) -> Result<Elng> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: elng version {version} != 0")));
}
let (extended_language, _after) = read_cstr(rest, 0)?;
Ok(Elng { extended_language })
}
fn parse_fnch(body: &[u8]) -> Result<Fnch> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: fnch version {version} != 0")));
}
let (font_family, after_family) = read_cstr(rest, 0)?;
let (font_style, after_style) = read_cstr(rest, after_family)?;
let (font_weight, _after_weight) = read_cstr(rest, after_style)?;
Ok(Fnch {
font_family,
font_style,
font_weight,
})
}
fn parse_mskc(body: &[u8]) -> Result<MaskC> {
let (version, _flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: mskC version {version} != 0")));
}
let bits_per_pixel = *rest
.first()
.ok_or_else(|| Error::invalid("avif: mskC missing bits_per_pixel"))?;
Ok(MaskC { bits_per_pixel })
}
fn parse_cmex(body: &[u8]) -> Result<Cmex> {
let (version, flags, rest) = parse_full_box(body)?;
if version > 1 {
return Err(Error::invalid(format!("avif: cmex version {version} > 1")));
}
let mut off = 0usize;
let read_i32_at = |rest: &[u8], off: &mut usize| -> Result<i32> {
let v = read_u32(rest, *off)? as i32;
*off += 4;
Ok(v)
};
let mut cmex = Cmex {
flags,
version,
..Cmex::default()
};
if flags & Cmex::FLAG_POS_X_PRESENT != 0 {
cmex.pos_x = Some(read_i32_at(rest, &mut off)?);
}
if flags & Cmex::FLAG_POS_Y_PRESENT != 0 {
cmex.pos_y = Some(read_i32_at(rest, &mut off)?);
}
if flags & Cmex::FLAG_POS_Z_PRESENT != 0 {
cmex.pos_z = Some(read_i32_at(rest, &mut off)?);
}
if flags & Cmex::FLAG_ORIENTATION_PRESENT != 0 {
match version {
0 => {
if flags & Cmex::FLAG_ROT_LARGE_FIELD_SIZE != 0 {
cmex.quat_x = Some(read_i32_at(rest, &mut off)?);
cmex.quat_y = Some(read_i32_at(rest, &mut off)?);
cmex.quat_z = Some(read_i32_at(rest, &mut off)?);
} else {
cmex.quat_x = Some(i32::from(read_u16(rest, off)? as i16));
off += 2;
cmex.quat_y = Some(i32::from(read_u16(rest, off)? as i16));
off += 2;
cmex.quat_z = Some(i32::from(read_u16(rest, off)? as i16));
off += 2;
}
}
_ => {
return Err(Error::unsupported(
"avif: cmex version 1 orientation \
(ViewpointGlobalCoordinateSysRotationStruct, \
ISO/IEC 23090-7) is not supported",
));
}
}
}
if flags & Cmex::FLAG_ID_PRESENT != 0 {
cmex.id = Some(read_u32(rest, off)?);
}
Ok(cmex)
}
fn parse_cmin(body: &[u8]) -> Result<Cmin> {
let (version, flags, rest) = parse_full_box(body)?;
if version != 0 {
return Err(Error::invalid(format!("avif: cmin version {version} != 0")));
}
let full = flags & Cmin::FLAG_FULL_INTRINSICS != 0;
let need = if full { 20 } else { 12 };
if rest.len() < need {
return Err(Error::invalid(format!(
"avif: cmin too short ({} < {need})",
rest.len()
)));
}
let focal_length_x = read_u32(rest, 0)? as i32;
let principal_point_x = read_u32(rest, 4)? as i32;
let principal_point_y = read_u32(rest, 8)? as i32;
let (focal_length_y, skew_factor) = if full {
(
Some(read_u32(rest, 12)? as i32),
Some(read_u32(rest, 16)? as i32),
)
} else {
(None, None)
};
Ok(Cmin {
flags,
focal_length_x,
principal_point_x,
principal_point_y,
focal_length_y,
skew_factor,
})
}
#[cfg(test)]
fn meta_with_child(box_type: &[u8; 4], payload: &[u8]) -> Result<Meta> {
let mut body = vec![0u8, 0, 0, 0]; body.extend_from_slice(&((8 + payload.len()) as u32).to_be_bytes());
body.extend_from_slice(box_type);
body.extend_from_slice(payload);
Meta::parse(&body)
}
#[cfg(test)]
fn parse_infe(payload: &[u8]) -> Result<ItemInfo> {
let mut iinf = vec![0u8, 0, 0, 0, 0, 1]; iinf.extend_from_slice(&((8 + payload.len()) as u32).to_be_bytes());
iinf.extend_from_slice(b"infe");
iinf.extend_from_slice(payload);
let mut items = meta_with_child(b"iinf", &iinf)?.items;
items
.pop()
.ok_or_else(|| Error::invalid("test: iinf yielded no item"))
}
#[cfg(test)]
fn parse_iloc(payload: &[u8]) -> Result<Vec<ItemLocation>> {
Ok(meta_with_child(b"iloc", payload)?.locations)
}
#[cfg(test)]
fn parse_iref(payload: &[u8]) -> Result<Vec<IrefEntry>> {
Ok(meta_with_child(b"iref", payload)?.irefs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn colr_nclx() {
let buf = [
b'n', b'c', b'l', b'x', 0x00, 0x01, 0x00, 0x0d, 0x00, 0x05, 0x80,
];
let c = parse_colr(&buf).unwrap();
match c {
Colr::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
} => {
assert_eq!(colour_primaries, 1);
assert_eq!(transfer_characteristics, 13);
assert_eq!(matrix_coefficients, 5);
assert!(full_range);
}
_ => panic!("expected nclx"),
}
}
fn build_iloc_v1(cm: u8, index_size: u8, extents: &[(u64, u32, u32)]) -> Vec<u8> {
let mut p = Vec::new();
p.push(1); p.extend_from_slice(&[0, 0, 0]); p.push(0x44);
p.push(index_size & 0x0f);
p.extend_from_slice(&1u16.to_be_bytes()); p.extend_from_slice(&7u16.to_be_bytes()); p.extend_from_slice(&[0x00, cm & 0x0f]); p.extend_from_slice(&0u16.to_be_bytes()); p.extend_from_slice(&(extents.len() as u16).to_be_bytes());
for (idx, off, len) in extents {
match index_size {
0 => {}
4 => p.extend_from_slice(&(*idx as u32).to_be_bytes()),
8 => p.extend_from_slice(&idx.to_be_bytes()),
_ => unreachable!(),
}
p.extend_from_slice(&off.to_be_bytes());
p.extend_from_slice(&len.to_be_bytes());
}
p
}
#[test]
fn iloc_v1_cm2_captures_extent_index() {
let payload = build_iloc_v1(2, 4, &[(3, 100, 16), (5, 200, 0)]);
let locs = parse_iloc(&payload).unwrap();
assert_eq!(locs.len(), 1);
let l = &locs[0];
assert_eq!(l.id, 7);
assert_eq!(l.construction_method, 2);
assert_eq!(l.extents.len(), 2);
assert_eq!(l.extents[0].extent_index, 3);
assert_eq!(l.extents[0].offset, 100);
assert_eq!(l.extents[0].length, 16);
assert_eq!(l.extents[1].extent_index, 5);
assert_eq!(l.extents[1].offset, 200);
assert_eq!(l.extents[1].length, 0);
}
#[test]
fn iloc_v1_index_size_zero_yields_zero_extent_index() {
let payload = build_iloc_v1(0, 0, &[(0, 42, 8)]);
let locs = parse_iloc(&payload).unwrap();
assert_eq!(locs[0].extents[0].extent_index, 0);
assert_eq!(locs[0].extents[0].offset, 42);
assert_eq!(locs[0].construction_method, 0);
}
#[test]
fn ispe_round_trip() {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&100u32.to_be_bytes());
buf.extend_from_slice(&200u32.to_be_bytes());
let ispe = parse_ispe(&buf).unwrap();
assert_eq!(ispe.width, 100);
assert_eq!(ispe.height, 200);
}
#[test]
fn irot_imir_masks_reserved_bits() {
let irot = parse_irot(&[0xff]).unwrap();
assert_eq!(irot.angle, 3);
let imir = parse_imir(&[0xff]).unwrap();
assert_eq!(imir.axis, 1);
}
#[test]
fn clap_reads_all_fields() {
let mut buf = Vec::new();
for i in 0..8u32 {
buf.extend_from_slice(&i.to_be_bytes());
}
let c = parse_clap(&buf).unwrap();
assert_eq!(c.clean_aperture_width_n, 0);
assert_eq!(c.clean_aperture_width_d, 1);
assert_eq!(c.vert_off_d, 7);
}
#[test]
fn auxc_extracts_urn() {
let mut buf = vec![0u8; 4]; buf.extend_from_slice(b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0");
let a = parse_auxc(&buf).unwrap();
assert!(a
.aux_type
.starts_with("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"));
}
#[test]
fn pixi_parses_3_channel_8bit() {
let mut buf = vec![0u8; 4];
buf.push(3);
buf.extend_from_slice(&[8, 8, 8]);
let pixi = parse_pixi(&buf).unwrap();
assert_eq!(pixi.num_channels(), 3);
assert_eq!(pixi.bits_per_channel, vec![8, 8, 8]);
assert_eq!(pixi.max_bit_depth(), 8);
assert!(pixi.is_uniform_depth());
}
#[test]
fn pixi_parses_10bit_hdr() {
let mut buf = vec![0u8; 4];
buf.push(3);
buf.extend_from_slice(&[10, 10, 10]);
let pixi = parse_pixi(&buf).unwrap();
assert_eq!(pixi.max_bit_depth(), 10);
assert!(pixi.is_uniform_depth());
}
#[test]
fn pixi_handles_mixed_depth() {
let mut buf = vec![0u8; 4];
buf.push(3);
buf.extend_from_slice(&[8, 10, 12]);
let pixi = parse_pixi(&buf).unwrap();
assert_eq!(pixi.max_bit_depth(), 12);
assert!(!pixi.is_uniform_depth());
}
#[test]
fn pixi_rejects_truncated_channel_list() {
let mut buf = vec![0u8; 4];
buf.push(4);
buf.extend_from_slice(&[8, 8]);
assert!(parse_pixi(&buf).is_err());
}
#[test]
fn pixi_zero_channels_parses_empty() {
let mut buf = vec![0u8; 4];
buf.push(0);
let pixi = parse_pixi(&buf).unwrap();
assert_eq!(pixi.num_channels(), 0);
assert_eq!(pixi.max_bit_depth(), 0);
assert!(!pixi.is_uniform_depth());
}
#[test]
fn pasp_parses_square_and_anamorphic() {
let mut buf = Vec::new();
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(&1u32.to_be_bytes());
let p = parse_pasp(&buf).unwrap();
assert!(p.is_square());
assert_eq!(p.ratio(), Some(1.0));
let mut buf = Vec::new();
buf.extend_from_slice(&16u32.to_be_bytes());
buf.extend_from_slice(&11u32.to_be_bytes());
let p = parse_pasp(&buf).unwrap();
assert!(!p.is_square());
let r = p.ratio().unwrap();
assert!((r - 16.0 / 11.0).abs() < 1e-9);
}
#[test]
fn pasp_zero_v_spacing_is_safe() {
let mut buf = Vec::new();
buf.extend_from_slice(&4u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
let p = parse_pasp(&buf).unwrap();
assert_eq!(p.ratio(), None);
assert!(!p.is_square());
}
#[test]
fn pasp_rejects_truncated() {
let buf = vec![0u8; 4]; assert!(parse_pasp(&buf).is_err());
}
#[test]
fn iref_v0_round_trip() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); let child_payload: Vec<u8> = {
let mut cp = Vec::new();
cp.extend_from_slice(&2u16.to_be_bytes()); cp.extend_from_slice(&1u16.to_be_bytes()); cp.extend_from_slice(&1u16.to_be_bytes()); cp
};
let child_size = (8 + child_payload.len()) as u32;
body.extend_from_slice(&child_size.to_be_bytes());
body.extend_from_slice(b"auxl");
body.extend_from_slice(&child_payload);
let refs = parse_iref(&body).unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(&refs[0].reference_type, b"auxl");
assert_eq!(refs[0].from_id, 2);
assert_eq!(refs[0].to_ids, vec![1]);
}
#[test]
fn mdcv_round_trip() {
let mut buf = Vec::new();
buf.extend_from_slice(&13250u16.to_be_bytes());
buf.extend_from_slice(&34500u16.to_be_bytes());
buf.extend_from_slice(&7500u16.to_be_bytes());
buf.extend_from_slice(&40000u16.to_be_bytes());
buf.extend_from_slice(&3000u16.to_be_bytes());
buf.extend_from_slice(&1000u16.to_be_bytes());
buf.extend_from_slice(&15635u16.to_be_bytes());
buf.extend_from_slice(&16450u16.to_be_bytes());
buf.extend_from_slice(&10_000_000u32.to_be_bytes());
buf.extend_from_slice(&50u32.to_be_bytes());
let m = parse_mdcv(&buf).unwrap();
assert_eq!(m.display_primaries_xy[0], (13250, 34500)); assert_eq!(m.display_primaries_xy[1], (7500, 40000)); assert_eq!(m.display_primaries_xy[2], (3000, 1000)); assert_eq!(m.white_point_xy, (15635, 16450));
assert_eq!(m.max_display_mastering_luminance, 10_000_000);
assert_eq!(m.min_display_mastering_luminance, 50);
}
#[test]
fn mdcv_rejects_truncated() {
let buf = vec![0u8; 23];
assert!(parse_mdcv(&buf).is_err());
}
#[test]
fn clli_round_trip() {
let mut buf = Vec::new();
buf.extend_from_slice(&1000u16.to_be_bytes()); buf.extend_from_slice(&400u16.to_be_bytes()); let c = parse_clli(&buf).unwrap();
assert_eq!(c.max_content_light_level, 1000);
assert_eq!(c.max_pic_average_light_level, 400);
}
#[test]
fn clli_rejects_truncated() {
let buf = vec![0u8; 3];
assert!(parse_clli(&buf).is_err());
}
#[test]
fn cclv_round_trip() {
let mut buf = Vec::new();
buf.extend_from_slice(&2000u16.to_be_bytes()); buf.extend_from_slice(&800u16.to_be_bytes()); let c = parse_cclv(&buf).unwrap();
assert_eq!(c.max_content_light_level, 2000);
assert_eq!(c.max_pic_average_light_level, 800);
}
#[test]
fn cclv_rejects_truncated() {
let buf = vec![0u8; 1];
assert!(parse_cclv(&buf).is_err());
}
#[test]
fn amve_round_trip() {
let buf = [0x00, 0x01, 0x86, 0xA0, 0x3D, 0x13, 0x40, 0x42];
let a = parse_amve(&buf).unwrap();
assert_eq!(a.ambient_illuminance, 100_000);
assert_eq!(a.ambient_light_x, 15635);
assert_eq!(a.ambient_light_y, 16450);
assert!((a.illuminance_lux() - 10.0).abs() < 1e-9);
let (x, y) = a.light_xy();
assert!((x - 0.3127).abs() < 1e-9);
assert!((y - 0.3290).abs() < 1e-9);
}
#[test]
fn amve_property_kind_round_trip() {
let body = [0x00, 0x01, 0x86, 0xA0, 0x3D, 0x13, 0x40, 0x42];
let mut buf = Vec::new();
buf.extend_from_slice(&(8u32 + body.len() as u32).to_be_bytes());
buf.extend_from_slice(b"amve");
buf.extend_from_slice(&body);
let props = parse_ipco(&buf).unwrap();
assert_eq!(props.len(), 1);
assert_eq!(props[0].kind(), AMVE);
match &props[0] {
Property::Amve(a) => assert_eq!(a.ambient_illuminance, 100_000),
other => panic!("expected Amve, got {other:?}"),
}
}
#[test]
fn amve_rejects_truncated() {
let buf = vec![0u8; 7];
assert!(parse_amve(&buf).is_err());
}
fn build_infe_v2(item_type: &[u8; 4], name: &str, tail: &[u8]) -> Vec<u8> {
let mut buf = vec![2u8, 0, 0, 0];
buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(item_type); buf.extend_from_slice(name.as_bytes());
buf.push(0); buf.extend_from_slice(tail);
buf
}
#[test]
fn infe_v2_mime_parses_content_type_and_encoding() {
let mut tail = Vec::new();
tail.extend_from_slice(b"application/rdf+xml\0");
tail.extend_from_slice(b"\0"); let payload = build_infe_v2(b"mime", "xmp", &tail);
let info = parse_infe(&payload).unwrap();
assert_eq!(info.id, 1);
assert_eq!(&info.item_type, b"mime");
assert_eq!(info.name, "xmp");
assert_eq!(info.content_type.as_deref(), Some("application/rdf+xml"));
assert!(info.content_encoding.is_none());
assert!(info.item_uri_type.is_none());
}
#[test]
fn infe_v3_mime_octet_stream_for_exif() {
let mut buf = vec![3u8, 0, 0, 0];
buf.extend_from_slice(&42u32.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(b"mime"); buf.extend_from_slice(b"\0"); buf.extend_from_slice(b"application/octet-stream\0");
let info = parse_infe(&buf).unwrap();
assert_eq!(info.id, 42);
assert_eq!(&info.item_type, b"mime");
assert_eq!(
info.content_type.as_deref(),
Some("application/octet-stream")
);
assert!(info.content_encoding.is_none());
}
#[test]
fn infe_v2_uri_parses_uri_type() {
let mut tail = Vec::new();
tail.extend_from_slice(b"https://example.invalid/spec\0");
let payload = build_infe_v2(b"uri ", "uri-meta", &tail);
let info = parse_infe(&payload).unwrap();
assert_eq!(&info.item_type, b"uri ");
assert!(info.content_type.is_none());
assert_eq!(
info.item_uri_type.as_deref(),
Some("https://example.invalid/spec")
);
}
#[test]
fn infe_v2_generic_item_type_stops_at_name() {
let payload = build_infe_v2(b"av01", "color", &[]);
let info = parse_infe(&payload).unwrap();
assert_eq!(&info.item_type, b"av01");
assert!(info.content_type.is_none());
assert!(info.content_encoding.is_none());
assert!(info.item_uri_type.is_none());
let payload = build_infe_v2(b"Exif", "exif-blob", &[]);
let info = parse_infe(&payload).unwrap();
assert_eq!(&info.item_type, b"Exif");
assert!(info.content_type.is_none());
}
#[test]
fn iref_sources_of_returns_all_matches() {
let m = Meta {
irefs: vec![
IrefEntry {
reference_type: *b"thmb",
from_id: 10,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"thmb",
from_id: 11,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"auxl",
from_id: 12,
to_ids: vec![1],
},
IrefEntry {
reference_type: *b"thmb",
from_id: 13,
to_ids: vec![2],
},
],
..Meta::default()
};
let mut got = m.iref_sources_of(b"thmb", 1);
got.sort_unstable();
assert_eq!(got, vec![10, 11]);
assert!(m.iref_sources_of(b"thmb", 99).is_empty());
}
#[test]
fn iref_targets_of_concatenates_in_entry_order() {
let m = Meta {
irefs: vec![
IrefEntry {
reference_type: *b"pred",
from_id: 7,
to_ids: vec![3, 5],
},
IrefEntry {
reference_type: *b"pred",
from_id: 7,
to_ids: vec![6],
},
IrefEntry {
reference_type: *b"base",
from_id: 7,
to_ids: vec![99],
},
IrefEntry {
reference_type: *b"pred",
from_id: 8,
to_ids: vec![1],
},
],
..Meta::default()
};
assert_eq!(m.iref_targets_of(b"pred", 7), vec![3, 5, 6]);
assert_eq!(m.iref_targets_of(b"base", 7), vec![99]);
assert!(m.iref_targets_of(b"pred", 99).is_empty());
}
#[test]
fn is_alpha_premultiplied_for_detects_prem_iref() {
let m = Meta {
irefs: vec![IrefEntry {
reference_type: *b"prem",
from_id: 2,
to_ids: vec![1],
}],
..Meta::default()
};
assert!(m.is_alpha_premultiplied_for(1));
assert!(!m.is_alpha_premultiplied_for(2));
let empty = Meta::default();
assert!(!empty.is_alpha_premultiplied_for(1));
}
#[test]
fn auxc_kind_classifies_alpha() {
let mpeg = AuxC {
aux_type: AUX_URN_ALPHA_MPEG.to_string(),
aux_subtype: Vec::new(),
};
assert_eq!(mpeg.kind(), AuxKind::Alpha);
assert!(mpeg.is_alpha());
assert!(!mpeg.is_depth_map());
let hevc = AuxC {
aux_type: AUX_URN_ALPHA_HEVC.to_string(),
aux_subtype: Vec::new(),
};
assert_eq!(hevc.kind(), AuxKind::Alpha);
assert!(hevc.is_alpha());
}
#[test]
fn auxc_kind_classifies_depth_map() {
let mpeg = AuxC {
aux_type: AUX_URN_DEPTH_MPEG.to_string(),
aux_subtype: Vec::new(),
};
assert_eq!(mpeg.kind(), AuxKind::DepthMap);
assert!(mpeg.is_depth_map());
assert!(!mpeg.is_alpha());
let hevc = AuxC {
aux_type: AUX_URN_DEPTH_HEVC.to_string(),
aux_subtype: Vec::new(),
};
assert_eq!(hevc.kind(), AuxKind::DepthMap);
}
#[test]
fn auxc_kind_classifies_hdr_gain_map() {
let g = AuxC {
aux_type: AUX_URN_HDR_GAINMAP.to_string(),
aux_subtype: Vec::new(),
};
assert_eq!(g.kind(), AuxKind::HdrGainMap);
assert!(g.is_hdr_gain_map());
assert!(!g.is_alpha());
}
#[test]
fn auxc_kind_other_for_unknown_urn() {
let custom = AuxC {
aux_type: "urn:example:foo:bar".to_string(),
aux_subtype: Vec::new(),
};
assert_eq!(custom.kind(), AuxKind::Other);
assert!(!custom.is_alpha());
let prefix = AuxC {
aux_type: format!("{AUX_URN_ALPHA_MPEG}:extended"),
aux_subtype: Vec::new(),
};
assert_eq!(prefix.kind(), AuxKind::Other);
}
#[test]
fn rloc_round_trip() {
let mut buf = vec![0u8; 4]; buf.extend_from_slice(&96u32.to_be_bytes()); buf.extend_from_slice(&128u32.to_be_bytes()); let r = parse_rloc(&buf).unwrap();
assert_eq!(r.horizontal_offset, 96);
assert_eq!(r.vertical_offset, 128);
}
#[test]
fn rloc_rejects_nonzero_version() {
let mut buf = vec![1u8, 0, 0, 0]; buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
assert!(parse_rloc(&buf).is_err());
}
#[test]
fn rloc_rejects_truncated() {
let buf = vec![0u8; 4 + 4]; assert!(parse_rloc(&buf).is_err());
}
#[test]
fn lsel_round_trip() {
let buf = 3u16.to_be_bytes();
let l = parse_lsel(&buf).unwrap();
assert_eq!(l.layer_id, 3);
}
#[test]
fn lsel_rejects_truncated() {
let buf = vec![0u8; 1];
assert!(parse_lsel(&buf).is_err());
}
#[test]
fn ipco_dispatches_rloc_and_lsel() {
let rloc_body = {
let mut b = vec![0u8; 4];
b.extend_from_slice(&10u32.to_be_bytes());
b.extend_from_slice(&20u32.to_be_bytes());
b
};
let lsel_body = 5u16.to_be_bytes().to_vec();
let mut ipco = Vec::new();
let rloc_size = 8 + rloc_body.len() as u32;
ipco.extend_from_slice(&rloc_size.to_be_bytes());
ipco.extend_from_slice(b"rloc");
ipco.extend_from_slice(&rloc_body);
let lsel_size = 8 + lsel_body.len() as u32;
ipco.extend_from_slice(&lsel_size.to_be_bytes());
ipco.extend_from_slice(b"lsel");
ipco.extend_from_slice(&lsel_body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 2);
match &props[0] {
Property::Rloc(r) => {
assert_eq!(r.horizontal_offset, 10);
assert_eq!(r.vertical_offset, 20);
}
other => panic!("expected Rloc, got {other:?}"),
}
match &props[1] {
Property::Lsel(l) => assert_eq!(l.layer_id, 5),
other => panic!("expected Lsel, got {other:?}"),
}
}
#[test]
fn a1op_reads_op_index() {
let a = parse_a1op(&[7]).unwrap();
assert_eq!(a.op_index, 7);
assert!(parse_a1op(&[]).is_err());
}
#[test]
fn a1lx_16bit_field_width() {
let mut buf = vec![0x00u8];
buf.extend_from_slice(&100u16.to_be_bytes());
buf.extend_from_slice(&200u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
let a = parse_a1lx(&buf).unwrap();
assert!(!a.large_size);
assert_eq!(a.layer_size, [100, 200, 0]);
assert_eq!(a.documented_layers(), 2);
}
#[test]
fn a1lx_32bit_field_width_ignores_reserved() {
let mut buf = vec![0xFFu8];
buf.extend_from_slice(&70_000u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
let a = parse_a1lx(&buf).unwrap();
assert!(a.large_size);
assert_eq!(a.layer_size, [70_000, 0, 0]);
assert_eq!(a.documented_layers(), 1);
let short = vec![0x01u8, 0, 0, 0, 0];
assert!(parse_a1lx(&short).is_err());
}
#[test]
fn ipco_dispatches_a1op_and_a1lx() {
let a1op_body = vec![3u8]; let a1lx_body = {
let mut b = vec![0x00u8]; b.extend_from_slice(&5u16.to_be_bytes());
b.extend_from_slice(&0u16.to_be_bytes());
b.extend_from_slice(&0u16.to_be_bytes());
b
};
let mut ipco = Vec::new();
let s = 8 + a1op_body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"a1op");
ipco.extend_from_slice(&a1op_body);
let s = 8 + a1lx_body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"a1lx");
ipco.extend_from_slice(&a1lx_body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 2);
match &props[0] {
Property::A1op(a) => assert_eq!(a.op_index, 3),
other => panic!("expected A1op, got {other:?}"),
}
match &props[1] {
Property::A1lx(a) => {
assert!(!a.large_size);
assert_eq!(a.layer_size, [5, 0, 0]);
}
other => panic!("expected A1lx, got {other:?}"),
}
}
#[test]
fn unsupported_essential_property_detected() {
let m = Meta {
properties: vec![
Property::Irot(Irot { angle: 1 }),
Property::Other(*b"zzzz", vec![0, 1, 2]),
],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![
PropertyAssociation {
index: 0,
essential: true,
},
PropertyAssociation {
index: 1,
essential: true,
},
],
}],
..Meta::default()
};
assert!(m.has_unsupported_essential_property(1));
assert_eq!(m.unsupported_essential_properties(1), vec![*b"zzzz"]);
assert!(!m.has_unsupported_essential_property(99));
}
#[test]
fn non_essential_unknown_property_does_not_block() {
let m = Meta {
properties: vec![Property::Other(*b"zzzz", vec![0])],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn essential_property_with_dangling_index_blocks() {
let m = Meta {
properties: vec![],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 5,
essential: true,
}],
}],
..Meta::default()
};
assert!(m.has_unsupported_essential_property(1));
}
fn iscl_body(wn: u16, wd: u16, hn: u16, hd: u16) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&wn.to_be_bytes());
buf.extend_from_slice(&wd.to_be_bytes());
buf.extend_from_slice(&hn.to_be_bytes());
buf.extend_from_slice(&hd.to_be_bytes());
buf
}
#[test]
fn iscl_round_trip_reads_all_four_fields() {
let buf = iscl_body(3, 2, 5, 4);
let s = parse_iscl(&buf).unwrap();
assert_eq!(s.target_width_numerator, 3);
assert_eq!(s.target_width_denominator, 2);
assert_eq!(s.target_height_numerator, 5);
assert_eq!(s.target_height_denominator, 4);
assert!(s.is_well_formed());
}
#[test]
fn iscl_rejects_truncated_body() {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0, 1, 0, 1, 0, 1, 0]);
assert!(parse_iscl(&buf).is_err());
}
#[test]
fn iscl_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0]; buf.extend_from_slice(&iscl_body(1, 1, 1, 1)[4..]);
assert!(parse_iscl(&buf).is_err());
}
#[test]
fn iscl_is_well_formed_rejects_zero_field() {
let s = parse_iscl(&iscl_body(0, 2, 5, 4)).unwrap();
assert!(!s.is_well_formed());
let s = parse_iscl(&iscl_body(3, 0, 5, 4)).unwrap();
assert!(!s.is_well_formed());
let s = parse_iscl(&iscl_body(3, 2, 0, 4)).unwrap();
assert!(!s.is_well_formed());
let s = parse_iscl(&iscl_body(3, 2, 5, 0)).unwrap();
assert!(!s.is_well_formed());
}
#[test]
fn iscl_scaled_dims_applies_ceil_division() {
let s = parse_iscl(&iscl_body(3, 2, 5, 4)).unwrap();
assert_eq!(s.scaled_dims(100, 100), Some((150, 125)));
let s = parse_iscl(&iscl_body(1, 3, 1, 7)).unwrap();
assert_eq!(s.scaled_dims(100, 100), Some((34, 15)));
let s = parse_iscl(&iscl_body(1, 1, 1, 1)).unwrap();
assert_eq!(s.scaled_dims(640, 480), Some((640, 480)));
}
#[test]
fn iscl_scaled_dims_short_circuits_zero_denominator() {
let s = parse_iscl(&iscl_body(1, 0, 1, 1)).unwrap();
assert_eq!(s.scaled_dims(100, 100), None);
let s = parse_iscl(&iscl_body(1, 1, 1, 0)).unwrap();
assert_eq!(s.scaled_dims(100, 100), None);
}
#[test]
fn iscl_scaled_dims_saturates_on_u32_overflow() {
let s = parse_iscl(&iscl_body(u16::MAX, 1, u16::MAX, 1)).unwrap();
let dims = s.scaled_dims(u32::MAX, u32::MAX).unwrap();
assert_eq!(dims, (u32::MAX, u32::MAX));
}
#[test]
fn iscl_dispatched_through_parse_ipco() {
let body = iscl_body(7, 5, 11, 3);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"iscl");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Iscl(s) => {
assert_eq!(s.target_width_numerator, 7);
assert_eq!(s.target_height_denominator, 3);
}
other => panic!("expected Iscl, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"iscl");
}
fn rref_body(types: &[&[u8; 4]]) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.push(types.len() as u8);
for t in types {
buf.extend_from_slice(&t[..]);
}
buf
}
#[test]
fn rref_round_trip_reads_typed_four_ccs() {
let buf = rref_body(&[b"dimg", b"auxl", b"thmb"]);
let r = parse_rref(&buf).unwrap();
assert_eq!(r.count(), 3);
assert_eq!(r.reference_types[0], *b"dimg");
assert_eq!(r.reference_types[1], *b"auxl");
assert_eq!(r.reference_types[2], *b"thmb");
assert!(r.requires(&b(b"dimg")));
assert!(r.requires(&b(b"thmb")));
assert!(!r.requires(&b(b"pred")));
}
#[test]
fn rref_empty_list_parses() {
let buf = rref_body(&[]);
let r = parse_rref(&buf).unwrap();
assert_eq!(r.count(), 0);
assert!(!r.requires(&b(b"dimg")));
}
#[test]
fn rref_rejects_truncated_table() {
let mut buf = vec![0u8; 4];
buf.push(2);
buf.extend_from_slice(b"dimg");
buf.extend_from_slice(&[0x00, 0x00]); assert!(parse_rref(&buf).is_err());
}
#[test]
fn rref_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0]; buf.push(0);
assert!(parse_rref(&buf).is_err());
}
#[test]
fn rref_rejects_missing_count() {
let buf = vec![0u8; 4];
assert!(parse_rref(&buf).is_err());
}
#[test]
fn rref_dispatched_through_parse_ipco() {
let body = rref_body(&[b"dimg"]);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"rref");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Rref(r) => {
assert_eq!(r.count(), 1);
assert_eq!(r.reference_types[0], *b"dimg");
}
other => panic!("expected Rref, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"rref");
}
#[test]
fn iscl_and_rref_essential_associations_are_recognised() {
let m = Meta {
properties: vec![
Property::Iscl(Iscl {
target_width_numerator: 1,
target_width_denominator: 1,
target_height_numerator: 1,
target_height_denominator: 1,
}),
Property::Rref(Rref {
reference_types: vec![*b"dimg"],
}),
],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![
PropertyAssociation {
index: 0,
essential: true,
},
PropertyAssociation {
index: 1,
essential: true,
},
],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
fn crtt_body(creation_time: u64) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&creation_time.to_be_bytes());
buf
}
#[test]
fn crtt_round_trip_reads_creation_time() {
let raw = 0x0102_0304_0506_0708u64;
let c = parse_crtt(&crtt_body(raw)).unwrap();
assert_eq!(c.creation_time, raw);
}
#[test]
fn crtt_rejects_truncated_body() {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0]);
assert!(parse_crtt(&buf).is_err());
}
#[test]
fn crtt_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&0u64.to_be_bytes());
assert!(parse_crtt(&buf).is_err());
}
#[test]
fn crtt_rejects_missing_payload() {
let buf = vec![0u8; 4];
assert!(parse_crtt(&buf).is_err());
}
#[test]
fn crtt_dispatched_through_parse_ipco() {
let body = crtt_body(0xDEAD_BEEF_CAFE_F00Du64);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"crtt");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Crtt(c) => assert_eq!(c.creation_time, 0xDEAD_BEEF_CAFE_F00Du64),
other => panic!("expected Crtt, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"crtt");
}
#[test]
fn crtt_seconds_since_unix_epoch_matches_documented_offset() {
let c = Crtt { creation_time: 0 };
assert_eq!(c.seconds_since_unix_epoch(), None);
let c = Crtt {
creation_time: HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS * 1_000_000,
};
assert_eq!(c.seconds_since_unix_epoch(), Some(0));
let c = Crtt {
creation_time: (HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS + 1) * 1_000_000,
};
assert_eq!(c.seconds_since_unix_epoch(), Some(1));
}
#[test]
fn crtt_subsecond_micros_isolates_remainder() {
let secs = HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS;
let c = Crtt {
creation_time: secs * 1_000_000 + 500_000,
};
assert_eq!(c.seconds_since_unix_epoch(), Some(0));
assert_eq!(c.subsecond_micros(), 500_000);
let c = Crtt {
creation_time: secs * 1_000_000,
};
assert_eq!(c.subsecond_micros(), 0);
let c = Crtt {
creation_time: secs * 1_000_000 + 999_999,
};
assert_eq!(c.subsecond_micros(), 999_999);
}
#[test]
fn crtt_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Crtt(Crtt { creation_time: 0 })],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
fn mdft_body(modification_time: u64) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&modification_time.to_be_bytes());
buf
}
#[test]
fn mdft_round_trip_reads_modification_time() {
let raw = 0x1122_3344_5566_7788u64;
let m = parse_mdft(&mdft_body(raw)).unwrap();
assert_eq!(m.modification_time, raw);
}
#[test]
fn mdft_rejects_truncated_body() {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0]);
assert!(parse_mdft(&buf).is_err());
}
#[test]
fn mdft_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&0u64.to_be_bytes());
assert!(parse_mdft(&buf).is_err());
}
#[test]
fn mdft_rejects_missing_payload() {
let buf = vec![0u8; 4];
assert!(parse_mdft(&buf).is_err());
}
#[test]
fn mdft_dispatched_through_parse_ipco() {
let body = mdft_body(0xFEED_FACE_DEAD_BEEFu64);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"mdft");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Mdft(m) => assert_eq!(m.modification_time, 0xFEED_FACE_DEAD_BEEFu64),
other => panic!("expected Mdft, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"mdft");
}
#[test]
fn mdft_seconds_since_unix_epoch_matches_documented_offset() {
let m = Mdft {
modification_time: 0,
};
assert_eq!(m.seconds_since_unix_epoch(), None);
let m = Mdft {
modification_time: HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS * 1_000_000,
};
assert_eq!(m.seconds_since_unix_epoch(), Some(0));
let m = Mdft {
modification_time: (HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS + 1) * 1_000_000,
};
assert_eq!(m.seconds_since_unix_epoch(), Some(1));
}
#[test]
fn mdft_subsecond_micros_isolates_remainder() {
let secs = HEIF_EPOCH_TO_UNIX_EPOCH_SECONDS;
let m = Mdft {
modification_time: secs * 1_000_000 + 500_000,
};
assert_eq!(m.seconds_since_unix_epoch(), Some(0));
assert_eq!(m.subsecond_micros(), 500_000);
let m = Mdft {
modification_time: secs * 1_000_000,
};
assert_eq!(m.subsecond_micros(), 0);
let m = Mdft {
modification_time: secs * 1_000_000 + 999_999,
};
assert_eq!(m.subsecond_micros(), 999_999);
}
#[test]
fn mdft_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Mdft(Mdft {
modification_time: 0,
})],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn mdft_and_crtt_coexist_on_same_item() {
let crtt_raw = 0x0102_0304_0506_0708u64;
let mdft_raw = 0xAABB_CCDD_EEFF_0011u64;
let crtt_b = crtt_body(crtt_raw);
let mdft_b = mdft_body(mdft_raw);
let mut ipco = Vec::new();
let s1 = 8 + crtt_b.len() as u32;
ipco.extend_from_slice(&s1.to_be_bytes());
ipco.extend_from_slice(b"crtt");
ipco.extend_from_slice(&crtt_b);
let s2 = 8 + mdft_b.len() as u32;
ipco.extend_from_slice(&s2.to_be_bytes());
ipco.extend_from_slice(b"mdft");
ipco.extend_from_slice(&mdft_b);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 2);
let m = Meta {
properties: props,
associations: vec![ItemPropertyAssociation {
item_id: 7,
entries: vec![
PropertyAssociation {
index: 0,
essential: false,
},
PropertyAssociation {
index: 1,
essential: false,
},
],
}],
..Meta::default()
};
match m.property_for(7, &CRTT) {
Some(Property::Crtt(c)) => assert_eq!(c.creation_time, crtt_raw),
other => panic!("expected Crtt, got {other:?}"),
}
match m.property_for(7, &MDFT) {
Some(Property::Mdft(d)) => assert_eq!(d.modification_time, mdft_raw),
other => panic!("expected Mdft, got {other:?}"),
}
}
fn udes_body(lang: &str, name: &str, description: &str, tags: &str) -> Vec<u8> {
let mut buf = vec![0u8; 4];
for s in [lang, name, description, tags] {
buf.extend_from_slice(s.as_bytes());
buf.push(0);
}
buf
}
#[test]
fn udes_round_trip_reads_all_four_fields() {
let u = parse_udes(&udes_body(
"en-US",
"Front porch",
"Photo of the cabin's front porch at dusk",
"outdoor,porch,dusk",
))
.unwrap();
assert_eq!(u.lang, "en-US");
assert_eq!(u.name, "Front porch");
assert_eq!(u.description, "Photo of the cabin's front porch at dusk");
assert_eq!(u.tags, "outdoor,porch,dusk");
}
#[test]
fn udes_empty_strings_are_preserved_and_projectable_to_none() {
let u = parse_udes(&udes_body("", "", "", "")).unwrap();
assert_eq!(u.lang, "");
assert_eq!(u.name, "");
assert_eq!(u.description, "");
assert_eq!(u.tags, "");
assert_eq!(u.lang_opt(), None);
assert_eq!(u.name_opt(), None);
assert_eq!(u.description_opt(), None);
assert_eq!(u.tags_opt(), None);
assert!(u.tag_list().is_empty());
}
#[test]
fn udes_opt_helpers_round_trip_non_empty() {
let u = parse_udes(&udes_body("fr-FR", "Mer", "Vue de la mer", "mer,été")).unwrap();
assert_eq!(u.lang_opt(), Some("fr-FR"));
assert_eq!(u.name_opt(), Some("Mer"));
assert_eq!(u.description_opt(), Some("Vue de la mer"));
assert_eq!(u.tags_opt(), Some("mer,été"));
}
#[test]
fn udes_tag_list_splits_and_trims() {
let u = parse_udes(&udes_body(
"en",
"n",
"d",
"outdoor, sunset ,, beach,,, mountain",
))
.unwrap();
assert_eq!(u.tag_list(), vec!["outdoor", "sunset", "beach", "mountain"]);
assert_eq!(u.tags, "outdoor, sunset ,, beach,,, mountain");
}
#[test]
fn udes_preserves_utf8_multibyte() {
let u = parse_udes(&udes_body(
"zh-CN",
"海岸",
"夕暮れの海岸",
"海, 夕暮れ, 風景",
))
.unwrap();
assert_eq!(u.lang, "zh-CN");
assert_eq!(u.name, "海岸");
assert_eq!(u.description, "夕暮れの海岸");
assert_eq!(u.tag_list(), vec!["海", "夕暮れ", "風景"]);
}
#[test]
fn udes_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&[0, 0, 0, 0]);
assert!(parse_udes(&buf).is_err());
}
#[test]
fn udes_rejects_truncated_body() {
let mut buf = vec![0u8; 4];
buf.push(0);
buf.push(0);
buf.push(0);
buf.extend_from_slice(b"abc");
assert!(parse_udes(&buf).is_err());
}
#[test]
fn udes_tolerates_trailing_bytes() {
let mut body = udes_body("en", "n", "d", "t");
body.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let u = parse_udes(&body).unwrap();
assert_eq!(u.lang, "en");
assert_eq!(u.tags, "t");
}
#[test]
fn udes_dispatched_through_parse_ipco() {
let body = udes_body("en-US", "Cabin", "Front porch", "outdoor,porch");
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"udes");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Udes(u) => {
assert_eq!(u.lang, "en-US");
assert_eq!(u.name, "Cabin");
assert_eq!(u.description, "Front porch");
assert_eq!(u.tags, "outdoor,porch");
}
other => panic!("expected Udes, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"udes");
}
#[test]
fn udes_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Udes(Udes::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn udes_multiple_languages_coexist_on_same_item() {
let en = udes_body("en-US", "Beach", "Sunset over the bay", "beach,sunset");
let fr = udes_body(
"fr-FR",
"Plage",
"Coucher de soleil sur la baie",
"plage,coucher",
);
let mut ipco = Vec::new();
let se = 8 + en.len() as u32;
ipco.extend_from_slice(&se.to_be_bytes());
ipco.extend_from_slice(b"udes");
ipco.extend_from_slice(&en);
let sf = 8 + fr.len() as u32;
ipco.extend_from_slice(&sf.to_be_bytes());
ipco.extend_from_slice(b"udes");
ipco.extend_from_slice(&fr);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 2);
let langs: Vec<&str> = props
.iter()
.filter_map(|p| match p {
Property::Udes(u) => Some(u.lang.as_str()),
_ => None,
})
.collect();
assert_eq!(langs, vec!["en-US", "fr-FR"]);
}
fn altt_body(alt_text: &str, alt_lang: &str) -> Vec<u8> {
let mut buf = vec![0u8; 4];
for s in [alt_text, alt_lang] {
buf.extend_from_slice(s.as_bytes());
buf.push(0);
}
buf
}
#[test]
fn altt_round_trip_reads_text_then_lang() {
let a = parse_altt(&altt_body(
"Photo of the cabin's front porch at dusk",
"en-US",
))
.unwrap();
assert_eq!(a.alt_text, "Photo of the cabin's front porch at dusk");
assert_eq!(a.alt_lang, "en-US");
}
#[test]
fn altt_empty_strings_are_preserved_and_projectable_to_none() {
let a = parse_altt(&altt_body("", "")).unwrap();
assert_eq!(a.alt_text, "");
assert_eq!(a.alt_lang, "");
assert_eq!(a.alt_text_opt(), None);
assert_eq!(a.alt_lang_opt(), None);
}
#[test]
fn altt_opt_helpers_round_trip_non_empty() {
let a = parse_altt(&altt_body("Vue de la mer", "fr-FR")).unwrap();
assert_eq!(a.alt_text_opt(), Some("Vue de la mer"));
assert_eq!(a.alt_lang_opt(), Some("fr-FR"));
}
#[test]
fn altt_preserves_utf8_multibyte() {
let a = parse_altt(&altt_body("夕暮れの海岸", "zh-CN")).unwrap();
assert_eq!(a.alt_text, "夕暮れの海岸");
assert_eq!(a.alt_lang, "zh-CN");
}
#[test]
fn altt_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&[0, 0]);
assert!(parse_altt(&buf).is_err());
}
#[test]
fn altt_rejects_truncated_body() {
let mut buf = vec![0u8; 4];
buf.push(0);
buf.extend_from_slice(b"en");
assert!(parse_altt(&buf).is_err());
}
#[test]
fn altt_tolerates_trailing_bytes() {
let mut body = altt_body("hi", "en");
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let a = parse_altt(&body).unwrap();
assert_eq!(a.alt_text, "hi");
assert_eq!(a.alt_lang, "en");
}
#[test]
fn altt_dispatched_through_parse_ipco() {
let body = altt_body("Cabin at dusk", "en-US");
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"altt");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Altt(a) => {
assert_eq!(a.alt_text, "Cabin at dusk");
assert_eq!(a.alt_lang, "en-US");
}
other => panic!("expected Altt, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"altt");
}
#[test]
fn altt_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Altt(Altt::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn altt_multiple_languages_coexist_on_same_item() {
let en = altt_body("Sunset over the bay", "en-US");
let fr = altt_body("Coucher de soleil sur la baie", "fr-FR");
let mut ipco = Vec::new();
let se = 8 + en.len() as u32;
ipco.extend_from_slice(&se.to_be_bytes());
ipco.extend_from_slice(b"altt");
ipco.extend_from_slice(&en);
let sf = 8 + fr.len() as u32;
ipco.extend_from_slice(&sf.to_be_bytes());
ipco.extend_from_slice(b"altt");
ipco.extend_from_slice(&fr);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 2);
let langs: Vec<&str> = props
.iter()
.filter_map(|p| match p {
Property::Altt(a) => Some(a.alt_lang.as_str()),
_ => None,
})
.collect();
assert_eq!(langs, vec!["en-US", "fr-FR"]);
}
#[test]
fn altt_field_order_is_text_then_lang_not_reversed() {
let a = parse_altt(&altt_body("Sunset", "en-US")).unwrap();
assert_eq!(a.alt_text, "Sunset");
assert_eq!(a.alt_lang, "en-US");
let mis = parse_altt(&altt_body("en-US", "Sunset")).unwrap();
assert_eq!(mis.alt_text, "en-US");
assert_eq!(mis.alt_lang, "Sunset");
}
fn aebr_body(exposure_step: i8, exposure_numerator: i8) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.push(exposure_step as u8);
buf.push(exposure_numerator as u8);
buf
}
#[test]
fn aebr_round_trip_reads_step_then_numerator() {
let a = parse_aebr(&aebr_body(3, 5)).unwrap();
assert_eq!(a.exposure_step, 3);
assert_eq!(a.exposure_numerator, 5);
}
#[test]
fn aebr_defined_step_enumeration() {
for s in [
Aebr::STEP_FULL,
Aebr::STEP_HALF,
Aebr::STEP_THIRD,
Aebr::STEP_QUARTER,
] {
let a = parse_aebr(&aebr_body(s, 1)).unwrap();
assert!(a.is_defined_step(), "step {s} should be defined");
}
for s in [-1, 0, 5, 7, i8::MIN, i8::MAX] {
let a = parse_aebr(&aebr_body(s, 1)).unwrap();
assert!(!a.is_defined_step(), "step {s} must NOT be defined");
}
}
#[test]
fn aebr_exposure_stops_matches_spec_ratio() {
let a = parse_aebr(&aebr_body(1, -2)).unwrap();
assert_eq!(a.exposure_stops(), Some(-2.0));
let a = parse_aebr(&aebr_body(2, 3)).unwrap();
assert_eq!(a.exposure_stops(), Some(1.5));
let a = parse_aebr(&aebr_body(3, 4)).unwrap();
let v = a.exposure_stops().unwrap();
assert!((v - 4.0 / 3.0).abs() < 1e-12, "got {v}");
let a = parse_aebr(&aebr_body(4, -3)).unwrap();
assert_eq!(a.exposure_stops(), Some(-0.75));
let a = parse_aebr(&aebr_body(0, 7)).unwrap();
assert_eq!(a.exposure_stops(), None);
}
#[test]
fn aebr_signed_byte_reinterpretation() {
let a = parse_aebr(&aebr_body(1, -1)).unwrap();
assert_eq!(a.exposure_numerator, -1);
assert_eq!(a.exposure_stops(), Some(-1.0));
let a = parse_aebr(&aebr_body(-1, 5)).unwrap();
assert_eq!(a.exposure_step, -1);
assert!(!a.is_defined_step());
assert_eq!(a.exposure_stops(), Some(-5.0));
let a = parse_aebr(&aebr_body(i8::MIN, i8::MAX)).unwrap();
assert_eq!(a.exposure_step, i8::MIN);
assert_eq!(a.exposure_numerator, i8::MAX);
}
#[test]
fn aebr_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&[0, 0]);
assert!(parse_aebr(&buf).is_err());
}
#[test]
fn aebr_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_aebr(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.push(2);
assert!(parse_aebr(&buf).is_err());
}
#[test]
fn aebr_tolerates_trailing_bytes() {
let mut body = aebr_body(2, 1);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let a = parse_aebr(&body).unwrap();
assert_eq!(a.exposure_step, 2);
assert_eq!(a.exposure_numerator, 1);
}
#[test]
fn aebr_dispatched_through_parse_ipco() {
let body = aebr_body(3, -2);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"aebr");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Aebr(a) => {
assert_eq!(a.exposure_step, 3);
assert_eq!(a.exposure_numerator, -2);
}
other => panic!("expected Aebr, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"aebr");
}
#[test]
fn aebr_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Aebr(Aebr::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn aebr_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Aebr(Aebr {
exposure_step: 2,
exposure_numerator: -1,
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"aebr") {
Some(Property::Aebr(a)) => {
assert_eq!(a.exposure_step, 2);
assert_eq!(a.exposure_numerator, -1);
assert_eq!(a.exposure_stops(), Some(-0.5));
}
other => panic!("expected Aebr, got {other:?}"),
}
assert!(m.property_for(99, b"aebr").is_none());
}
fn wbbr_body(blue_amber: u16, green_magenta: i8) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&blue_amber.to_be_bytes());
buf.push(green_magenta as u8);
buf
}
#[test]
fn wbbr_round_trip_reads_blue_amber_then_green_magenta() {
let w = parse_wbbr(&wbbr_body(5600, 7)).unwrap();
assert_eq!(w.blue_amber, 5600);
assert_eq!(w.green_magenta, 7);
}
#[test]
fn wbbr_blue_amber_is_big_endian() {
let w = parse_wbbr(&wbbr_body(0x15B0, 0)).unwrap();
assert_eq!(w.blue_amber, 0x15B0);
let w = parse_wbbr(&wbbr_body(u16::MAX, 0)).unwrap();
assert_eq!(w.blue_amber, u16::MAX);
let w = parse_wbbr(&wbbr_body(0, 0)).unwrap();
assert_eq!(w.blue_amber, 0);
}
#[test]
fn wbbr_signed_green_magenta_reinterpretation() {
let w = parse_wbbr(&wbbr_body(5600, -1)).unwrap();
assert_eq!(w.green_magenta, -1);
let w = parse_wbbr(&wbbr_body(5600, i8::MIN)).unwrap();
assert_eq!(w.green_magenta, i8::MIN);
let w = parse_wbbr(&wbbr_body(5600, i8::MAX)).unwrap();
assert_eq!(w.green_magenta, i8::MAX);
}
#[test]
fn wbbr_green_magenta_duv_projection() {
let w = parse_wbbr(&wbbr_body(5600, -50)).unwrap();
assert_eq!(w.green_magenta_duv(), -0.5);
let w = parse_wbbr(&wbbr_body(5600, 50)).unwrap();
assert_eq!(w.green_magenta_duv(), 0.5);
let w = parse_wbbr(&wbbr_body(5600, 0)).unwrap();
assert_eq!(w.green_magenta_duv(), 0.0);
let w = parse_wbbr(&wbbr_body(5600, i8::MIN)).unwrap();
assert!((w.green_magenta_duv() - (-1.28)).abs() < 1e-12);
}
#[test]
fn wbbr_green_magenta_neutral_predicate() {
let w = parse_wbbr(&wbbr_body(5600, 0)).unwrap();
assert!(w.is_green_magenta_neutral());
let w = parse_wbbr(&wbbr_body(2700, 0)).unwrap();
assert!(w.is_green_magenta_neutral());
let w = parse_wbbr(&wbbr_body(0, 0)).unwrap();
assert!(w.is_green_magenta_neutral());
for gm in [-1, 1, -50, 50, i8::MIN, i8::MAX] {
let w = parse_wbbr(&wbbr_body(5600, gm)).unwrap();
assert!(
!w.is_green_magenta_neutral(),
"green_magenta {gm} must NOT be neutral"
);
}
assert_eq!(Wbbr::NEUTRAL_GREEN_MAGENTA, 0);
}
#[test]
fn wbbr_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&[0, 0, 0]);
assert!(parse_wbbr(&buf).is_err());
}
#[test]
fn wbbr_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_wbbr(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0x15, 0xB0]);
assert!(parse_wbbr(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.push(0x15);
assert!(parse_wbbr(&buf).is_err());
}
#[test]
fn wbbr_tolerates_trailing_bytes() {
let mut body = wbbr_body(5600, -3);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let w = parse_wbbr(&body).unwrap();
assert_eq!(w.blue_amber, 5600);
assert_eq!(w.green_magenta, -3);
}
#[test]
fn wbbr_dispatched_through_parse_ipco() {
let body = wbbr_body(6500, -7);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"wbbr");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Wbbr(w) => {
assert_eq!(w.blue_amber, 6500);
assert_eq!(w.green_magenta, -7);
}
other => panic!("expected Wbbr, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"wbbr");
}
#[test]
fn wbbr_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Wbbr(Wbbr::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn wbbr_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Wbbr(Wbbr {
blue_amber: 4800,
green_magenta: 25,
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"wbbr") {
Some(Property::Wbbr(w)) => {
assert_eq!(w.blue_amber, 4800);
assert_eq!(w.green_magenta, 25);
assert_eq!(w.green_magenta_duv(), 0.25);
assert!(!w.is_green_magenta_neutral());
}
other => panic!("expected Wbbr, got {other:?}"),
}
assert!(m.property_for(99, b"wbbr").is_none());
}
fn fobr_body(num: u16, den: u16) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&num.to_be_bytes());
buf.extend_from_slice(&den.to_be_bytes());
buf
}
#[test]
fn fobr_round_trip_reads_numerator_then_denominator() {
let f = parse_fobr(&fobr_body(17, 10)).unwrap();
assert_eq!(f.focus_distance_numerator, 17);
assert_eq!(f.focus_distance_denominator, 10);
}
#[test]
fn fobr_fields_are_big_endian() {
let f = parse_fobr(&fobr_body(0x0125, 0x0008)).unwrap();
assert_eq!(f.focus_distance_numerator, 0x0125);
assert_eq!(f.focus_distance_denominator, 0x0008);
let f = parse_fobr(&fobr_body(u16::MAX, u16::MAX)).unwrap();
assert_eq!(f.focus_distance_numerator, u16::MAX);
assert_eq!(f.focus_distance_denominator, u16::MAX);
let f = parse_fobr(&fobr_body(0, 0)).unwrap();
assert_eq!(f.focus_distance_numerator, 0);
assert_eq!(f.focus_distance_denominator, 0);
}
#[test]
fn fobr_focus_distance_metres_projection() {
let f = parse_fobr(&fobr_body(17, 10)).unwrap();
assert_eq!(f.focus_distance_metres(), Some(1.7));
let f = parse_fobr(&fobr_body(1, 1)).unwrap();
assert_eq!(f.focus_distance_metres(), Some(1.0));
let f = parse_fobr(&fobr_body(5, 100)).unwrap();
assert_eq!(f.focus_distance_metres(), Some(0.05));
let f = parse_fobr(&fobr_body(u16::MAX, 1)).unwrap();
assert_eq!(f.focus_distance_metres(), Some(f64::from(u16::MAX)));
let f = parse_fobr(&fobr_body(0, 0)).unwrap();
assert_eq!(f.focus_distance_metres(), None);
let f = parse_fobr(&fobr_body(42, 0)).unwrap();
assert_eq!(f.focus_distance_metres(), None);
}
#[test]
fn fobr_is_focus_at_infinity_predicate() {
let f = parse_fobr(&fobr_body(0, 0)).unwrap();
assert!(f.is_focus_at_infinity());
let f = parse_fobr(&fobr_body(42, 0)).unwrap();
assert!(f.is_focus_at_infinity());
for den in [1u16, 10, 100, 1000, u16::MAX] {
let f = parse_fobr(&fobr_body(17, den)).unwrap();
assert!(
!f.is_focus_at_infinity(),
"denominator {den} must NOT be infinity"
);
}
assert_eq!(Fobr::INFINITY_DENOMINATOR, 0);
}
#[test]
fn fobr_well_formed_infinity_sentinel_predicate() {
let f = parse_fobr(&fobr_body(0, 0)).unwrap();
assert!(f.has_well_formed_infinity_sentinel());
let f = parse_fobr(&fobr_body(1, 0)).unwrap();
assert!(!f.has_well_formed_infinity_sentinel());
let f = parse_fobr(&fobr_body(u16::MAX, 0)).unwrap();
assert!(!f.has_well_formed_infinity_sentinel());
let f = parse_fobr(&fobr_body(0, 1)).unwrap();
assert!(!f.has_well_formed_infinity_sentinel());
assert_eq!(f.focus_distance_metres(), Some(0.0));
let f = parse_fobr(&fobr_body(17, 10)).unwrap();
assert!(!f.has_well_formed_infinity_sentinel());
}
#[test]
fn fobr_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&[0, 0, 0, 0]);
assert!(parse_fobr(&buf).is_err());
}
#[test]
fn fobr_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_fobr(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0x00, 0x11]);
assert!(parse_fobr(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0x00, 0x11, 0x00]);
assert!(parse_fobr(&buf).is_err());
}
#[test]
fn fobr_tolerates_trailing_bytes() {
let mut body = fobr_body(17, 10);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let f = parse_fobr(&body).unwrap();
assert_eq!(f.focus_distance_numerator, 17);
assert_eq!(f.focus_distance_denominator, 10);
assert_eq!(f.focus_distance_metres(), Some(1.7));
}
#[test]
fn fobr_dispatched_through_parse_ipco() {
let body = fobr_body(7, 2);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"fobr");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Fobr(f) => {
assert_eq!(f.focus_distance_numerator, 7);
assert_eq!(f.focus_distance_denominator, 2);
assert_eq!(f.focus_distance_metres(), Some(3.5));
}
other => panic!("expected Fobr, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"fobr");
}
#[test]
fn fobr_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Fobr(Fobr::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn fobr_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Fobr(Fobr {
focus_distance_numerator: 17,
focus_distance_denominator: 10,
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"fobr") {
Some(Property::Fobr(f)) => {
assert_eq!(f.focus_distance_numerator, 17);
assert_eq!(f.focus_distance_denominator, 10);
assert_eq!(f.focus_distance_metres(), Some(1.7));
assert!(!f.is_focus_at_infinity());
}
other => panic!("expected Fobr, got {other:?}"),
}
assert!(m.property_for(99, b"fobr").is_none());
let m = Meta {
properties: vec![Property::Fobr(Fobr {
focus_distance_numerator: 0,
focus_distance_denominator: 0,
})],
associations: vec![ItemPropertyAssociation {
item_id: 5,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(5, b"fobr") {
Some(Property::Fobr(f)) => {
assert!(f.is_focus_at_infinity());
assert!(f.has_well_formed_infinity_sentinel());
assert_eq!(f.focus_distance_metres(), None);
}
other => panic!("expected Fobr, got {other:?}"),
}
}
fn afbr_body(num: i8, den: i8) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.push(num as u8);
buf.push(den as u8);
buf
}
#[test]
fn afbr_round_trip_reads_numerator_then_denominator() {
let a = parse_afbr(&afbr_body(1, 2)).unwrap();
assert_eq!(a.flash_exposure_numerator, 1);
assert_eq!(a.flash_exposure_denominator, 2);
}
#[test]
fn afbr_fields_are_signed() {
let a = parse_afbr(&afbr_body(-1, 2)).unwrap();
assert_eq!(a.flash_exposure_numerator, -1);
assert_eq!(a.flash_exposure_denominator, 2);
let a = parse_afbr(&afbr_body(1, -2)).unwrap();
assert_eq!(a.flash_exposure_numerator, 1);
assert_eq!(a.flash_exposure_denominator, -2);
let a = parse_afbr(&afbr_body(-1, -2)).unwrap();
assert_eq!(a.flash_exposure_numerator, -1);
assert_eq!(a.flash_exposure_denominator, -2);
let a = parse_afbr(&afbr_body(i8::MIN, i8::MAX)).unwrap();
assert_eq!(a.flash_exposure_numerator, i8::MIN);
assert_eq!(a.flash_exposure_denominator, i8::MAX);
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0xFF, 0x02]);
let a = parse_afbr(&buf).unwrap();
assert_eq!(a.flash_exposure_numerator, -1);
assert_eq!(a.flash_exposure_denominator, 2);
}
#[test]
fn afbr_flash_exposure_stops_projection() {
let a = parse_afbr(&afbr_body(1, 2)).unwrap();
assert_eq!(a.flash_exposure_stops(), Some(0.5));
let a = parse_afbr(&afbr_body(-1, 2)).unwrap();
assert_eq!(a.flash_exposure_stops(), Some(-0.5));
let a = parse_afbr(&afbr_body(1, 1)).unwrap();
assert_eq!(a.flash_exposure_stops(), Some(1.0));
let a = parse_afbr(&afbr_body(-2, 3)).unwrap();
let v = a.flash_exposure_stops().unwrap();
assert!((v - (-2.0 / 3.0)).abs() < 1e-12, "got {v}");
let a = parse_afbr(&afbr_body(i8::MIN, -1)).unwrap();
assert_eq!(a.flash_exposure_stops(), Some(128.0));
let a = parse_afbr(&afbr_body(1, 0)).unwrap();
assert_eq!(a.flash_exposure_stops(), None);
let a = parse_afbr(&afbr_body(0, 1)).unwrap();
assert_eq!(a.flash_exposure_stops(), Some(0.0));
let a = parse_afbr(&afbr_body(0, 0)).unwrap();
assert_eq!(a.flash_exposure_stops(), None);
}
#[test]
fn afbr_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&[0, 0]);
assert!(parse_afbr(&buf).is_err());
}
#[test]
fn afbr_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_afbr(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.push(0x01);
assert!(parse_afbr(&buf).is_err());
}
#[test]
fn afbr_tolerates_trailing_bytes() {
let mut body = afbr_body(1, 2);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let a = parse_afbr(&body).unwrap();
assert_eq!(a.flash_exposure_numerator, 1);
assert_eq!(a.flash_exposure_denominator, 2);
assert_eq!(a.flash_exposure_stops(), Some(0.5));
}
#[test]
fn afbr_dispatched_through_parse_ipco() {
let body = afbr_body(-3, 4);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"afbr");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Afbr(a) => {
assert_eq!(a.flash_exposure_numerator, -3);
assert_eq!(a.flash_exposure_denominator, 4);
assert_eq!(a.flash_exposure_stops(), Some(-0.75));
}
other => panic!("expected Afbr, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"afbr");
}
#[test]
fn afbr_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Afbr(Afbr::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn afbr_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Afbr(Afbr {
flash_exposure_numerator: 1,
flash_exposure_denominator: 2,
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"afbr") {
Some(Property::Afbr(a)) => {
assert_eq!(a.flash_exposure_numerator, 1);
assert_eq!(a.flash_exposure_denominator, 2);
assert_eq!(a.flash_exposure_stops(), Some(0.5));
}
other => panic!("expected Afbr, got {other:?}"),
}
assert!(m.property_for(99, b"afbr").is_none());
let m = Meta {
properties: vec![Property::Afbr(Afbr {
flash_exposure_numerator: -3,
flash_exposure_denominator: 4,
})],
associations: vec![ItemPropertyAssociation {
item_id: 5,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(5, b"afbr") {
Some(Property::Afbr(a)) => {
assert_eq!(a.flash_exposure_numerator, -3);
assert_eq!(a.flash_exposure_denominator, 4);
assert_eq!(a.flash_exposure_stops(), Some(-0.75));
}
other => panic!("expected Afbr, got {other:?}"),
}
let m = Meta {
properties: vec![Property::Afbr(Afbr {
flash_exposure_numerator: 1,
flash_exposure_denominator: 0,
})],
associations: vec![ItemPropertyAssociation {
item_id: 7,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(7, b"afbr") {
Some(Property::Afbr(a)) => {
assert_eq!(a.flash_exposure_numerator, 1);
assert_eq!(a.flash_exposure_denominator, 0);
assert_eq!(a.flash_exposure_stops(), None);
}
other => panic!("expected Afbr, got {other:?}"),
}
}
fn dobr_body(num: i8, den: i8) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.push(num as u8);
buf.push(den as u8);
buf
}
#[test]
fn dobr_round_trip_reads_numerator_then_denominator() {
let d = parse_dobr(&dobr_body(1, 2)).unwrap();
assert_eq!(d.f_stop_numerator, 1);
assert_eq!(d.f_stop_denominator, 2);
}
#[test]
fn dobr_fields_are_signed() {
let d = parse_dobr(&dobr_body(-1, 2)).unwrap();
assert_eq!(d.f_stop_numerator, -1);
assert_eq!(d.f_stop_denominator, 2);
let d = parse_dobr(&dobr_body(1, -2)).unwrap();
assert_eq!(d.f_stop_numerator, 1);
assert_eq!(d.f_stop_denominator, -2);
let d = parse_dobr(&dobr_body(-1, -2)).unwrap();
assert_eq!(d.f_stop_numerator, -1);
assert_eq!(d.f_stop_denominator, -2);
let d = parse_dobr(&dobr_body(i8::MIN, i8::MAX)).unwrap();
assert_eq!(d.f_stop_numerator, i8::MIN);
assert_eq!(d.f_stop_denominator, i8::MAX);
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0xFF, 0x02]);
let d = parse_dobr(&buf).unwrap();
assert_eq!(d.f_stop_numerator, -1);
assert_eq!(d.f_stop_denominator, 2);
}
#[test]
fn dobr_aperture_stops_projection() {
let d = parse_dobr(&dobr_body(1, 2)).unwrap();
assert_eq!(d.aperture_stops(), Some(0.5));
let d = parse_dobr(&dobr_body(-1, 2)).unwrap();
assert_eq!(d.aperture_stops(), Some(-0.5));
let d = parse_dobr(&dobr_body(1, 1)).unwrap();
assert_eq!(d.aperture_stops(), Some(1.0));
let d = parse_dobr(&dobr_body(-2, 3)).unwrap();
let v = d.aperture_stops().unwrap();
assert!((v - (-2.0 / 3.0)).abs() < 1e-12, "got {v}");
let d = parse_dobr(&dobr_body(i8::MIN, -1)).unwrap();
assert_eq!(d.aperture_stops(), Some(128.0));
let d = parse_dobr(&dobr_body(1, 0)).unwrap();
assert_eq!(d.aperture_stops(), None);
let d = parse_dobr(&dobr_body(0, 1)).unwrap();
assert_eq!(d.aperture_stops(), Some(0.0));
let d = parse_dobr(&dobr_body(0, 0)).unwrap();
assert_eq!(d.aperture_stops(), None);
}
#[test]
fn dobr_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&[0, 0]);
assert!(parse_dobr(&buf).is_err());
}
#[test]
fn dobr_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_dobr(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.push(0x01);
assert!(parse_dobr(&buf).is_err());
}
#[test]
fn dobr_tolerates_trailing_bytes() {
let mut body = dobr_body(1, 2);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let d = parse_dobr(&body).unwrap();
assert_eq!(d.f_stop_numerator, 1);
assert_eq!(d.f_stop_denominator, 2);
assert_eq!(d.aperture_stops(), Some(0.5));
}
#[test]
fn dobr_dispatched_through_parse_ipco() {
let body = dobr_body(-3, 4);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"dobr");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Dobr(d) => {
assert_eq!(d.f_stop_numerator, -3);
assert_eq!(d.f_stop_denominator, 4);
assert_eq!(d.aperture_stops(), Some(-0.75));
}
other => panic!("expected Dobr, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"dobr");
}
#[test]
fn dobr_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Dobr(Dobr::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn dobr_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Dobr(Dobr {
f_stop_numerator: 1,
f_stop_denominator: 2,
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"dobr") {
Some(Property::Dobr(d)) => {
assert_eq!(d.f_stop_numerator, 1);
assert_eq!(d.f_stop_denominator, 2);
assert_eq!(d.aperture_stops(), Some(0.5));
}
other => panic!("expected Dobr, got {other:?}"),
}
assert!(m.property_for(99, b"dobr").is_none());
let m = Meta {
properties: vec![Property::Dobr(Dobr {
f_stop_numerator: -3,
f_stop_denominator: 4,
})],
associations: vec![ItemPropertyAssociation {
item_id: 5,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(5, b"dobr") {
Some(Property::Dobr(d)) => {
assert_eq!(d.f_stop_numerator, -3);
assert_eq!(d.f_stop_denominator, 4);
assert_eq!(d.aperture_stops(), Some(-0.75));
}
other => panic!("expected Dobr, got {other:?}"),
}
let m = Meta {
properties: vec![Property::Dobr(Dobr {
f_stop_numerator: 1,
f_stop_denominator: 0,
})],
associations: vec![ItemPropertyAssociation {
item_id: 7,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(7, b"dobr") {
Some(Property::Dobr(d)) => {
assert_eq!(d.f_stop_numerator, 1);
assert_eq!(d.f_stop_denominator, 0);
assert_eq!(d.aperture_stops(), None);
}
other => panic!("expected Dobr, got {other:?}"),
}
let m = Meta {
properties: vec![
Property::Dobr(Dobr {
f_stop_numerator: 1,
f_stop_denominator: 2,
}),
Property::Afbr(Afbr {
flash_exposure_numerator: -1,
flash_exposure_denominator: 2,
}),
],
associations: vec![ItemPropertyAssociation {
item_id: 3,
entries: vec![
PropertyAssociation {
index: 0,
essential: false,
},
PropertyAssociation {
index: 1,
essential: false,
},
],
}],
..Meta::default()
};
match m.property_for(3, b"dobr") {
Some(Property::Dobr(d)) => assert_eq!(d.aperture_stops(), Some(0.5)),
other => panic!("expected Dobr, got {other:?}"),
}
match m.property_for(3, b"afbr") {
Some(Property::Afbr(a)) => assert_eq!(a.flash_exposure_stops(), Some(-0.5)),
other => panic!("expected Afbr, got {other:?}"),
}
}
fn cmin_body(flags: u32, fx: i32, cx: i32, cy: i32, tail: Option<(i32, i32)>) -> Vec<u8> {
let mut buf = vec![0u8]; buf.extend_from_slice(&flags.to_be_bytes()[1..]); buf.extend_from_slice(&(fx as u32).to_be_bytes());
buf.extend_from_slice(&(cx as u32).to_be_bytes());
buf.extend_from_slice(&(cy as u32).to_be_bytes());
if let Some((fy, skew)) = tail {
buf.extend_from_slice(&(fy as u32).to_be_bytes());
buf.extend_from_slice(&(skew as u32).to_be_bytes());
}
buf
}
#[test]
fn cmin_simplified_reads_three_mandatory_fields() {
let c = parse_cmin(&cmin_body(0, 100, 20, 30, None)).unwrap();
assert_eq!(c.focal_length_x, 100);
assert_eq!(c.principal_point_x, 20);
assert_eq!(c.principal_point_y, 30);
assert_eq!(c.focal_length_y, None);
assert_eq!(c.skew_factor, None);
assert!(!c.has_skew());
}
#[test]
fn cmin_full_reads_focal_y_then_skew() {
let c = parse_cmin(&cmin_body(1, 100, 20, 30, Some((110, 5)))).unwrap();
assert_eq!(c.focal_length_x, 100);
assert_eq!(c.principal_point_x, 20);
assert_eq!(c.principal_point_y, 30);
assert_eq!(c.focal_length_y, Some(110));
assert_eq!(c.skew_factor, Some(5));
assert!(c.has_skew());
}
#[test]
fn cmin_fields_are_signed() {
let c = parse_cmin(&cmin_body(1, -1, -20, -30, Some((-110, -5)))).unwrap();
assert_eq!(c.focal_length_x, -1);
assert_eq!(c.principal_point_x, -20);
assert_eq!(c.principal_point_y, -30);
assert_eq!(c.focal_length_y, Some(-110));
assert_eq!(c.skew_factor, Some(-5));
let c = parse_cmin(&cmin_body(
1,
i32::MIN,
i32::MAX,
0,
Some((i32::MIN, i32::MAX)),
))
.unwrap();
assert_eq!(c.focal_length_x, i32::MIN);
assert_eq!(c.principal_point_x, i32::MAX);
assert_eq!(c.focal_length_y, Some(i32::MIN));
assert_eq!(c.skew_factor, Some(i32::MAX));
}
#[test]
fn cmin_denominators_decoded_from_flags() {
let flags = (8u32 << 8) | (4u32 << 16) | 1;
let c = parse_cmin(&cmin_body(flags, 0, 0, 0, Some((0, 0)))).unwrap();
assert_eq!(c.denominator_shift(), 8);
assert_eq!(c.skew_denominator_shift(), 4);
assert_eq!(c.denominator(), 256);
assert_eq!(c.skew_denominator(), 16);
let c = parse_cmin(&cmin_body(0, 0, 0, 0, None)).unwrap();
assert_eq!(c.denominator(), 1);
assert_eq!(c.skew_denominator(), 1);
let flags = (31u32 << 8) | (31u32 << 16);
let c = parse_cmin(&cmin_body(flags, 0, 0, 0, None)).unwrap();
assert_eq!(c.denominator(), 1u64 << 31);
assert_eq!(c.skew_denominator(), 1u64 << 31);
}
#[test]
fn cmin_matrix_entry_projections() {
let flags = (8u32 << 8) | 1;
let c = parse_cmin(&cmin_body(flags, 256, 128, 96, Some((256, 0)))).unwrap();
assert!((c.focal_length_x_value(640) - 640.0).abs() < 1e-9);
assert!((c.focal_length_y_value(640, 480) - 480.0).abs() < 1e-9);
assert!((c.principal_point_x_value(640) - 320.0).abs() < 1e-9);
assert!((c.principal_point_y_value(480) - 180.0).abs() < 1e-9);
assert!((c.skew_value() - 0.0).abs() < 1e-12);
}
#[test]
fn cmin_simplified_infers_fy_equals_fx_and_zero_skew() {
let c = parse_cmin(&cmin_body(0, 100, 0, 0, None)).unwrap();
let fx = c.focal_length_x_value(640);
assert!((c.focal_length_y_value(640, 480) - fx).abs() < 1e-9);
assert_eq!(c.skew_value(), 0.0);
}
#[test]
fn cmin_skew_value_uses_skew_denominator() {
let flags = (2u32 << 16) | 1;
let c = parse_cmin(&cmin_body(flags, 0, 0, 0, Some((0, 6)))).unwrap();
assert!((c.skew_value() - 1.5).abs() < 1e-12);
let c = parse_cmin(&cmin_body(flags, 0, 0, 0, Some((0, -6)))).unwrap();
assert!((c.skew_value() - (-1.5)).abs() < 1e-12);
}
#[test]
fn cmin_rejects_unknown_version() {
let mut buf = cmin_body(0, 1, 2, 3, None);
buf[0] = 1;
assert!(parse_cmin(&buf).is_err());
}
#[test]
fn cmin_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_cmin(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&[0u8; 8]);
assert!(parse_cmin(&buf).is_err());
let buf = cmin_body(0, 1, 2, 3, None);
let mut buf_full = buf.clone();
buf_full[3] = 1; assert!(parse_cmin(&buf_full).is_err());
let mut buf = cmin_body(1, 1, 2, 3, Some((4, 5)));
buf.truncate(buf.len() - 4); assert!(parse_cmin(&buf).is_err());
}
#[test]
fn cmin_tolerates_trailing_bytes() {
let mut body = cmin_body(1, 100, 20, 30, Some((110, 5)));
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let c = parse_cmin(&body).unwrap();
assert_eq!(c.focal_length_x, 100);
assert_eq!(c.skew_factor, Some(5));
}
#[test]
fn cmin_dispatched_through_parse_ipco() {
let body = cmin_body(1, 256, 128, 96, Some((256, 0)));
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"cmin");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Cmin(c) => {
assert_eq!(c.focal_length_x, 256);
assert_eq!(c.focal_length_y, Some(256));
assert_eq!(c.skew_factor, Some(0));
}
other => panic!("expected Cmin, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"cmin");
}
#[test]
fn cmin_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Cmin(Cmin::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn cmin_lookup_via_property_for() {
let flags = (8u32 << 8) | 1;
let m = Meta {
properties: vec![Property::Cmin(Cmin {
flags,
focal_length_x: 256,
principal_point_x: 128,
principal_point_y: 96,
focal_length_y: Some(256),
skew_factor: Some(0),
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"cmin") {
Some(Property::Cmin(c)) => {
assert_eq!(c.denominator(), 256);
assert!((c.focal_length_x_value(640) - 640.0).abs() < 1e-9);
}
other => panic!("expected Cmin, got {other:?}"),
}
assert!(m.property_for(99, b"cmin").is_none());
}
fn cmex_body(
version: u8,
flags: u32,
pos: [Option<i32>; 3],
quat: Option<[i32; 3]>,
id: Option<u32>,
) -> Vec<u8> {
let mut buf = vec![version];
buf.extend_from_slice(&flags.to_be_bytes()[1..]); for p in pos.into_iter().flatten() {
buf.extend_from_slice(&(p as u32).to_be_bytes());
}
if let Some(q) = quat {
let large = flags & Cmex::FLAG_ROT_LARGE_FIELD_SIZE != 0;
for c in q {
if large {
buf.extend_from_slice(&(c as u32).to_be_bytes());
} else {
buf.extend_from_slice(&(c as i16).to_be_bytes());
}
}
}
if let Some(i) = id {
buf.extend_from_slice(&i.to_be_bytes());
}
buf
}
#[test]
fn cmex_position_x_only() {
let flags = Cmex::FLAG_POS_X_PRESENT;
let c = parse_cmex(&cmex_body(0, flags, [Some(65_000), None, None], None, None)).unwrap();
assert_eq!(c.pos_x, Some(65_000));
assert_eq!(c.pos_y, None);
assert_eq!(c.pos_z, None);
assert_eq!(c.position(), [65_000, 0, 0]);
assert!(c.pos_x_present());
assert!(!c.orientation_present());
assert_eq!(c.quaternion(), None);
}
#[test]
fn cmex_positions_signed_in_order() {
let flags = Cmex::FLAG_POS_X_PRESENT | Cmex::FLAG_POS_Y_PRESENT | Cmex::FLAG_POS_Z_PRESENT;
let c = parse_cmex(&cmex_body(
0,
flags,
[Some(-1), Some(2), Some(i32::MIN)],
None,
None,
))
.unwrap();
assert_eq!(c.position(), [-1, 2, i32::MIN]);
}
#[test]
fn cmex_quaternion_16bit_identity() {
let flags = Cmex::FLAG_ORIENTATION_PRESENT;
let c = parse_cmex(&cmex_body(0, flags, [None; 3], Some([0, 0, 0]), None)).unwrap();
assert_eq!(c.orientation_precision(), 0);
let q = c.quaternion().unwrap();
assert!((q[0]).abs() < 1e-12);
assert!((q[3] - 1.0).abs() < 1e-12);
}
#[test]
fn cmex_quaternion_rotation_matrix_z_90deg() {
let flags = Cmex::FLAG_ORIENTATION_PRESENT;
let qz = 11585; let c = parse_cmex(&cmex_body(0, flags, [None; 3], Some([0, 0, qz]), None)).unwrap();
let q = c.quaternion().unwrap();
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
assert!((q[2] - inv_sqrt2).abs() < 1e-3);
assert!((q[3] - inv_sqrt2).abs() < 1e-3);
let r = c.rotation_matrix().unwrap();
assert!((r[0][0]).abs() < 1e-3);
assert!((r[0][1] - (-1.0)).abs() < 1e-3);
assert!((r[1][0] - 1.0).abs() < 1e-3);
assert!((r[2][2] - 1.0).abs() < 1e-3);
}
#[test]
fn cmex_quaternion_32bit_large_field() {
let flags = Cmex::FLAG_ORIENTATION_PRESENT | Cmex::FLAG_ROT_LARGE_FIELD_SIZE;
let qz = 536_870_912;
let c = parse_cmex(&cmex_body(0, flags, [None; 3], Some([0, 0, qz]), None)).unwrap();
assert_eq!(c.orientation_precision(), 16);
assert!(c.rot_large_field_size());
let q = c.quaternion().unwrap();
assert!((q[2] - 0.5).abs() < 1e-9);
assert!((q[3] - 0.75_f64.sqrt()).abs() < 1e-9);
}
#[test]
fn cmex_all_fields_with_id() {
let flags = Cmex::FLAG_POS_X_PRESENT
| Cmex::FLAG_POS_Y_PRESENT
| Cmex::FLAG_POS_Z_PRESENT
| Cmex::FLAG_ORIENTATION_PRESENT
| Cmex::FLAG_ID_PRESENT;
let c = parse_cmex(&cmex_body(
0,
flags,
[Some(10), Some(20), Some(30)],
Some([0, 0, 0]),
Some(7),
))
.unwrap();
assert_eq!(c.position(), [10, 20, 30]);
assert_eq!(c.id, Some(7));
assert_eq!(c.coordinate_system_id(), 7);
assert!((c.quaternion().unwrap()[3] - 1.0).abs() < 1e-12);
}
#[test]
fn cmex_id_after_positions_no_orientation() {
let flags = Cmex::FLAG_POS_X_PRESENT | Cmex::FLAG_ID_PRESENT;
let c = parse_cmex(&cmex_body(0, flags, [Some(42), None, None], None, Some(99))).unwrap();
assert_eq!(c.pos_x, Some(42));
assert_eq!(c.id, Some(99));
}
#[test]
fn cmex_quaternion_qw_non_negative() {
let flags = Cmex::FLAG_ORIENTATION_PRESENT;
let c = parse_cmex(&cmex_body(
0,
flags,
[None; 3],
Some([16_384, 16_384, 16_384]),
None,
))
.unwrap();
let q = c.quaternion().unwrap();
assert!(q[3].is_finite());
assert_eq!(q[3], 0.0);
}
#[test]
fn cmex_v1_orientation_unsupported() {
let flags = Cmex::FLAG_ORIENTATION_PRESENT;
let body = cmex_body(1, flags, [None; 3], Some([0, 0, 0]), None);
assert!(matches!(parse_cmex(&body), Err(Error::Unsupported(_))));
}
#[test]
fn cmex_v1_positions_only_ok() {
let flags = Cmex::FLAG_POS_X_PRESENT | Cmex::FLAG_ID_PRESENT;
let c = parse_cmex(&cmex_body(1, flags, [Some(5), None, None], None, Some(3))).unwrap();
assert_eq!(c.version, 1);
assert_eq!(c.pos_x, Some(5));
assert_eq!(c.id, Some(3));
assert_eq!(c.quaternion(), None);
}
#[test]
fn cmex_rejects_unknown_version() {
let body = cmex_body(
0,
Cmex::FLAG_POS_X_PRESENT,
[Some(1), None, None],
None,
None,
);
let mut buf = body;
buf[0] = 2;
assert!(parse_cmex(&buf).is_err());
}
#[test]
fn cmex_rejects_truncated_body() {
let buf = vec![0u8, 0, 0, Cmex::FLAG_POS_X_PRESENT as u8];
assert!(parse_cmex(&buf).is_err());
let mut buf = vec![0u8, 0, 0, Cmex::FLAG_ORIENTATION_PRESENT as u8];
buf.extend_from_slice(&[0u8; 4]);
assert!(parse_cmex(&buf).is_err());
}
#[test]
fn cmex_tolerates_trailing_bytes() {
let mut body = cmex_body(
0,
Cmex::FLAG_POS_X_PRESENT,
[Some(7), None, None],
None,
None,
);
body.extend_from_slice(&[0xDE, 0xAD]);
let c = parse_cmex(&body).unwrap();
assert_eq!(c.pos_x, Some(7));
}
#[test]
fn cmex_dispatched_through_parse_ipco() {
let flags = Cmex::FLAG_POS_X_PRESENT | Cmex::FLAG_ORIENTATION_PRESENT;
let body = cmex_body(0, flags, [Some(1234), None, None], Some([0, 0, 0]), None);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"cmex");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Cmex(c) => {
assert_eq!(c.pos_x, Some(1234));
assert!((c.quaternion().unwrap()[3] - 1.0).abs() < 1e-12);
}
other => panic!("expected Cmex, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"cmex");
}
#[test]
fn cmex_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Cmex(Cmex::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn cmex_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Cmex(Cmex {
flags: Cmex::FLAG_POS_X_PRESENT | Cmex::FLAG_ID_PRESENT,
version: 0,
pos_x: Some(500),
id: Some(4),
..Cmex::default()
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"cmex") {
Some(Property::Cmex(c)) => {
assert_eq!(c.position(), [500, 0, 0]);
assert_eq!(c.coordinate_system_id(), 4);
}
other => panic!("expected Cmex, got {other:?}"),
}
assert!(m.property_for(99, b"cmex").is_none());
}
fn pano_body_linear(direction: u8) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.push(direction);
buf
}
fn pano_body_grid(direction: u8, rows_minus_one: u8, columns_minus_one: u8) -> Vec<u8> {
let mut buf = pano_body_linear(direction);
buf.push(rows_minus_one);
buf.push(columns_minus_one);
buf
}
#[test]
fn pano_linear_directions_have_no_grid() {
for direction in [
Pano::DIRECTION_LEFT_TO_RIGHT,
Pano::DIRECTION_RIGHT_TO_LEFT,
Pano::DIRECTION_BOTTOM_TO_TOP,
Pano::DIRECTION_TOP_TO_BOTTOM,
] {
let p = parse_pano(&pano_body_linear(direction)).unwrap();
assert_eq!(p.panorama_direction, direction);
assert_eq!(p.grid, None);
assert!(p.is_defined_direction());
assert!(!p.is_grid());
}
}
#[test]
fn pano_grid_directions_carry_shape() {
for direction in [Pano::DIRECTION_GRID_RASTER, Pano::DIRECTION_GRID_CONTINUOUS] {
let p = parse_pano(&pano_body_grid(direction, 1, 2)).unwrap();
assert_eq!(p.panorama_direction, direction);
assert!(p.is_defined_direction());
assert!(p.is_grid());
let g = p.grid.expect("grid direction must carry shape");
assert_eq!(g.rows_minus_one, 1);
assert_eq!(g.columns_minus_one, 2);
assert_eq!(g.rows(), 2);
assert_eq!(g.columns(), 3);
}
}
#[test]
fn pano_grid_dims_widen_past_u8() {
let p = parse_pano(&pano_body_grid(Pano::DIRECTION_GRID_RASTER, 0xFF, 0xFF)).unwrap();
let g = p.grid.unwrap();
assert_eq!(g.rows(), 256);
assert_eq!(g.columns(), 256);
let p = parse_pano(&pano_body_grid(Pano::DIRECTION_GRID_CONTINUOUS, 0, 0)).unwrap();
let g = p.grid.unwrap();
assert_eq!(g.rows(), 1);
assert_eq!(g.columns(), 1);
}
#[test]
fn pano_undefined_direction_is_preserved_not_rejected() {
for direction in [6u8, 7, 0x7F, 0xFF] {
let p = parse_pano(&pano_body_linear(direction)).unwrap();
assert_eq!(p.panorama_direction, direction);
assert_eq!(p.grid, None);
assert!(!p.is_defined_direction());
assert!(!p.is_grid());
}
}
#[test]
fn pano_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.push(Pano::DIRECTION_LEFT_TO_RIGHT);
assert!(parse_pano(&buf).is_err());
}
#[test]
fn pano_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_pano(&buf).is_err());
assert!(parse_pano(&pano_body_linear(Pano::DIRECTION_GRID_RASTER)).is_err());
let mut buf = pano_body_linear(Pano::DIRECTION_GRID_CONTINUOUS);
buf.push(2);
assert!(parse_pano(&buf).is_err());
}
#[test]
fn pano_tolerates_trailing_bytes() {
let mut body = pano_body_linear(Pano::DIRECTION_TOP_TO_BOTTOM);
body.extend_from_slice(&[0xDE, 0xAD]);
let p = parse_pano(&body).unwrap();
assert_eq!(p.panorama_direction, Pano::DIRECTION_TOP_TO_BOTTOM);
assert_eq!(p.grid, None);
let mut body = pano_body_grid(Pano::DIRECTION_GRID_RASTER, 3, 4);
body.extend_from_slice(&[0xBE, 0xEF]);
let p = parse_pano(&body).unwrap();
let g = p.grid.unwrap();
assert_eq!((g.rows(), g.columns()), (4, 5));
}
#[test]
fn pano_dispatched_through_parse_ipco() {
let body = pano_body_grid(Pano::DIRECTION_GRID_CONTINUOUS, 2, 3);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"pano");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Pano(p) => {
assert_eq!(p.panorama_direction, Pano::DIRECTION_GRID_CONTINUOUS);
let g = p.grid.unwrap();
assert_eq!((g.rows(), g.columns()), (3, 4));
}
other => panic!("expected Pano, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"pano");
}
#[test]
fn pano_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Pano(Pano::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn pano_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Pano(Pano {
panorama_direction: Pano::DIRECTION_RIGHT_TO_LEFT,
grid: None,
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"pano") {
Some(Property::Pano(p)) => {
assert_eq!(p.panorama_direction, Pano::DIRECTION_RIGHT_TO_LEFT);
assert!(p.is_defined_direction());
assert!(!p.is_grid());
}
other => panic!("expected Pano, got {other:?}"),
}
assert!(m.property_for(99, b"pano").is_none());
}
fn subs_body(version: u8, flags: u32, samples: &[(u32, u8, u8, u32)]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(version);
buf.extend_from_slice(&[(flags >> 16) as u8, (flags >> 8) as u8, flags as u8]);
buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&(samples.len() as u16).to_be_bytes());
for &(size, prio, disc, csp) in samples {
if version == 1 {
buf.extend_from_slice(&size.to_be_bytes());
} else {
buf.extend_from_slice(&(size as u16).to_be_bytes());
}
buf.push(prio);
buf.push(disc);
buf.extend_from_slice(&csp.to_be_bytes());
}
buf
}
#[test]
fn subs_v0_round_trips_entries() {
let samples = [(0x0102u32, 7u8, 1u8, 0xDEAD_BEEFu32), (0x0003, 0, 0, 0)];
let s = parse_subs(&subs_body(0, 0, &samples)).unwrap();
assert_eq!(s.flags, 0);
assert_eq!(s.entries.len(), 2);
assert_eq!(s.entries[0].subsample_size, 0x0102);
assert_eq!(s.entries[0].subsample_priority, 7);
assert_eq!(s.entries[0].discardable, 1);
assert_eq!(s.entries[0].codec_specific_parameters, 0xDEAD_BEEF);
assert_eq!(s.entries[1].subsample_size, 3);
}
#[test]
fn subs_v1_uses_32bit_size() {
let samples = [(0x0001_0002u32, 0u8, 0u8, 0u32)];
let s = parse_subs(&subs_body(1, 0, &samples)).unwrap();
assert_eq!(s.entries[0].subsample_size, 0x0001_0002);
}
#[test]
fn subs_surfaces_flags() {
let s = parse_subs(&subs_body(0, 0x00AB_CDEF & 0x00FF_FFFF, &[])).unwrap();
assert_eq!(s.flags, 0x00AB_CDEF & 0x00FF_FFFF);
assert!(s.entries.is_empty());
}
#[test]
fn subs_empty_subsample_count_ok() {
let s = parse_subs(&subs_body(1, 0, &[])).unwrap();
assert!(s.entries.is_empty());
}
#[test]
fn subs_rejects_entry_count_not_one() {
let mut buf = vec![0u8, 0, 0, 0];
buf.extend_from_slice(&2u32.to_be_bytes()); assert!(parse_subs(&buf).is_err());
}
#[test]
fn subs_rejects_nonzero_sample_delta() {
let mut buf = vec![0u8, 0, 0, 0];
buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&5u32.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); assert!(parse_subs(&buf).is_err());
}
#[test]
fn subs_rejects_unknown_version() {
let mut buf = vec![2u8, 0, 0, 0]; buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
assert!(parse_subs(&buf).is_err());
}
#[test]
fn subs_rejects_truncated_tuple() {
let mut buf = vec![0u8, 0, 0, 0];
buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&0x0010u16.to_be_bytes()); assert!(parse_subs(&buf).is_err());
}
#[test]
fn subs_tolerates_trailing_bytes() {
let mut body = subs_body(0, 0, &[(4, 0, 0, 0)]);
body.extend_from_slice(&[0xFF, 0xFF]);
let s = parse_subs(&body).unwrap();
assert_eq!(s.entries.len(), 1);
assert_eq!(s.entries[0].subsample_size, 4);
}
#[test]
fn subs_dispatched_through_parse_ipco() {
let body = subs_body(1, 0, &[(0x0000_0009, 3, 0, 0x0000_0001)]);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"subs");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Subs(sub) => {
assert_eq!(sub.entries.len(), 1);
assert_eq!(sub.entries[0].subsample_size, 9);
assert_eq!(sub.entries[0].subsample_priority, 3);
assert_eq!(sub.entries[0].codec_specific_parameters, 1);
}
other => panic!("expected Subs, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"subs");
}
#[test]
fn subs_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Subs(Subs::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn subs_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Subs(Subs {
flags: 1,
entries: vec![SubsEntry {
subsample_size: 12,
subsample_priority: 2,
discardable: 0,
codec_specific_parameters: 0,
}],
})],
associations: vec![ItemPropertyAssociation {
item_id: 4,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(4, b"subs") {
Some(Property::Subs(sub)) => {
assert_eq!(sub.flags, 1);
assert_eq!(sub.entries[0].subsample_size, 12);
}
other => panic!("expected Subs, got {other:?}"),
}
assert!(m.property_for(99, b"subs").is_none());
}
fn tols_body(target_ols_idx: u16) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(&target_ols_idx.to_be_bytes());
buf
}
#[test]
fn tols_round_trip_reads_target_ols_idx() {
for raw in [0u16, 1, 7, 0x0102, u16::MAX] {
let t = parse_tols(&tols_body(raw)).unwrap();
assert_eq!(t.target_ols_idx, raw);
}
}
#[test]
fn tols_target_ols_idx_is_big_endian() {
let t = parse_tols(&tols_body(0x0102)).unwrap();
assert_eq!(t.target_ols_idx, 0x0102);
let mut buf = vec![0u8; 4];
buf.push(0x01);
buf.push(0x02);
assert_eq!(parse_tols(&buf).unwrap().target_ols_idx, 0x0102);
}
#[test]
fn tols_rejects_unknown_version() {
let mut buf = vec![1u8, 0, 0, 0];
buf.extend_from_slice(&3u16.to_be_bytes());
assert!(parse_tols(&buf).is_err());
}
#[test]
fn tols_rejects_truncated_body() {
let buf = vec![0u8; 4];
assert!(parse_tols(&buf).is_err());
let mut buf = vec![0u8; 4];
buf.push(0x12);
assert!(parse_tols(&buf).is_err());
}
#[test]
fn tols_tolerates_trailing_bytes() {
let mut body = tols_body(42);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let t = parse_tols(&body).unwrap();
assert_eq!(t.target_ols_idx, 42);
}
#[test]
fn tols_dispatched_through_parse_ipco() {
let body = tols_body(0xBEEF);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"tols");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Tols(t) => assert_eq!(t.target_ols_idx, 0xBEEF),
other => panic!("expected Tols, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"tols");
}
#[test]
fn tols_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Tols(Tols { target_ols_idx: 2 })],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn tols_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Tols(Tols {
target_ols_idx: 0x0304,
})],
associations: vec![ItemPropertyAssociation {
item_id: 7,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
match m.property_for(7, b"tols") {
Some(Property::Tols(t)) => assert_eq!(t.target_ols_idx, 0x0304),
other => panic!("expected Tols, got {other:?}"),
}
assert!(m.property_for(99, b"tols").is_none());
}
fn prdi_body(version: u8, flags: u32, item_counts: &[u16]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(version);
buf.extend_from_slice(&[(flags >> 16) as u8, (flags >> 8) as u8, flags as u8]);
let one_item_per_step = flags & Prdi::FLAG_ONE_ITEM_PER_STEP != 0;
let alt_is_candidate = flags & Prdi::FLAG_ALTERNATIVE_IS_CANDIDATE != 0;
if !one_item_per_step || alt_is_candidate {
buf.extend_from_slice(&(item_counts.len() as u16).to_be_bytes());
}
if !one_item_per_step {
for &c in item_counts {
buf.extend_from_slice(&c.to_be_bytes());
}
}
buf
}
#[test]
fn prdi_full_form_reads_step_count_and_item_counts() {
let p = parse_prdi(&prdi_body(0, 0, &[3, 1, 2])).unwrap();
assert_eq!(p.flags, 0);
assert_eq!(p.step_count, Some(3));
assert_eq!(p.item_counts.as_deref(), Some(&[3u16, 1, 2][..]));
assert!(!p.is_one_item_per_step());
assert!(!p.is_alternative_is_candidate());
assert!(!p.is_item_reference_order());
assert_eq!(p.step_count_or_inferred(99), 3);
assert_eq!(p.item_count_for_step(0), Some(3));
assert_eq!(p.item_count_for_step(2), Some(2));
assert_eq!(p.item_count_for_step(3), None);
}
#[test]
fn prdi_one_item_per_step_with_alternatives_keeps_step_count_only() {
let flags = Prdi::FLAG_ONE_ITEM_PER_STEP | Prdi::FLAG_ALTERNATIVE_IS_CANDIDATE;
let p = parse_prdi(&prdi_body(0, flags, &[0, 0, 0, 0])).unwrap();
assert_eq!(p.step_count, Some(4));
assert_eq!(p.item_counts, None);
assert!(p.is_one_item_per_step());
assert!(p.is_alternative_is_candidate());
for i in 0..4 {
assert_eq!(p.item_count_for_step(i), Some(1));
}
assert_eq!(p.step_count_or_inferred(99), 4);
}
#[test]
fn prdi_one_item_per_step_no_alternatives_has_empty_body() {
let flags = Prdi::FLAG_ONE_ITEM_PER_STEP;
let body = prdi_body(0, flags, &[]);
assert_eq!(body.len(), 4);
let p = parse_prdi(&body).unwrap();
assert_eq!(p.step_count, None);
assert_eq!(p.item_counts, None);
assert_eq!(p.step_count_or_inferred(5), 5);
assert_eq!(p.item_count_for_step(0), Some(1));
assert_eq!(p.item_count_for_step(42), Some(1));
}
#[test]
fn prdi_item_reference_order_flag_round_trips() {
let flags = Prdi::FLAG_ITEM_REFERENCE_ORDER;
let p = parse_prdi(&prdi_body(0, flags, &[2, 2])).unwrap();
assert!(p.is_item_reference_order());
assert!(!p.is_one_item_per_step());
assert_eq!(p.step_count, Some(2));
assert_eq!(p.item_counts.as_deref(), Some(&[2u16, 2][..]));
assert_eq!(p.flags, Prdi::FLAG_ITEM_REFERENCE_ORDER);
}
#[test]
fn prdi_zero_step_count_yields_empty_item_counts() {
let p = parse_prdi(&prdi_body(0, 0, &[])).unwrap();
assert_eq!(p.step_count, Some(0));
assert_eq!(p.item_counts.as_deref(), Some(&[][..]));
assert_eq!(p.item_count_for_step(0), None);
}
#[test]
fn prdi_rejects_unknown_version() {
let mut buf = prdi_body(0, 0, &[1]);
buf[0] = 1;
assert!(parse_prdi(&buf).is_err());
}
#[test]
fn prdi_rejects_truncated_body() {
assert!(parse_prdi(&[0u8, 0, 0, 0]).is_err());
let mut buf = vec![0u8, 0, 0, 0];
buf.extend_from_slice(&2u16.to_be_bytes()); buf.extend_from_slice(&7u16.to_be_bytes()); assert!(parse_prdi(&buf).is_err());
let flags = Prdi::FLAG_ONE_ITEM_PER_STEP | Prdi::FLAG_ALTERNATIVE_IS_CANDIDATE;
let hdr = [0u8, (flags >> 16) as u8, (flags >> 8) as u8, flags as u8];
assert!(parse_prdi(&hdr).is_err());
}
#[test]
fn prdi_tolerates_trailing_bytes() {
let mut body = prdi_body(0, 0, &[1, 2]);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let p = parse_prdi(&body).unwrap();
assert_eq!(p.step_count, Some(2));
assert_eq!(p.item_counts.as_deref(), Some(&[1u16, 2][..]));
}
#[test]
fn prdi_dispatched_through_parse_ipco() {
let body = prdi_body(0, 0, &[3, 1]);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"prdi");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Prdi(p) => {
assert_eq!(p.step_count, Some(2));
assert_eq!(p.item_counts.as_deref(), Some(&[3u16, 1][..]));
}
other => panic!("expected Prdi, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"prdi");
}
#[test]
fn prdi_essential_association_is_recognised() {
let m = Meta {
properties: vec![Property::Prdi(Prdi::default())],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: vec![PropertyAssociation {
index: 0,
essential: true,
}],
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn prdi_lookup_via_property_for() {
let m = Meta {
properties: vec![Property::Prdi(Prdi {
flags: Prdi::FLAG_ITEM_REFERENCE_ORDER,
step_count: Some(2),
item_counts: Some(vec![1, 1]),
})],
associations: vec![ItemPropertyAssociation {
item_id: 9,
entries: vec![PropertyAssociation {
index: 0,
essential: false,
}],
}],
..Meta::default()
};
match m.property_for(9, b"prdi") {
Some(Property::Prdi(p)) => {
assert!(p.is_item_reference_order());
assert_eq!(p.step_count, Some(2));
}
other => panic!("expected Prdi, got {other:?}"),
}
assert!(m.property_for(99, b"prdi").is_none());
}
#[test]
fn sstr_empty_body_parses_and_dispatches() {
assert_eq!(parse_sstr(&[]).unwrap(), Sstr);
let mut ipco = Vec::new();
ipco.extend_from_slice(&8u32.to_be_bytes());
ipco.extend_from_slice(b"sstr");
let props = parse_ipco(&ipco).unwrap();
assert_eq!(props.len(), 1);
assert!(matches!(props[0], Property::Sstr(_)));
assert_eq!(props[0].kind(), *b"sstr");
}
#[test]
fn sstr_tolerates_trailing_bytes() {
assert_eq!(parse_sstr(&[0xDE, 0xAD]).unwrap(), Sstr);
}
#[allow(clippy::too_many_arguments)]
fn txlo_body(
flags: u32,
reference_width: u32,
reference_height: u32,
x: i32,
y: i32,
width: u32,
height: u32,
font_size: u16,
direction: &str,
writing_mode: &str,
) -> Vec<u8> {
let mut buf = vec![0u8, (flags >> 16) as u8, (flags >> 8) as u8, flags as u8];
let large = flags & Txlo::FLAG_LARGE_FIELD_SIZE != 0;
let push_u = |buf: &mut Vec<u8>, v: u32| {
if large {
buf.extend_from_slice(&v.to_be_bytes());
} else {
buf.extend_from_slice(&(v as u16).to_be_bytes());
}
};
let push_i = |buf: &mut Vec<u8>, v: i32| {
if large {
buf.extend_from_slice(&v.to_be_bytes());
} else {
buf.extend_from_slice(&(v as i16).to_be_bytes());
}
};
push_u(&mut buf, reference_width);
push_u(&mut buf, reference_height);
push_i(&mut buf, x);
push_i(&mut buf, y);
push_u(&mut buf, width);
push_u(&mut buf, height);
buf.extend_from_slice(&font_size.to_be_bytes());
buf.extend_from_slice(direction.as_bytes());
buf.push(0);
buf.extend_from_slice(writing_mode.as_bytes());
buf.push(0);
buf
}
#[test]
fn txlo_16bit_fields_parse() {
let body = txlo_body(0, 1920, 1080, -5, -3, 640, 480, 0x0A00, "ltr", "lrtb");
let t = parse_txlo(&body).unwrap();
assert!(!t.is_large_field_size());
assert_eq!(t.reference_width, 1920);
assert_eq!(t.reference_height, 1080);
assert_eq!(t.x, -5);
assert_eq!(t.y, -3);
assert_eq!(t.width, 640);
assert_eq!(t.height, 480);
assert_eq!(t.font_size, 0x0A00);
assert_eq!(t.font_size_percent(), 10.0);
assert_eq!(t.direction, "ltr");
assert_eq!(t.writing_mode, "lrtb");
}
#[test]
fn txlo_32bit_fields_parse() {
let body = txlo_body(1, 70000, 80000, -100000, 42, 65536, 1, 256, "rtl", "tblr");
let t = parse_txlo(&body).unwrap();
assert!(t.is_large_field_size());
assert_eq!(t.reference_width, 70000);
assert_eq!(t.reference_height, 80000);
assert_eq!(t.x, -100000);
assert_eq!(t.y, 42);
assert_eq!(t.width, 65536);
assert_eq!(t.font_size_percent(), 1.0);
assert_eq!(t.direction, "rtl");
assert_eq!(t.writing_mode, "tblr");
}
#[test]
fn txlo_empty_strings_round_trip_and_dispatch() {
let body = txlo_body(0, 100, 100, 0, 0, 100, 100, 0x1900, "", "");
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"txlo");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
match &props[0] {
Property::Txlo(t) => {
assert_eq!(t.direction, "");
assert_eq!(t.writing_mode, "");
assert_eq!(t.font_size_percent(), 25.0);
}
other => panic!("expected Txlo, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"txlo");
}
#[test]
fn txlo_rejects_unknown_version() {
let mut body = txlo_body(0, 10, 10, 0, 0, 10, 10, 0, "ltr", "lrtb");
body[0] = 1;
assert!(parse_txlo(&body).is_err());
}
#[test]
fn txlo_rejects_truncated_geometry() {
let body = vec![0u8, 0, 0, 0, 0x07, 0x80, 0x04];
assert!(parse_txlo(&body).is_err());
}
fn elng_body(version: u8, lang: &str) -> Vec<u8> {
let mut buf = vec![version, 0, 0, 0];
buf.extend_from_slice(lang.as_bytes());
buf.push(0);
buf
}
#[test]
fn elng_parses_language_tag_and_dispatches() {
let body = elng_body(0, "en-US");
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"elng");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
match &props[0] {
Property::Elng(e) => assert_eq!(e.extended_language, "en-US"),
other => panic!("expected Elng, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"elng");
}
#[test]
fn elng_empty_tag_round_trips() {
let e = parse_elng(&elng_body(0, "")).unwrap();
assert_eq!(e.extended_language, "");
}
#[test]
fn elng_rejects_unknown_version() {
assert!(parse_elng(&elng_body(1, "de")).is_err());
}
fn fnch_body(version: u8, family: &str, style: &str, weight: &str) -> Vec<u8> {
let mut buf = vec![version, 0, 0, 0];
for s in [family, style, weight] {
buf.extend_from_slice(s.as_bytes());
buf.push(0);
}
buf
}
#[test]
fn fnch_parses_three_strings_and_dispatches() {
let body = fnch_body(0, "Helvetica", "italic", "bold");
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"fnch");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
match &props[0] {
Property::Fnch(f) => {
assert_eq!(f.font_family, "Helvetica");
assert_eq!(f.font_style, "italic");
assert_eq!(f.font_weight, "bold");
}
other => panic!("expected Fnch, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"fnch");
}
#[test]
fn fnch_empty_strings_round_trip() {
let f = parse_fnch(&fnch_body(0, "", "", "")).unwrap();
assert_eq!(f.font_family, "");
assert_eq!(f.font_style, "");
assert_eq!(f.font_weight, "");
}
#[test]
fn fnch_rejects_unknown_version() {
assert!(parse_fnch(&fnch_body(2, "Arial", "normal", "normal")).is_err());
}
#[test]
fn fnch_rejects_unterminated_string() {
let mut body = vec![0u8, 0, 0, 0];
body.extend_from_slice(b"Arial");
assert!(parse_fnch(&body).is_err());
}
fn mskc_body(version: u8, bits_per_pixel: u8) -> Vec<u8> {
vec![version, 0, 0, 0, bits_per_pixel]
}
#[test]
fn mskc_parses_bits_per_pixel_and_dispatches() {
for bpp in [1u8, 2, 4, 8, 16, 24] {
let body = mskc_body(0, bpp);
let m = parse_mskc(&body).unwrap();
assert_eq!(m.bits_per_pixel, bpp);
assert!(m.is_defined_depth());
}
let body = mskc_body(0, 4);
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(b"mskC");
ipco.extend_from_slice(&body);
let props = parse_ipco(&ipco).unwrap();
match &props[0] {
Property::MaskC(m) => assert_eq!(m.bits_per_pixel, 4),
other => panic!("expected MaskC, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"mskC");
}
#[test]
fn mskc_pixels_per_byte_mapping() {
assert_eq!(
parse_mskc(&mskc_body(0, 1)).unwrap().pixels_per_byte(),
Some(8)
);
assert_eq!(
parse_mskc(&mskc_body(0, 2)).unwrap().pixels_per_byte(),
Some(4)
);
assert_eq!(
parse_mskc(&mskc_body(0, 4)).unwrap().pixels_per_byte(),
Some(2)
);
assert_eq!(
parse_mskc(&mskc_body(0, 8)).unwrap().pixels_per_byte(),
None
);
assert_eq!(
parse_mskc(&mskc_body(0, 24)).unwrap().pixels_per_byte(),
None
);
}
#[test]
fn mskc_reserved_depth_round_trips_but_flagged() {
let m = parse_mskc(&mskc_body(0, 3)).unwrap();
assert_eq!(m.bits_per_pixel, 3);
assert!(!m.is_defined_depth());
assert_eq!(m.pixels_per_byte(), None);
}
#[test]
fn mskc_rejects_unknown_version_and_truncation() {
assert!(parse_mskc(&mskc_body(1, 8)).is_err());
assert!(parse_mskc(&[0u8, 0, 0, 0]).is_err());
}
fn ipco_one(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut ipco = Vec::new();
let s = 8 + body.len() as u32;
ipco.extend_from_slice(&s.to_be_bytes());
ipco.extend_from_slice(fourcc);
ipco.extend_from_slice(body);
ipco
}
fn full_box_v0(payload: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; 4];
buf.extend_from_slice(payload);
buf
}
#[test]
fn wipe_parses_every_defined_direction() {
for dir in 0u8..=7 {
let w = parse_wipe(&full_box_v0(&[dir])).unwrap();
assert_eq!(w.transition_direction, dir);
assert!(w.is_known_direction());
}
assert_eq!(Wipe::FROM_LEFT, 0);
assert_eq!(Wipe::FROM_RIGHT, 1);
assert_eq!(Wipe::FROM_TOP, 2);
assert_eq!(Wipe::FROM_BOTTOM, 3);
assert_eq!(Wipe::FROM_RIGHT_BOTTOM, 7);
}
#[test]
fn wipe_reserved_direction_surfaced_not_known() {
let w = parse_wipe(&full_box_v0(&[8])).unwrap();
assert_eq!(w.transition_direction, 8);
assert!(!w.is_known_direction());
}
#[test]
fn wipe_rejects_short_and_unknown_version() {
assert!(parse_wipe(&full_box_v0(&[])).is_err());
assert!(parse_wipe(&[1u8, 0, 0, 0, 0]).is_err());
}
#[test]
fn wipe_tolerates_trailing_bytes_and_dispatches() {
let body = full_box_v0(&[Wipe::FROM_LEFT_TOP, 0xAA, 0xBB]);
let props = parse_ipco(&ipco_one(b"wipe", &body)).unwrap();
assert_eq!(props.len(), 1);
match &props[0] {
Property::Wipe(w) => assert_eq!(w.transition_direction, Wipe::FROM_LEFT_TOP),
other => panic!("expected Wipe, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"wipe");
}
#[test]
fn zoom_unpacks_direction_and_shape() {
let packed = (Zoom::DIRECTION_OUT << 7) | Zoom::SHAPE_STAR;
let z = parse_zoom(&full_box_v0(&[packed])).unwrap();
assert_eq!(z.transition_direction, Zoom::DIRECTION_OUT);
assert_eq!(z.transition_shape, Zoom::SHAPE_STAR);
assert!(z.is_known_shape());
let z = parse_zoom(&full_box_v0(&[0])).unwrap();
assert_eq!(z.transition_direction, Zoom::DIRECTION_IN);
assert_eq!(z.transition_shape, Zoom::SHAPE_RECTANGULAR);
let z = parse_zoom(&full_box_v0(&[Zoom::SHAPE_CIRCLE])).unwrap();
assert_eq!(z.transition_direction, Zoom::DIRECTION_IN);
assert_eq!(z.transition_shape, Zoom::SHAPE_CIRCLE);
}
#[test]
fn zoom_reserved_shape_surfaced_not_known() {
let z = parse_zoom(&full_box_v0(&[0xFF])).unwrap();
assert_eq!(z.transition_direction, Zoom::DIRECTION_OUT);
assert_eq!(z.transition_shape, 0x7f);
assert!(!z.is_known_shape());
}
#[test]
fn zoom_rejects_short_and_unknown_version() {
assert!(parse_zoom(&full_box_v0(&[])).is_err());
assert!(parse_zoom(&[3u8, 0, 0, 0, 0]).is_err());
}
#[test]
fn zoom_dispatches_through_ipco() {
let z = (Zoom::DIRECTION_IN << 7) | Zoom::SHAPE_CIRCLE;
let props = parse_ipco(&ipco_one(b"zoom", &full_box_v0(&[z]))).unwrap();
match &props[0] {
Property::Zoom(z) => {
assert_eq!(z.transition_direction, Zoom::DIRECTION_IN);
assert_eq!(z.transition_shape, Zoom::SHAPE_CIRCLE);
}
other => panic!("expected Zoom, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"zoom");
}
#[test]
fn fade_parses_every_defined_direction() {
for dir in 0u8..=2 {
let f = parse_fade(&full_box_v0(&[dir])).unwrap();
assert_eq!(f.transition_direction, dir);
assert!(f.is_known_direction());
}
assert_eq!(Fade::THROUGH_WHITE, 0);
assert_eq!(Fade::THROUGH_BLACK, 1);
assert_eq!(Fade::DISSOLVE, 2);
let f = parse_fade(&full_box_v0(&[3])).unwrap();
assert!(!f.is_known_direction());
}
#[test]
fn fade_rejects_short_and_unknown_version() {
assert!(parse_fade(&full_box_v0(&[])).is_err());
assert!(parse_fade(&[2u8, 0, 0, 0, 1]).is_err());
}
#[test]
fn splt_parses_every_defined_direction_and_dispatches() {
for dir in 0u8..=3 {
let s = parse_splt(&full_box_v0(&[dir])).unwrap();
assert_eq!(s.transition_direction, dir);
assert!(s.is_known_direction());
}
assert_eq!(Splt::VERTICAL_IN, 0);
assert_eq!(Splt::VERTICAL_OUT, 1);
assert_eq!(Splt::HORIZONTAL_IN, 2);
assert_eq!(Splt::HORIZONTAL_OUT, 3);
let s = parse_splt(&full_box_v0(&[4])).unwrap();
assert!(!s.is_known_direction());
let props = parse_ipco(&ipco_one(b"splt", &full_box_v0(&[Splt::HORIZONTAL_IN]))).unwrap();
match &props[0] {
Property::Splt(s) => assert_eq!(s.transition_direction, Splt::HORIZONTAL_IN),
other => panic!("expected Splt, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"splt");
}
#[test]
fn splt_rejects_short_and_unknown_version() {
assert!(parse_splt(&full_box_v0(&[])).is_err());
assert!(parse_splt(&[1u8, 0, 0, 0, 0]).is_err());
}
#[test]
fn stpe_parses_and_converts_period() {
let p = parse_stpe(&full_box_v0(&[32])).unwrap();
assert_eq!(p.transition_period, 32);
assert!((p.seconds() - 2.0).abs() < 1e-9);
let p = parse_stpe(&full_box_v0(&[8])).unwrap();
assert!((p.seconds() - 0.5).abs() < 1e-9);
}
#[test]
fn stpe_rejects_short_and_unknown_version() {
assert!(parse_stpe(&full_box_v0(&[])).is_err());
assert!(parse_stpe(&[1u8, 0, 0, 0, 5]).is_err());
}
#[test]
fn ssld_parses_converts_and_flags_reserved() {
let d = parse_ssld(&full_box_v0(&64u16.to_be_bytes())).unwrap();
assert_eq!(d.duration, 64);
assert!((d.seconds() - 4.0).abs() < 1e-9);
assert!(!d.is_reserved());
let d = parse_ssld(&full_box_v0(&0u16.to_be_bytes())).unwrap();
assert!(d.is_reserved());
assert_eq!(d.seconds(), 0.0);
}
#[test]
fn ssld_rejects_short_and_unknown_version() {
assert!(parse_ssld(&full_box_v0(&[0])).is_err());
assert!(parse_ssld(&full_box_v0(&[])).is_err());
assert!(parse_ssld(&[1u8, 0, 0, 0, 0, 16]).is_err());
}
#[test]
fn ssld_dispatches_and_tolerates_trailing() {
let mut body = full_box_v0(&80u16.to_be_bytes());
body.extend_from_slice(&[0xDE, 0xAD]);
let props = parse_ipco(&ipco_one(b"ssld", &body)).unwrap();
match &props[0] {
Property::Ssld(d) => assert_eq!(d.duration, 80),
other => panic!("expected Ssld, got {other:?}"),
}
assert_eq!(props[0].kind(), *b"ssld");
}
#[test]
fn transition_effects_essential_association_recognised() {
let m = Meta {
properties: vec![
Property::Wipe(Wipe::default()),
Property::Zoom(Zoom::default()),
Property::Fade(Fade::default()),
Property::Splt(Splt::default()),
Property::Stpe(Stpe::default()),
Property::Ssld(Ssld::default()),
],
associations: vec![ItemPropertyAssociation {
item_id: 1,
entries: (0..6)
.map(|i| PropertyAssociation {
index: i,
essential: true,
})
.collect(),
}],
..Meta::default()
};
assert!(!m.has_unsupported_essential_property(1));
assert!(m.unsupported_essential_properties(1).is_empty());
}
#[test]
fn transition_effects_lookup_via_property_for() {
let m = Meta {
properties: vec![
Property::Wipe(Wipe {
transition_direction: Wipe::FROM_TOP,
}),
Property::Ssld(Ssld { duration: 96 }),
],
associations: vec![ItemPropertyAssociation {
item_id: 7,
entries: vec![
PropertyAssociation {
index: 0,
essential: false,
},
PropertyAssociation {
index: 1,
essential: false,
},
],
}],
..Meta::default()
};
match m.property_for(7, b"wipe") {
Some(Property::Wipe(w)) => assert_eq!(w.transition_direction, Wipe::FROM_TOP),
other => panic!("expected Wipe, got {other:?}"),
}
match m.property_for(7, b"ssld") {
Some(Property::Ssld(d)) => assert_eq!(d.duration, 96),
other => panic!("expected Ssld, got {other:?}"),
}
assert!(m.property_for(99, b"wipe").is_none());
}
}