From 9a5549cd331dd74f55d10aab07c0e2043d4ece32 Mon Sep 17 00:00:00 2001 From: inferiorhumanorgans Date: Thu, 22 Aug 2024 00:48:49 -0700 Subject: [PATCH] Add support for the STAT table. (#166) --- README.md | 1 + examples/font-info.rs | 12 ++ src/lib.rs | 7 +- src/tables/mod.rs | 1 + src/tables/stat.rs | 477 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 src/tables/stat.rs diff --git a/README.md b/README.md index 7e280d2d..a3c5604e 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ There are roughly three types of TrueType tables: | `OS/2` table | ✓ | ✓ | | | `post` table | ✓ | ✓ | | | `sbix` table | ~ (PNG only) | ~ (PNG only) | | +| `STAT` table | ✓ | | | | `SVG ` table | ✓ | ✓ | ✓ | | `trak` table | ✓ | | | | `vhea` table | ✓ | ✓ | | diff --git a/examples/font-info.rs b/examples/font-info.rs index 1697eae3..7c0d4226 100644 --- a/examples/font-info.rs +++ b/examples/font-info.rs @@ -84,6 +84,18 @@ fn main() { } } + if let Some(stat) = face.tables().stat { + println!("Style attributes:"); + + println!(" Axes:"); + for axis in stat.axes { + println!(" {}", axis.tag); + if let Some(subtable) = stat.subtable_for_axis(axis.tag, None) { + println!(" {:?}", subtable) + } + } + } + println!("Elapsed: {}us", now.elapsed().as_micros()); } diff --git a/src/lib.rs b/src/lib.rs index dcac033a..f0d5f36d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,8 @@ pub use tables::{ankr, feat, kerx, morx, trak}; pub use tables::{avar, cff2, fvar, gvar, hvar, mvar, vvar}; pub use tables::{cbdt, cblc, cff1 as cff, vhea}; pub use tables::{ - cmap, colr, cpal, glyf, head, hhea, hmtx, kern, loca, maxp, name, os2, post, sbix, svg, vorg, + cmap, colr, cpal, glyf, head, hhea, hmtx, kern, loca, maxp, name, os2, post, sbix, stat, svg, + vorg, }; #[cfg(feature = "opentype-layout")] pub use tables::{gdef, gpos, gsub, math}; @@ -938,6 +939,7 @@ pub struct RawFaceTables<'a> { pub os2: Option<&'a [u8]>, pub post: Option<&'a [u8]>, pub sbix: Option<&'a [u8]>, + pub stat: Option<&'a [u8]>, pub svg: Option<&'a [u8]>, pub vhea: Option<&'a [u8]>, pub vmtx: Option<&'a [u8]>, @@ -1008,6 +1010,7 @@ pub struct FaceTables<'a> { pub os2: Option>, pub post: Option>, pub sbix: Option>, + pub stat: Option>, pub svg: Option>, pub vhea: Option, pub vmtx: Option>, @@ -1189,6 +1192,7 @@ impl<'a> Face<'a> { b"name" => tables.name = table_data, b"post" => tables.post = table_data, b"sbix" => tables.sbix = table_data, + b"STAT" => tables.stat = table_data, #[cfg(feature = "apple-layout")] b"trak" => tables.trak = table_data, b"vhea" => tables.vhea = table_data, @@ -1305,6 +1309,7 @@ impl<'a> Face<'a> { sbix: raw_tables .sbix .and_then(|data| sbix::Table::parse(maxp.number_of_glyphs, data)), + stat: raw_tables.stat.and_then(stat::Table::parse), svg: raw_tables.svg.and_then(svg::Table::parse), vhea: raw_tables.vhea.and_then(vhea::Table::parse), vmtx, diff --git a/src/tables/mod.rs b/src/tables/mod.rs index d00548c4..9079e052 100644 --- a/src/tables/mod.rs +++ b/src/tables/mod.rs @@ -15,6 +15,7 @@ pub mod name; pub mod os2; pub mod post; pub mod sbix; +pub mod stat; pub mod svg; pub mod vhea; pub mod vorg; diff --git a/src/tables/stat.rs b/src/tables/stat.rs new file mode 100644 index 00000000..77553428 --- /dev/null +++ b/src/tables/stat.rs @@ -0,0 +1,477 @@ +//! A [Style Attributes Table](https://docs.microsoft.com/en-us/typography/opentype/spec/stat) implementation. + +use crate::{ + parser::{Offset, Offset16, Offset32, Stream}, + Fixed, FromData, LazyArray16, Tag, +}; + +/// Axis-value pairing for [`AxisValueSubtableFormat4`]. +#[derive(Clone, Copy, Debug)] +pub struct AxisValue { + /// Zero-based index into [`Table::axes`]. + pub axis_index: u16, + /// Numeric value for this axis. + pub value: Fixed, +} + +impl FromData for AxisValue { + const SIZE: usize = 6; + + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + let axis_index = s.read::()?; + let value = s.read::()?; + + Some(AxisValue { axis_index, value }) + } +} + +/// Iterator over axis value subtables. +#[derive(Clone, Debug)] +pub struct AxisValueSubtables<'a> { + data: Stream<'a>, + start: Offset32, + offsets: LazyArray16<'a, Offset16>, + index: u16, + version: u32, +} + +impl<'a> Iterator for AxisValueSubtables<'a> { + type Item = AxisValueSubtable<'a>; + + #[inline] + fn next(&mut self) -> Option { + if self.index >= self.offsets.len() { + return None; + } + + let mut s = Stream::new_at( + self.data.tail()?, + self.offsets.get(self.index)?.to_usize() + self.start.to_usize(), + )?; + self.index += 1; + + let format_variant = s.read::()?; + + let value = match format_variant { + 1 => { + let value = s.read::()?; + Self::Item::Format1(value) + } + 2 => { + let value = s.read::()?; + Self::Item::Format2(value) + } + 3 => { + let value = s.read::()?; + Self::Item::Format3(value) + } + 4 => { + // Format 4 tables didn't exist until v1.2. + if self.version < 0x00010002 { + return None; + } + + let value = AxisValueSubtableFormat4::parse(s.tail()?)?; + Self::Item::Format4(value) + } + _ => return None, + }; + + Some(value) + } +} + +/// The [axis record](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-records) struct provides information about a single design axis. +#[derive(Clone, Copy, Debug)] +pub struct AxisRecord { + /// Axis tag. + pub tag: Tag, + /// The name ID for entries in the 'name' table that provide a display string for this axis. + pub name_id: u16, + /// Sort order for e.g. composing font family or face names. + pub ordering: u16, +} + +impl FromData for AxisRecord { + const SIZE: usize = 8; + + #[inline] + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(AxisRecord { + tag: s.read::()?, + name_id: s.read::()?, + ordering: s.read::()?, + }) + } +} + +/// [Flags](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#flags) for [`AxisValueSubtable`]. +#[derive(Clone, Copy)] +pub struct AxisValueFlags(u16); + +#[rustfmt::skip] +impl AxisValueFlags { + /// If set, this value also applies to older versions of this font. + #[inline] pub fn older_sibling_attribute(self) -> bool { self.0 & (1 << 0) != 0 } + + /// If set, this value is the normal (a.k.a. "regular") value for the font family. + #[inline] pub fn elidable(self) -> bool { self.0 & (1 << 1) != 0 } +} + +impl core::fmt::Debug for AxisValueFlags { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut dbg = f.debug_set(); + + if self.older_sibling_attribute() { + dbg.entry(&"OLDER_SIBLING_FONT_ATTRIBUTE"); + } + if self.elidable() { + dbg.entry(&"ELIDABLE_AXIS_VALUE_NAME"); + } + + dbg.finish() + } +} + +/// Axis value subtable [format 1](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1). +#[derive(Clone, Copy, Debug)] +pub struct AxisValueSubtableFormat1 { + /// Zero-based index into [`Table::axes`]. + pub axis_index: u16, + /// Flags for [`AxisValueSubtable`]. + pub flags: AxisValueFlags, + /// The name ID of the display string. + pub value_name_id: u16, + /// Numeric value for this record. + pub value: Fixed, +} + +impl FromData for AxisValueSubtableFormat1 { + const SIZE: usize = 10; + + #[inline] + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(AxisValueSubtableFormat1 { + axis_index: s.read::()?, + flags: AxisValueFlags(s.read::()?), + value_name_id: s.read::()?, + value: s.read::()?, + }) + } +} + +/// Axis value subtable [format 2](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-2). +#[derive(Clone, Copy, Debug)] +pub struct AxisValueSubtableFormat2 { + /// Zero-based index into [`Table::axes`]. + pub axis_index: u16, + /// Flags for [`AxisValueSubtable`]. + pub flags: AxisValueFlags, + /// The name ID of the display string. + pub value_name_id: u16, + /// Nominal numeric value for this record. + pub nominal_value: Fixed, + /// The minimum value for this record. + pub range_min_value: Fixed, + /// The maximum value for this record. + pub range_max_value: Fixed, +} + +impl FromData for AxisValueSubtableFormat2 { + const SIZE: usize = 18; + + #[inline] + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(AxisValueSubtableFormat2 { + axis_index: s.read::()?, + flags: AxisValueFlags(s.read::()?), + value_name_id: s.read::()?, + nominal_value: s.read::()?, + range_min_value: s.read::()?, + range_max_value: s.read::()?, + }) + } +} + +/// Axis value subtable [format 3](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-3). +#[derive(Clone, Copy, Debug)] +pub struct AxisValueSubtableFormat3 { + /// Zero-based index into [`Table::axes`]. + pub axis_index: u16, + /// Flags for [`AxisValueSubtable`]. + pub flags: AxisValueFlags, + /// The name ID of the display string. + pub value_name_id: u16, + /// Numeric value for this record. + pub value: Fixed, + /// Numeric value for a style-linked mapping. + pub linked_value: Fixed, +} + +impl FromData for AxisValueSubtableFormat3 { + const SIZE: usize = 14; + + #[inline] + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(AxisValueSubtableFormat3 { + axis_index: s.read::()?, + flags: AxisValueFlags(s.read::()?), + value_name_id: s.read::()?, + value: s.read::()?, + linked_value: s.read::()?, + }) + } +} + +/// Axis value subtable [format 4](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4). +#[derive(Clone, Copy, Debug)] +pub struct AxisValueSubtableFormat4<'a> { + /// Flags for [`AxisValueSubtable`]. + pub flags: AxisValueFlags, + /// The name ID of the display string. + pub value_name_id: u16, + /// List of axis-value pairings. + pub values: LazyArray16<'a, AxisValue>, +} + +impl<'a> AxisValueSubtableFormat4<'a> { + fn parse(data: &'a [u8]) -> Option { + let mut s = Stream::new(data); + let axis_count = s.read::()?; + let flags = AxisValueFlags(s.read::()?); + let value_name_id = s.read::()?; + let values = s.read_array16::(axis_count)?; + + Some(AxisValueSubtableFormat4 { + flags, + value_name_id, + values, + }) + } +} + +/// An [axis value subtable](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-tables). +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug)] +pub enum AxisValueSubtable<'a> { + Format1(AxisValueSubtableFormat1), + Format2(AxisValueSubtableFormat2), + Format3(AxisValueSubtableFormat3), + Format4(AxisValueSubtableFormat4<'a>), +} + +impl<'a> AxisValueSubtable<'a> { + /// Returns the value from an axis value subtable. + /// + /// For formats 1 and 3 the value is returned, for formats 2 and 4 `None` is returned as there + /// is no single value associated with those formats. + pub fn value(&self) -> Option { + match self { + Self::Format1(AxisValueSubtableFormat1 { value, .. }) + | Self::Format3(AxisValueSubtableFormat3 { value, .. }) => Some(*value), + _ => None, + } + } + + /// Returns `true` if the axis subtable either is the value or is a range that contains the + /// value passed in as an argument. + /// + /// Note: this will always return false for format 4 subtables as they may contain multiple + /// axes. + pub fn contains(&self, value: Fixed) -> bool { + if let Some(subtable_value) = self.value() { + if subtable_value.0 == value.0 { + return true; + } + } + + if let Self::Format2(AxisValueSubtableFormat2 { + range_min_value, + range_max_value, + .. + }) = self + { + // core::ops::Range doesn't work here because Fixed doesn't implement + // the required comparison traits. + if value.0 >= range_min_value.0 && value.0 < range_max_value.0 { + return true; + } + } + + false + } + + /// Returns the associated name ID. + pub fn name_id(&self) -> u16 { + match self { + Self::Format1(AxisValueSubtableFormat1 { value_name_id, .. }) + | Self::Format2(AxisValueSubtableFormat2 { value_name_id, .. }) + | Self::Format3(AxisValueSubtableFormat3 { value_name_id, .. }) + | Self::Format4(AxisValueSubtableFormat4 { value_name_id, .. }) => *value_name_id, + } + } + + #[inline] + fn flags(&self) -> AxisValueFlags { + match self { + Self::Format1(AxisValueSubtableFormat1 { flags, .. }) + | Self::Format2(AxisValueSubtableFormat2 { flags, .. }) + | Self::Format3(AxisValueSubtableFormat3 { flags, .. }) + | Self::Format4(AxisValueSubtableFormat4 { flags, .. }) => *flags, + } + } + + /// Returns `true` if the axis subtable has the `ELIDABLE_AXIS_VALUE_NAME` flag set. + pub fn is_elidable(&self) -> bool { + self.flags().elidable() + } + + /// Returns `true` if the axis subtable has the `OLDER_SIBLING_FONT_ATTRIBUTE` flag set. + pub fn is_older_sibling(&self) -> bool { + self.flags().older_sibling_attribute() + } +} + +/// A [Style Attributes Table](https://docs.microsoft.com/en-us/typography/opentype/spec/stat). +#[derive(Clone, Copy, Debug)] +pub struct Table<'a> { + /// List of axes + pub axes: LazyArray16<'a, AxisRecord>, + /// Fallback name when everything can be elided. + pub fallback_name_id: Option, + version: u32, + data: &'a [u8], + value_lookup_start: Offset32, + value_offsets: LazyArray16<'a, Offset16>, +} + +impl<'a> Table<'a> { + /// Parses a table from raw data. + pub fn parse(data: &'a [u8]) -> Option { + let mut s = Stream::new(data); + let version = s.read::()?; + + // Supported versions are: + // - 1.0 + // - 1.1 adds elidedFallbackNameId + // - 1.2 adds format 4 axis value table + if !(version == 0x00010000 || version == 0x00010001 || version == 0x00010002) { + return None; + } + + let _axis_size = s.read::()?; + let axis_count = s.read::()?; + let axis_offset = s.read::()?.to_usize(); + + let value_count = s.read::()?; + let value_lookup_start = s.read::()?; + + let fallback_name_id = if version >= 0x00010001 { + // If version >= 1.1 the field is required + Some(s.read::()?) + } else { + None + }; + + let mut s = Stream::new_at(data, axis_offset)?; + let axes = s.read_array16::(axis_count)?; + + let mut s = Stream::new_at(data, value_lookup_start.to_usize())?; + let value_offsets = s.read_array16::(value_count)?; + + Some(Self { + axes, + data, + value_lookup_start, + value_offsets, + fallback_name_id, + version, + }) + } + + /// Returns an iterator over the collection of axis value tables. + pub fn subtables(&self) -> AxisValueSubtables<'a> { + AxisValueSubtables { + data: Stream::new(self.data), + start: self.value_lookup_start, + offsets: self.value_offsets, + index: 0, + version: self.version, + } + } + + /// Returns the first matching subtable for a given axis. + /// + /// If no match value is given the first subtable for the axis is returned. If a match value is + /// given, the first subtable for the axis where the value matches is returned. A value matches + /// if it is equal to the subtable's value or contained within the range defined by the + /// subtable. If no matches are found `None` is returned. Typically a match value is not + /// specified for non-variable fonts as multiple subtables for a given axis ought not exist. For + /// variable fonts a non-`None` match value should be specified as multiple records for each of + /// the variation axes exist. + /// + /// Note: Format 4 subtables are explicitly ignored in this function. + pub fn subtable_for_axis( + &self, + axis: Tag, + match_value: Option, + ) -> Option { + for subtable in self.subtables() { + match subtable { + AxisValueSubtable::Format1(AxisValueSubtableFormat1 { + axis_index, value, .. + }) + | AxisValueSubtable::Format3(AxisValueSubtableFormat3 { + axis_index, value, .. + }) => { + if self.axes.get(axis_index)?.tag != axis { + continue; + } + + match match_value { + Some(match_value) => { + if match_value.0 == value.0 { + return Some(subtable); + } + } + None => return Some(subtable), + } + } + AxisValueSubtable::Format2(AxisValueSubtableFormat2 { + axis_index, + range_min_value, + range_max_value, + .. + }) => { + if self.axes.get(axis_index)?.tag == axis { + continue; + } + + match match_value { + Some(match_value) => { + if match_value.0 >= range_min_value.0 + && match_value.0 < range_max_value.0 + { + return Some(subtable); + } + } + None => return Some(subtable), + } + } + AxisValueSubtable::Format4(_) => { + // A query that's intended to search format 4 subtables can be performed + // across multiple axes. A separate function that takes a collection of + // axis-value pairs is more sutable than this. + continue; + } + } + } + + None + } +}