diff --git a/src/tables/fvar.rs b/src/tables/fvar.rs index 1e8d17e..c463c82 100644 --- a/src/tables/fvar.rs +++ b/src/tables/fvar.rs @@ -62,12 +62,180 @@ impl VariationAxis { } } +/// An [instance record](https://docs.microsoft.com/en-us/typography/opentype/spec/fvar#instancerecord). +#[derive(Clone, Copy, Debug)] +pub struct Instance<'a> { + /// The name ID for entries in the 'name' table that provide subfamily names for this instance. + pub subfamily_name_id: u16, + /// Reserved for future use — set to 0. + pub flags: u16, + /// The coordinate array for this instance (length = axisCount). + pub coordinates: LazyArray16<'a, Fixed>, + /// The name ID for entries in the 'name' table that provide PostScript names for this instance. + pub post_script_name_id: Option, +} + +impl<'a> Instance<'a> { + /// Returns an iterator over the coordinate values as `f32`. + /// + /// This is a convenience method that converts the internal `Fixed` representation + /// to floating-point values. + #[inline] + pub fn coordinates_f32(&self) -> impl Iterator + 'a { + self.coordinates.into_iter().map(|fixed| fixed.0) + } + + #[inline] + fn parse_from_record( + record: &'a [u8], + axis_count: u16, + has_post_script_name_id: bool, + ) -> Option { + let mut s = Stream::new(record); + let subfamily_name_id = s.read::()?; + let flags = s.read::()?; + let coordinates = s.read_array16::(axis_count)?; + let post_script_name_id = if has_post_script_name_id { + Some(s.read::()?) + } else { + None + }; + Some(Self { + subfamily_name_id, + flags, + coordinates, + post_script_name_id, + }) + } +} + +/// A view over the `InstanceRecord` array. +#[derive(Clone, Copy, Debug)] +pub struct Instances<'a> { + data: &'a [u8], + record_len: u16, + axis_count: u16, + count: u16, +} + +/// An iterator over [instance records](Instance). +#[derive(Clone, Copy, Debug)] +pub struct InstancesIter<'a> { + instances: Instances<'a>, + index: u16, +} + +impl<'a> Iterator for InstancesIter<'a> { + type Item = Instance<'a>; + + fn next(&mut self) -> Option { + if self.index < self.instances.count { + let instance = self.instances.get(self.index)?; + self.index += 1; + Some(instance) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = (self.instances.count - self.index) as usize; + (remaining, Some(remaining)) + } +} + +impl<'a> ExactSizeIterator for InstancesIter<'a> { + fn len(&self) -> usize { + (self.instances.count - self.index) as usize + } +} + +impl<'a> IntoIterator for Instances<'a> { + type Item = Instance<'a>; + type IntoIter = InstancesIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + InstancesIter { + instances: self, + index: 0, + } + } +} + +impl<'a> Instances<'a> { + /// Returns an iterator over the instance records. + /// + /// # Examples + /// + /// ``` + /// # use ttf_parser::fvar::Table; + /// # let data = &[/* font data */]; + /// # if let Some(table) = Table::parse(data) { + /// for instance in table.instances.iter() { + /// println!("Subfamily name ID: {}", instance.subfamily_name_id); + /// } + /// # } + /// ``` + #[inline] + pub fn iter(&self) -> InstancesIter<'a> { + (*self).into_iter() + } + + #[inline] + fn new(data: &'a [u8], record_len: u16, axis_count: u16, count: u16) -> Self { + Self { + data, + record_len, + axis_count, + count, + } + } + + /// Total number of instance records. + #[inline] + pub fn len(&self) -> u16 { + self.count + } + + /// Returns true when there are no instance records. + #[inline] + pub fn is_empty(&self) -> bool { + self.count == 0 + } + + /// Returns `true` when the `postScriptNameID` field is present in records. + #[inline] + pub fn has_post_script_name_id(&self) -> bool { + // The base size is 4 bytes (subfamilyNameID + flags) + 4 bytes per axis coordinate. + // If record_len is at least base + 2, the optional postScriptNameID field is present. + let axis_count = usize::from(self.axis_count); + let base = 4 + 4 * axis_count; + usize::from(self.record_len) >= base + 2 + } + + /// Returns the instance at the given index. + /// + /// Returns `None` if the index is out of bounds. + pub fn get(&self, index: u16) -> Option> { + if index >= self.count { + return None; + } + let len = usize::from(self.record_len); + let start = usize::from(index) * len; + let end = start + len; + let record = self.data.get(start..end)?; + Instance::parse_from_record(record, self.axis_count, self.has_post_script_name_id()) + } +} + /// A [Font Variations Table]( /// https://docs.microsoft.com/en-us/typography/opentype/spec/fvar). #[derive(Clone, Copy, Debug)] pub struct Table<'a> { /// A list of variation axes. pub axes: LazyArray16<'a, VariationAxis>, + /// A list of instance records. + pub instances: Instances<'a>, } impl<'a> Table<'a> { @@ -82,6 +250,13 @@ impl<'a> Table<'a> { let axes_array_offset = s.read::()?; s.skip::(); // reserved let axis_count = s.read::()?; + let axis_size = s.read::()?; + let instance_count = s.read::()?; + let instance_size = s.read::()?; + + if axis_size as usize != VariationAxis::SIZE { + return None; + } // 'If axisCount is zero, then the font is not functional as a variable font, // and must be treated as a non-variable font; @@ -91,6 +266,24 @@ impl<'a> Table<'a> { let mut s = Stream::new_at(data, axes_array_offset.to_usize())?; let axes = s.read_array16::(axis_count.get())?; - Some(Table { axes }) + // Instance records follow the axes array immediately. + let instances_offset = axes_array_offset + .to_usize() + .checked_add(usize::from(axis_count.get()) * VariationAxis::SIZE)?; + + // Validate instance record size: must be base or base + 2 (for psNameID). + let base = 4usize.checked_add(4usize.checked_mul(usize::from(axis_count.get()))?)?; + let inst_size = usize::from(instance_size); + if inst_size < base { + return None; + } + + let total_instances_len = + usize::from(instance_count).checked_mul(usize::from(instance_size))?; + let mut inst_stream = Stream::new_at(data, instances_offset)?; + let inst_data = inst_stream.read_bytes(total_instances_len)?; + let instances = Instances::new(inst_data, instance_size, axis_count.get(), instance_count); + + Some(Table { axes, instances }) } }