use crate::parser::{read_i32, read_u16};
use crate::Error;
const MIN_AXIS_SIZE: u16 = 20;
const MAX_AXES: u16 = 64;
const MAX_INSTANCES: u16 = 4096;
pub const AXIS_FLAG_HIDDEN: u16 = 0x0001;
#[derive(Debug, Clone, PartialEq)]
pub struct VariationAxis {
pub tag: [u8; 4],
pub min: f32,
pub default: f32,
pub max: f32,
pub flags: u16,
pub name_id: u16,
}
impl VariationAxis {
pub fn is_hidden(&self) -> bool {
self.flags & AXIS_FLAG_HIDDEN != 0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NamedInstance {
pub subfamily_name_id: u16,
pub flags: u16,
pub coords: Vec<f32>,
pub post_script_name_id: Option<u16>,
}
#[derive(Debug, Clone)]
pub struct FvarTable {
axes: Vec<VariationAxis>,
instances: Vec<NamedInstance>,
}
impl FvarTable {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < 16 {
return Err(Error::UnexpectedEof);
}
let major = read_u16(bytes, 0)?;
let minor = read_u16(bytes, 2)?;
if major != 1 || minor != 0 {
return Err(Error::BadStructure("fvar version not 1.0"));
}
let axes_array_offset = read_u16(bytes, 4)? as usize;
let axis_count = read_u16(bytes, 8)?;
let axis_size = read_u16(bytes, 10)?;
let instance_count = read_u16(bytes, 12)?;
let instance_size = read_u16(bytes, 14)?;
if axis_count > MAX_AXES {
return Err(Error::BadStructure("fvar axisCount exceeds sanity cap"));
}
if instance_count > MAX_INSTANCES {
return Err(Error::BadStructure("fvar instanceCount exceeds sanity cap"));
}
if axis_size < MIN_AXIS_SIZE {
return Err(Error::BadStructure("fvar axisSize < 20"));
}
let min_instance_size = 4u16
.checked_add(
axis_count
.checked_mul(4)
.ok_or(Error::BadStructure("fvar axisCount * 4 overflow"))?,
)
.ok_or(Error::BadStructure("fvar instanceSize overflow"))?;
if instance_size != min_instance_size && instance_size != min_instance_size + 2 {
return Err(Error::BadStructure("fvar instanceSize unexpected"));
}
let has_psname = instance_size == min_instance_size + 2;
let mut axes = Vec::with_capacity(axis_count as usize);
for i in 0..axis_count as usize {
let off = axes_array_offset
.checked_add(i.checked_mul(axis_size as usize).ok_or(Error::BadOffset)?)
.ok_or(Error::BadOffset)?;
if off + axis_size as usize > bytes.len() {
return Err(Error::UnexpectedEof);
}
let rec = &bytes[off..off + axis_size as usize];
let mut tag = [0u8; 4];
tag.copy_from_slice(&rec[0..4]);
let min = fixed_to_f32(read_i32(rec, 4)?);
let default = fixed_to_f32(read_i32(rec, 8)?);
let max = fixed_to_f32(read_i32(rec, 12)?);
let flags = read_u16(rec, 16)?;
let name_id = read_u16(rec, 18)?;
if !(min <= default && default <= max) {
return Err(Error::BadStructure("fvar axis min/default/max disorder"));
}
axes.push(VariationAxis {
tag,
min,
default,
max,
flags,
name_id,
});
}
let inst_array_offset = axes_array_offset
.checked_add(
(axis_count as usize)
.checked_mul(axis_size as usize)
.ok_or(Error::BadOffset)?,
)
.ok_or(Error::BadOffset)?;
let mut instances = Vec::with_capacity(instance_count as usize);
for i in 0..instance_count as usize {
let off = inst_array_offset
.checked_add(
i.checked_mul(instance_size as usize)
.ok_or(Error::BadOffset)?,
)
.ok_or(Error::BadOffset)?;
if off + instance_size as usize > bytes.len() {
return Err(Error::UnexpectedEof);
}
let rec = &bytes[off..off + instance_size as usize];
let subfamily_name_id = read_u16(rec, 0)?;
let flags = read_u16(rec, 2)?;
let mut coords = Vec::with_capacity(axis_count as usize);
for ai in 0..axis_count as usize {
coords.push(fixed_to_f32(read_i32(rec, 4 + ai * 4)?));
}
let post_script_name_id = if has_psname {
Some(read_u16(rec, 4 + axis_count as usize * 4)?)
} else {
None
};
instances.push(NamedInstance {
subfamily_name_id,
flags,
coords,
post_script_name_id,
});
}
Ok(Self { axes, instances })
}
pub fn axes(&self) -> &[VariationAxis] {
&self.axes
}
pub fn instances(&self) -> &[NamedInstance] {
&self.instances
}
pub fn axis_count(&self) -> usize {
self.axes.len()
}
}
#[inline]
fn fixed_to_f32(raw: i32) -> f32 {
raw as f32 / 65536.0
}
#[cfg(test)]
mod tests {
use super::*;
fn build_one_axis(min: f32, def: f32, max: f32) -> Vec<u8> {
let mut b = vec![0u8; 16 + 20];
b[0..2].copy_from_slice(&1u16.to_be_bytes()); b[2..4].copy_from_slice(&0u16.to_be_bytes()); b[4..6].copy_from_slice(&16u16.to_be_bytes()); b[6..8].copy_from_slice(&2u16.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..14].copy_from_slice(&0u16.to_be_bytes()); b[14..16].copy_from_slice(&8u16.to_be_bytes()); let rec = &mut b[16..36];
rec[0..4].copy_from_slice(b"wght");
rec[4..8].copy_from_slice(&((min * 65536.0) as i32).to_be_bytes());
rec[8..12].copy_from_slice(&((def * 65536.0) as i32).to_be_bytes());
rec[12..16].copy_from_slice(&((max * 65536.0) as i32).to_be_bytes());
rec[16..18].copy_from_slice(&0u16.to_be_bytes()); rec[18..20].copy_from_slice(&256u16.to_be_bytes()); b
}
#[test]
fn fvar_parses_wght_axis_min_default_max() {
let raw = build_one_axis(100.0, 400.0, 900.0);
let f = FvarTable::parse(&raw).expect("parse fvar");
assert_eq!(f.axes().len(), 1);
let a = &f.axes()[0];
assert_eq!(&a.tag, b"wght");
assert_eq!(a.min, 100.0);
assert_eq!(a.default, 400.0);
assert_eq!(a.max, 900.0);
assert_eq!(a.name_id, 256);
assert!(!a.is_hidden());
assert!(f.instances().is_empty());
}
#[test]
fn fvar_rejects_disordered_min_default_max() {
let raw = build_one_axis(900.0, 400.0, 100.0);
assert!(matches!(
FvarTable::parse(&raw),
Err(Error::BadStructure(_))
));
}
#[test]
fn fvar_parses_named_instance() {
let mut b = vec![0u8; 16 + 20 + 12];
b[0..2].copy_from_slice(&1u16.to_be_bytes());
b[4..6].copy_from_slice(&16u16.to_be_bytes());
b[6..8].copy_from_slice(&2u16.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..14].copy_from_slice(&1u16.to_be_bytes());
b[14..16].copy_from_slice(&8u16.to_be_bytes()); let rec = &mut b[16..36];
rec[0..4].copy_from_slice(b"wght");
rec[4..8].copy_from_slice(&(100i32 << 16).to_be_bytes());
rec[8..12].copy_from_slice(&(400i32 << 16).to_be_bytes());
rec[12..16].copy_from_slice(&(900i32 << 16).to_be_bytes());
rec[18..20].copy_from_slice(&256u16.to_be_bytes());
let inst = &mut b[36..44];
inst[0..2].copy_from_slice(&257u16.to_be_bytes()); inst[2..4].copy_from_slice(&0u16.to_be_bytes());
inst[4..8].copy_from_slice(&(700i32 << 16).to_be_bytes());
let f = FvarTable::parse(&b).expect("parse");
assert_eq!(f.instances().len(), 1);
let i = &f.instances()[0];
assert_eq!(i.subfamily_name_id, 257);
assert_eq!(i.coords, vec![700.0]);
assert!(i.post_script_name_id.is_none());
}
#[test]
fn fvar_rejects_short_header() {
let b = vec![0u8; 8];
assert!(matches!(FvarTable::parse(&b), Err(Error::UnexpectedEof)));
}
}