use crate::parser::{read_i16, read_i8, read_u16, read_u32};
use crate::Error;
type RegionAxis = (f32, f32, f32);
type Region = Vec<RegionAxis>;
const MAX_VALUE_RECORDS: u16 = 2048;
const MAX_IVD_SUBTABLES: u16 = 4096;
const MAX_REGIONS: u16 = 4096;
#[derive(Debug, Clone)]
pub struct MvarTable {
records: Vec<([u8; 4], u16, u16)>,
ivs: Option<ItemVariationStore>,
}
#[derive(Debug, Clone)]
pub struct ItemVariationStore {
axis_count: u16,
regions: Vec<Region>,
subtables: Vec<ItemVariationData>,
}
#[derive(Debug, Clone)]
struct ItemVariationData {
region_indexes: Vec<u16>,
delta_sets: Vec<Vec<i32>>,
}
impl MvarTable {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < 12 {
return Err(Error::UnexpectedEof);
}
let major = read_u16(bytes, 0)?;
let _minor = read_u16(bytes, 2)?;
let value_record_size = read_u16(bytes, 6)?;
let value_record_count = read_u16(bytes, 8)?;
let ivs_offset = read_u16(bytes, 10)? as usize;
if major != 1 {
return Err(Error::BadStructure("MVAR majorVersion != 1"));
}
if value_record_count == 0 {
return Ok(Self {
records: Vec::new(),
ivs: None,
});
}
if value_record_size < 8 {
return Err(Error::BadStructure("MVAR valueRecordSize < 8"));
}
if value_record_count > MAX_VALUE_RECORDS {
return Err(Error::BadStructure("MVAR valueRecordCount exceeds cap"));
}
if ivs_offset == 0 || ivs_offset > bytes.len() {
return Err(Error::BadOffset);
}
let stride = value_record_size as usize;
let total_records_bytes = (value_record_count as usize)
.checked_mul(stride)
.ok_or(Error::BadOffset)?;
if 12usize
.checked_add(total_records_bytes)
.map(|end| end > bytes.len())
.unwrap_or(true)
{
return Err(Error::UnexpectedEof);
}
let mut records = Vec::with_capacity(value_record_count as usize);
for i in 0..value_record_count as usize {
let off = 12 + i * stride;
let tag = [bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]];
let outer = read_u16(bytes, off + 4)?;
let inner = read_u16(bytes, off + 6)?;
records.push((tag, outer, inner));
}
let ivs = ItemVariationStore::parse(&bytes[ivs_offset..])?;
Ok(Self {
records,
ivs: Some(ivs),
})
}
pub fn value_record_count(&self) -> usize {
self.records.len()
}
pub fn value_records(&self) -> impl Iterator<Item = ([u8; 4], u16, u16)> + '_ {
self.records.iter().copied()
}
pub fn delta_for_tag(&self, tag: &[u8; 4], normalised_coords: &[f32]) -> Option<f32> {
let ivs = self.ivs.as_ref()?;
let (_, outer, inner) = self.records.iter().copied().find(|(t, _, _)| t == tag)?;
ivs.delta(outer, inner, normalised_coords)
}
pub fn item_variation_store(&self) -> Option<&ItemVariationStore> {
self.ivs.as_ref()
}
}
impl ItemVariationStore {
pub(crate) fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < 8 {
return Err(Error::UnexpectedEof);
}
let format = read_u16(bytes, 0)?;
if format != 1 {
return Err(Error::BadStructure("IVS format != 1"));
}
let vrl_off = read_u32(bytes, 2)? as usize;
let ivd_count = read_u16(bytes, 6)?;
if ivd_count > MAX_IVD_SUBTABLES {
return Err(Error::BadStructure("IVS subtable count exceeds cap"));
}
if vrl_off == 0 || vrl_off > bytes.len() {
return Err(Error::BadOffset);
}
let regions = parse_region_list(&bytes[vrl_off..])?;
let mut subtables = Vec::with_capacity(ivd_count as usize);
let off_base = 8usize;
let need = (ivd_count as usize)
.checked_mul(4)
.and_then(|n| n.checked_add(off_base))
.ok_or(Error::BadOffset)?;
if need > bytes.len() {
return Err(Error::UnexpectedEof);
}
for i in 0..ivd_count as usize {
let off = off_base + i * 4;
let sub_off = read_u32(bytes, off)? as usize;
if sub_off == 0 || sub_off > bytes.len() {
return Err(Error::BadOffset);
}
subtables.push(ItemVariationData::parse(
&bytes[sub_off..],
regions.len() as u16,
)?);
}
let axis_count = if let Some(first) = regions.first() {
first.len() as u16
} else {
0
};
Ok(Self {
axis_count,
regions,
subtables,
})
}
pub fn axis_count(&self) -> u16 {
self.axis_count
}
pub fn region_count(&self) -> usize {
self.regions.len()
}
pub fn subtable_count(&self) -> usize {
self.subtables.len()
}
pub fn delta(&self, outer: u16, inner: u16, normalised_coords: &[f32]) -> Option<f32> {
let sub = self.subtables.get(outer as usize)?;
let row = sub.delta_sets.get(inner as usize)?;
let mut acc = 0.0f32;
for (col, &delta) in row.iter().enumerate() {
let region_index = *sub.region_indexes.get(col)? as usize;
let region = self.regions.get(region_index)?;
let scalar = region_scalar(region, normalised_coords);
if scalar == 0.0 {
continue;
}
acc += scalar * delta as f32;
}
Some(acc)
}
}
fn parse_region_list(bytes: &[u8]) -> Result<Vec<Region>, Error> {
if bytes.len() < 4 {
return Err(Error::UnexpectedEof);
}
let axis_count = read_u16(bytes, 0)?;
let region_count = read_u16(bytes, 2)?;
if region_count > MAX_REGIONS {
return Err(Error::BadStructure("IVS regionCount exceeds cap"));
}
let stride = (axis_count as usize)
.checked_mul(6)
.ok_or(Error::BadOffset)?;
let total = (region_count as usize)
.checked_mul(stride)
.and_then(|n| n.checked_add(4))
.ok_or(Error::BadOffset)?;
if total > bytes.len() {
return Err(Error::UnexpectedEof);
}
let mut regions = Vec::with_capacity(region_count as usize);
for r in 0..region_count as usize {
let base = 4 + r * stride;
let mut axes = Vec::with_capacity(axis_count as usize);
for a in 0..axis_count as usize {
let off = base + a * 6;
let start = f2dot14(read_i16(bytes, off)?);
let peak = f2dot14(read_i16(bytes, off + 2)?);
let end = f2dot14(read_i16(bytes, off + 4)?);
axes.push((start, peak, end));
}
regions.push(axes);
}
Ok(regions)
}
impl ItemVariationData {
fn parse(bytes: &[u8], region_count_in_store: u16) -> Result<Self, Error> {
if bytes.len() < 6 {
return Err(Error::UnexpectedEof);
}
let item_count = read_u16(bytes, 0)?;
let short_delta_count = read_u16(bytes, 2)?;
let region_index_count = read_u16(bytes, 4)?;
if short_delta_count > region_index_count {
return Err(Error::BadStructure("shortDeltaCount > regionIndexCount"));
}
let region_index_bytes = (region_index_count as usize)
.checked_mul(2)
.ok_or(Error::BadOffset)?;
let header_end = 6usize
.checked_add(region_index_bytes)
.ok_or(Error::BadOffset)?;
if header_end > bytes.len() {
return Err(Error::UnexpectedEof);
}
let mut region_indexes = Vec::with_capacity(region_index_count as usize);
for i in 0..region_index_count as usize {
let idx = read_u16(bytes, 6 + i * 2)?;
if idx >= region_count_in_store {
return Err(Error::BadStructure("IVD region index out of range"));
}
region_indexes.push(idx);
}
let row_size = (short_delta_count as usize)
.checked_mul(2)
.and_then(|n| n.checked_add((region_index_count - short_delta_count) as usize))
.ok_or(Error::BadOffset)?;
let total = (item_count as usize)
.checked_mul(row_size)
.and_then(|n| n.checked_add(header_end))
.ok_or(Error::BadOffset)?;
if total > bytes.len() {
return Err(Error::UnexpectedEof);
}
let mut delta_sets = Vec::with_capacity(item_count as usize);
for r in 0..item_count as usize {
let row_start = header_end + r * row_size;
let mut row = Vec::with_capacity(region_index_count as usize);
for c in 0..short_delta_count as usize {
row.push(read_i16(bytes, row_start + c * 2)? as i32);
}
let i8_base = row_start + (short_delta_count as usize) * 2;
for c in 0..(region_index_count - short_delta_count) as usize {
row.push(read_i8(bytes, i8_base + c)? as i32);
}
delta_sets.push(row);
}
Ok(Self {
region_indexes,
delta_sets,
})
}
}
fn region_scalar(region: &[RegionAxis], coords: &[f32]) -> f32 {
let mut s = 1.0f32;
for (ai, &(start, peak, end)) in region.iter().enumerate() {
let c = coords.get(ai).copied().unwrap_or(0.0);
if peak == 0.0 {
continue;
}
if c == peak {
continue;
}
if c <= start || c >= end {
return 0.0;
}
if c < peak {
if (peak - start).abs() < f32::EPSILON {
return 0.0;
}
s *= (c - start) / (peak - start);
} else {
if (end - peak).abs() < f32::EPSILON {
return 0.0;
}
s *= (end - c) / (end - peak);
}
}
s
}
#[inline]
fn f2dot14(raw: i16) -> f32 {
raw as f32 / 16384.0
}
#[cfg(test)]
mod tests {
use super::*;
fn build_single_axis_mvar() -> Vec<u8> {
let ivd_rel = 22u32;
let mut b = vec![0u8; 12 + 8 + 22 + 10];
b[0..2].copy_from_slice(&1u16.to_be_bytes()); b[6..8].copy_from_slice(&8u16.to_be_bytes()); b[8..10].copy_from_slice(&1u16.to_be_bytes()); b[10..12].copy_from_slice(&20u16.to_be_bytes());
b[12..16].copy_from_slice(b"xhgt");
b[16..18].copy_from_slice(&0u16.to_be_bytes()); b[18..20].copy_from_slice(&0u16.to_be_bytes());
let ivs = 20usize;
b[ivs..ivs + 2].copy_from_slice(&1u16.to_be_bytes()); b[ivs + 2..ivs + 6].copy_from_slice(&12u32.to_be_bytes()); b[ivs + 6..ivs + 8].copy_from_slice(&1u16.to_be_bytes()); b[ivs + 8..ivs + 12].copy_from_slice(&ivd_rel.to_be_bytes());
let rl = ivs + 12;
b[rl..rl + 2].copy_from_slice(&1u16.to_be_bytes()); b[rl + 2..rl + 4].copy_from_slice(&1u16.to_be_bytes()); b[rl + 4..rl + 6].copy_from_slice(&0i16.to_be_bytes());
b[rl + 6..rl + 8].copy_from_slice(&16384i16.to_be_bytes());
b[rl + 8..rl + 10].copy_from_slice(&16384i16.to_be_bytes());
let ivd = ivs + ivd_rel as usize;
b[ivd..ivd + 2].copy_from_slice(&1u16.to_be_bytes()); b[ivd + 2..ivd + 4].copy_from_slice(&1u16.to_be_bytes()); b[ivd + 4..ivd + 6].copy_from_slice(&1u16.to_be_bytes()); b[ivd + 6..ivd + 8].copy_from_slice(&0u16.to_be_bytes()); b[ivd + 8..ivd + 10].copy_from_slice(&(-100i16).to_be_bytes()); b
}
#[test]
fn parses_minimal_table() {
let raw = build_single_axis_mvar();
let m = MvarTable::parse(&raw).expect("parse");
assert_eq!(m.value_record_count(), 1);
assert_eq!(
m.value_records().collect::<Vec<_>>(),
vec![(*b"xhgt", 0u16, 0u16)]
);
let ivs = m.item_variation_store().expect("ivs");
assert_eq!(ivs.axis_count(), 1);
assert_eq!(ivs.region_count(), 1);
assert_eq!(ivs.subtable_count(), 1);
}
#[test]
fn delta_zero_at_default_coords() {
let raw = build_single_axis_mvar();
let m = MvarTable::parse(&raw).unwrap();
let d = m.delta_for_tag(b"xhgt", &[0.0]).expect("known tag");
assert_eq!(d, 0.0);
}
#[test]
fn delta_interpolates_along_axis() {
let raw = build_single_axis_mvar();
let m = MvarTable::parse(&raw).unwrap();
let d = m.delta_for_tag(b"xhgt", &[0.5]).expect("known tag");
assert!((d - (-50.0)).abs() < 1e-5, "got {d}");
let d = m.delta_for_tag(b"xhgt", &[1.0]).expect("known tag");
assert!((d - (-100.0)).abs() < 1e-5, "got {d}");
}
#[test]
fn unknown_tag_returns_none() {
let raw = build_single_axis_mvar();
let m = MvarTable::parse(&raw).unwrap();
assert!(m.delta_for_tag(b"none", &[0.5]).is_none());
}
#[test]
fn empty_value_records_parses_with_no_ivs() {
let mut b = vec![0u8; 12];
b[0..2].copy_from_slice(&1u16.to_be_bytes());
let m = MvarTable::parse(&b).expect("parse");
assert_eq!(m.value_record_count(), 0);
assert!(m.item_variation_store().is_none());
assert!(m.delta_for_tag(b"xhgt", &[0.0]).is_none());
}
#[test]
fn rejects_short_value_record_size() {
let mut b = vec![0u8; 14];
b[0..2].copy_from_slice(&1u16.to_be_bytes());
b[6..8].copy_from_slice(&4u16.to_be_bytes()); b[8..10].copy_from_slice(&1u16.to_be_bytes()); b[10..12].copy_from_slice(&12u16.to_be_bytes());
assert!(matches!(MvarTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn honours_larger_value_record_stride() {
let stride: u16 = 10;
let count: u16 = 1;
let ivd_rel: u32 = 22;
let mut b = vec![0u8; 12 + stride as usize + 32];
b[0..2].copy_from_slice(&1u16.to_be_bytes());
b[6..8].copy_from_slice(&stride.to_be_bytes());
b[8..10].copy_from_slice(&count.to_be_bytes());
let ivs_off: u16 = 12 + stride;
b[10..12].copy_from_slice(&ivs_off.to_be_bytes());
b[12..16].copy_from_slice(b"hasc");
b[16..18].copy_from_slice(&0u16.to_be_bytes()); b[18..20].copy_from_slice(&0u16.to_be_bytes()); let ivs = ivs_off as usize;
b[ivs..ivs + 2].copy_from_slice(&1u16.to_be_bytes());
b[ivs + 2..ivs + 6].copy_from_slice(&12u32.to_be_bytes()); b[ivs + 6..ivs + 8].copy_from_slice(&1u16.to_be_bytes());
b[ivs + 8..ivs + 12].copy_from_slice(&ivd_rel.to_be_bytes());
let rl = ivs + 12;
b[rl..rl + 2].copy_from_slice(&1u16.to_be_bytes()); b[rl + 2..rl + 4].copy_from_slice(&1u16.to_be_bytes()); b[rl + 6..rl + 8].copy_from_slice(&16384i16.to_be_bytes());
b[rl + 8..rl + 10].copy_from_slice(&16384i16.to_be_bytes());
let ivd = ivs + ivd_rel as usize;
b[ivd..ivd + 2].copy_from_slice(&1u16.to_be_bytes());
b[ivd + 2..ivd + 4].copy_from_slice(&1u16.to_be_bytes());
b[ivd + 4..ivd + 6].copy_from_slice(&1u16.to_be_bytes());
b[ivd + 6..ivd + 8].copy_from_slice(&0u16.to_be_bytes());
b[ivd + 8..ivd + 10].copy_from_slice(&(42i16).to_be_bytes());
let m = MvarTable::parse(&b).expect("parse");
assert_eq!(m.value_record_count(), 1);
let d = m.delta_for_tag(b"hasc", &[1.0]).expect("tag");
assert!((d - 42.0).abs() < 1e-5);
}
#[test]
fn region_scalar_zero_outside_span() {
let region = [(-1.0f32, -1.0, 0.0)];
assert_eq!(region_scalar(®ion, &[0.5]), 0.0);
assert_eq!(region_scalar(®ion, &[0.0]), 0.0);
assert_eq!(region_scalar(®ion, &[-1.0]), 1.0);
let s = region_scalar(®ion, &[-0.5]);
assert!((s - 0.5).abs() < 1e-5);
}
#[test]
fn region_scalar_axis_ignored_when_peak_zero() {
let region = [(0.0f32, 0.0, 0.0), (0.0, 1.0, 1.0)];
let s = region_scalar(®ion, &[0.7, 1.0]);
assert!((s - 1.0).abs() < 1e-5);
}
#[test]
fn rejects_ivs_format_other_than_1() {
let mut b = build_single_axis_mvar();
let ivs = 20usize;
b[ivs..ivs + 2].copy_from_slice(&2u16.to_be_bytes());
assert!(matches!(MvarTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_out_of_range_region_index_in_ivd() {
let mut b = build_single_axis_mvar();
let ivd = 20 + 22;
b[ivd + 6..ivd + 8].copy_from_slice(&5u16.to_be_bytes());
assert!(matches!(MvarTable::parse(&b), Err(Error::BadStructure(_))));
}
}