ttf_parser/tables/
stat.rs

1//! A [Style Attributes Table](https://docs.microsoft.com/en-us/typography/opentype/spec/stat) implementation.
2
3use crate::{
4    parser::{Offset, Offset16, Offset32, Stream},
5    Fixed, FromData, LazyArray16, Tag,
6};
7
8/// Axis-value pairing for [`AxisValueSubtableFormat4`].
9#[derive(Clone, Copy, Debug)]
10pub struct AxisValue {
11    /// Zero-based index into [`Table::axes`].
12    pub axis_index: u16,
13    /// Numeric value for this axis.
14    pub value: Fixed,
15}
16
17impl FromData for AxisValue {
18    const SIZE: usize = 6;
19
20    fn parse(data: &[u8]) -> Option<Self> {
21        let mut s = Stream::new(data);
22        let axis_index = s.read::<u16>()?;
23        let value = s.read::<Fixed>()?;
24
25        Some(AxisValue { axis_index, value })
26    }
27}
28
29/// Iterator over axis value subtables.
30#[derive(Clone, Debug)]
31pub struct AxisValueSubtables<'a> {
32    data: Stream<'a>,
33    start: Offset32,
34    offsets: LazyArray16<'a, Offset16>,
35    index: u16,
36    version: u32,
37}
38
39impl<'a> Iterator for AxisValueSubtables<'a> {
40    type Item = AxisValueSubtable<'a>;
41
42    #[inline]
43    fn next(&mut self) -> Option<Self::Item> {
44        if self.index >= self.offsets.len() {
45            return None;
46        }
47
48        let mut s = Stream::new_at(
49            self.data.tail()?,
50            self.offsets.get(self.index)?.to_usize() + self.start.to_usize(),
51        )?;
52        self.index += 1;
53
54        let format_variant = s.read::<u16>()?;
55
56        let value = match format_variant {
57            1 => {
58                let value = s.read::<AxisValueSubtableFormat1>()?;
59                Self::Item::Format1(value)
60            }
61            2 => {
62                let value = s.read::<AxisValueSubtableFormat2>()?;
63                Self::Item::Format2(value)
64            }
65            3 => {
66                let value = s.read::<AxisValueSubtableFormat3>()?;
67                Self::Item::Format3(value)
68            }
69            4 => {
70                // Format 4 tables didn't exist until v1.2.
71                if self.version < 0x00010002 {
72                    return None;
73                }
74
75                let value = AxisValueSubtableFormat4::parse(s.tail()?)?;
76                Self::Item::Format4(value)
77            }
78            _ => return None,
79        };
80
81        Some(value)
82    }
83}
84
85/// The [axis record](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-records) struct provides information about a single design axis.
86#[derive(Clone, Copy, Debug)]
87pub struct AxisRecord {
88    /// Axis tag.
89    pub tag: Tag,
90    /// The name ID for entries in the 'name' table that provide a display string for this axis.
91    pub name_id: u16,
92    /// Sort order for e.g. composing font family or face names.
93    pub ordering: u16,
94}
95
96impl FromData for AxisRecord {
97    const SIZE: usize = 8;
98
99    #[inline]
100    fn parse(data: &[u8]) -> Option<Self> {
101        let mut s = Stream::new(data);
102        Some(AxisRecord {
103            tag: s.read::<Tag>()?,
104            name_id: s.read::<u16>()?,
105            ordering: s.read::<u16>()?,
106        })
107    }
108}
109
110/// [Flags](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#flags) for [`AxisValueSubtable`].
111#[derive(Clone, Copy)]
112pub struct AxisValueFlags(u16);
113
114#[rustfmt::skip]
115impl AxisValueFlags {
116    /// If set, this value also applies to older versions of this font.
117    #[inline] pub fn older_sibling_attribute(self) -> bool { self.0 & (1 << 0) != 0 }
118
119    /// If set, this value is the normal (a.k.a. "regular") value for the font family.
120    #[inline] pub fn elidable(self) -> bool { self.0 & (1 << 1) != 0 }
121}
122
123impl core::fmt::Debug for AxisValueFlags {
124    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125        let mut dbg = f.debug_set();
126
127        if self.older_sibling_attribute() {
128            dbg.entry(&"OLDER_SIBLING_FONT_ATTRIBUTE");
129        }
130        if self.elidable() {
131            dbg.entry(&"ELIDABLE_AXIS_VALUE_NAME");
132        }
133
134        dbg.finish()
135    }
136}
137
138/// Axis value subtable [format 1](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1).
139#[derive(Clone, Copy, Debug)]
140pub struct AxisValueSubtableFormat1 {
141    /// Zero-based index into [`Table::axes`].
142    pub axis_index: u16,
143    /// Flags for [`AxisValueSubtable`].
144    pub flags: AxisValueFlags,
145    /// The name ID of the display string.
146    pub value_name_id: u16,
147    /// Numeric value for this record.
148    pub value: Fixed,
149}
150
151impl FromData for AxisValueSubtableFormat1 {
152    const SIZE: usize = 10;
153
154    #[inline]
155    fn parse(data: &[u8]) -> Option<Self> {
156        let mut s = Stream::new(data);
157        Some(AxisValueSubtableFormat1 {
158            axis_index: s.read::<u16>()?,
159            flags: AxisValueFlags(s.read::<u16>()?),
160            value_name_id: s.read::<u16>()?,
161            value: s.read::<Fixed>()?,
162        })
163    }
164}
165
166/// Axis value subtable [format 2](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-2).
167#[derive(Clone, Copy, Debug)]
168pub struct AxisValueSubtableFormat2 {
169    /// Zero-based index into [`Table::axes`].
170    pub axis_index: u16,
171    /// Flags for [`AxisValueSubtable`].
172    pub flags: AxisValueFlags,
173    /// The name ID of the display string.
174    pub value_name_id: u16,
175    /// Nominal numeric value for this record.
176    pub nominal_value: Fixed,
177    /// The minimum value for this record.
178    pub range_min_value: Fixed,
179    /// The maximum value for this record.
180    pub range_max_value: Fixed,
181}
182
183impl FromData for AxisValueSubtableFormat2 {
184    const SIZE: usize = 18;
185
186    #[inline]
187    fn parse(data: &[u8]) -> Option<Self> {
188        let mut s = Stream::new(data);
189        Some(AxisValueSubtableFormat2 {
190            axis_index: s.read::<u16>()?,
191            flags: AxisValueFlags(s.read::<u16>()?),
192            value_name_id: s.read::<u16>()?,
193            nominal_value: s.read::<Fixed>()?,
194            range_min_value: s.read::<Fixed>()?,
195            range_max_value: s.read::<Fixed>()?,
196        })
197    }
198}
199
200/// Axis value subtable [format 3](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-3).
201#[derive(Clone, Copy, Debug)]
202pub struct AxisValueSubtableFormat3 {
203    /// Zero-based index into [`Table::axes`].
204    pub axis_index: u16,
205    /// Flags for [`AxisValueSubtable`].
206    pub flags: AxisValueFlags,
207    /// The name ID of the display string.
208    pub value_name_id: u16,
209    /// Numeric value for this record.
210    pub value: Fixed,
211    /// Numeric value for a style-linked mapping.
212    pub linked_value: Fixed,
213}
214
215impl FromData for AxisValueSubtableFormat3 {
216    const SIZE: usize = 14;
217
218    #[inline]
219    fn parse(data: &[u8]) -> Option<Self> {
220        let mut s = Stream::new(data);
221        Some(AxisValueSubtableFormat3 {
222            axis_index: s.read::<u16>()?,
223            flags: AxisValueFlags(s.read::<u16>()?),
224            value_name_id: s.read::<u16>()?,
225            value: s.read::<Fixed>()?,
226            linked_value: s.read::<Fixed>()?,
227        })
228    }
229}
230
231/// Axis value subtable [format 4](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4).
232#[derive(Clone, Copy, Debug)]
233pub struct AxisValueSubtableFormat4<'a> {
234    /// Flags for [`AxisValueSubtable`].
235    pub flags: AxisValueFlags,
236    /// The name ID of the display string.
237    pub value_name_id: u16,
238    /// List of axis-value pairings.
239    pub values: LazyArray16<'a, AxisValue>,
240}
241
242impl<'a> AxisValueSubtableFormat4<'a> {
243    fn parse(data: &'a [u8]) -> Option<Self> {
244        let mut s = Stream::new(data);
245        let axis_count = s.read::<u16>()?;
246        let flags = AxisValueFlags(s.read::<u16>()?);
247        let value_name_id = s.read::<u16>()?;
248        let values = s.read_array16::<AxisValue>(axis_count)?;
249
250        Some(AxisValueSubtableFormat4 {
251            flags,
252            value_name_id,
253            values,
254        })
255    }
256}
257
258/// An [axis value subtable](https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-tables).
259#[allow(missing_docs)]
260#[derive(Clone, Copy, Debug)]
261pub enum AxisValueSubtable<'a> {
262    Format1(AxisValueSubtableFormat1),
263    Format2(AxisValueSubtableFormat2),
264    Format3(AxisValueSubtableFormat3),
265    Format4(AxisValueSubtableFormat4<'a>),
266}
267
268impl<'a> AxisValueSubtable<'a> {
269    /// Returns the value from an axis value subtable.
270    ///
271    /// For formats 1 and 3 the value is returned, for formats 2 and 4 `None` is returned as there
272    /// is no single value associated with those formats.
273    pub fn value(&self) -> Option<Fixed> {
274        match self {
275            Self::Format1(AxisValueSubtableFormat1 { value, .. })
276            | Self::Format3(AxisValueSubtableFormat3 { value, .. }) => Some(*value),
277            _ => None,
278        }
279    }
280
281    /// Returns `true` if the axis subtable either is the value or is a range that contains the
282    /// value passed in as an argument.
283    ///
284    /// Note: this will always return false for format 4 subtables as they may contain multiple
285    /// axes.
286    pub fn contains(&self, value: Fixed) -> bool {
287        if let Some(subtable_value) = self.value() {
288            if subtable_value.0 == value.0 {
289                return true;
290            }
291        }
292
293        if let Self::Format2(AxisValueSubtableFormat2 {
294            range_min_value,
295            range_max_value,
296            ..
297        }) = self
298        {
299            // core::ops::Range doesn't work here because Fixed doesn't implement
300            // the required comparison traits.
301            if value.0 >= range_min_value.0 && value.0 < range_max_value.0 {
302                return true;
303            }
304        }
305
306        false
307    }
308
309    /// Returns the associated name ID.
310    pub fn name_id(&self) -> u16 {
311        match self {
312            Self::Format1(AxisValueSubtableFormat1 { value_name_id, .. })
313            | Self::Format2(AxisValueSubtableFormat2 { value_name_id, .. })
314            | Self::Format3(AxisValueSubtableFormat3 { value_name_id, .. })
315            | Self::Format4(AxisValueSubtableFormat4 { value_name_id, .. }) => *value_name_id,
316        }
317    }
318
319    #[inline]
320    fn flags(&self) -> AxisValueFlags {
321        match self {
322            Self::Format1(AxisValueSubtableFormat1 { flags, .. })
323            | Self::Format2(AxisValueSubtableFormat2 { flags, .. })
324            | Self::Format3(AxisValueSubtableFormat3 { flags, .. })
325            | Self::Format4(AxisValueSubtableFormat4 { flags, .. }) => *flags,
326        }
327    }
328
329    /// Returns `true` if the axis subtable has the `ELIDABLE_AXIS_VALUE_NAME` flag set.
330    pub fn is_elidable(&self) -> bool {
331        self.flags().elidable()
332    }
333
334    /// Returns `true` if the axis subtable has the `OLDER_SIBLING_FONT_ATTRIBUTE` flag set.
335    pub fn is_older_sibling(&self) -> bool {
336        self.flags().older_sibling_attribute()
337    }
338}
339
340/// A [Style Attributes Table](https://docs.microsoft.com/en-us/typography/opentype/spec/stat).
341#[derive(Clone, Copy, Debug)]
342pub struct Table<'a> {
343    /// List of axes
344    pub axes: LazyArray16<'a, AxisRecord>,
345    /// Fallback name when everything can be elided.
346    pub fallback_name_id: Option<u16>,
347    version: u32,
348    data: &'a [u8],
349    value_lookup_start: Offset32,
350    value_offsets: LazyArray16<'a, Offset16>,
351}
352
353impl<'a> Table<'a> {
354    /// Parses a table from raw data.
355    pub fn parse(data: &'a [u8]) -> Option<Self> {
356        let mut s = Stream::new(data);
357        let version = s.read::<u32>()?;
358
359        // Supported versions are:
360        // - 1.0
361        // - 1.1 adds elidedFallbackNameId
362        // - 1.2 adds format 4 axis value table
363        if !(version == 0x00010000 || version == 0x00010001 || version == 0x00010002) {
364            return None;
365        }
366
367        let _axis_size = s.read::<u16>()?;
368        let axis_count = s.read::<u16>()?;
369        let axis_offset = s.read::<Offset32>()?.to_usize();
370
371        let value_count = s.read::<u16>()?;
372        let value_lookup_start = s.read::<Offset32>()?;
373
374        let fallback_name_id = if version >= 0x00010001 {
375            // If version >= 1.1 the field is required
376            Some(s.read::<u16>()?)
377        } else {
378            None
379        };
380
381        let mut s = Stream::new_at(data, axis_offset)?;
382        let axes = s.read_array16::<AxisRecord>(axis_count)?;
383
384        let mut s = Stream::new_at(data, value_lookup_start.to_usize())?;
385        let value_offsets = s.read_array16::<Offset16>(value_count)?;
386
387        Some(Self {
388            axes,
389            data,
390            value_lookup_start,
391            value_offsets,
392            fallback_name_id,
393            version,
394        })
395    }
396
397    /// Returns an iterator over the collection of axis value tables.
398    pub fn subtables(&self) -> AxisValueSubtables<'a> {
399        AxisValueSubtables {
400            data: Stream::new(self.data),
401            start: self.value_lookup_start,
402            offsets: self.value_offsets,
403            index: 0,
404            version: self.version,
405        }
406    }
407
408    /// Returns the first matching subtable for a given axis.
409    ///
410    /// If no match value is given the first subtable for the axis is returned. If a match value is
411    /// given, the first subtable for the axis where the value matches is returned. A value matches
412    /// if it is equal to the subtable's value or contained within the range defined by the
413    /// subtable. If no matches are found `None` is returned. Typically a match value is not
414    /// specified for non-variable fonts as multiple subtables for a given axis ought not exist. For
415    /// variable fonts a non-`None` match value should be specified as multiple records for each of
416    /// the variation axes exist.
417    ///
418    /// Note: Format 4 subtables are explicitly ignored in this function.
419    pub fn subtable_for_axis(
420        &self,
421        axis: Tag,
422        match_value: Option<Fixed>,
423    ) -> Option<AxisValueSubtable> {
424        for subtable in self.subtables() {
425            match subtable {
426                AxisValueSubtable::Format1(AxisValueSubtableFormat1 {
427                    axis_index, value, ..
428                })
429                | AxisValueSubtable::Format3(AxisValueSubtableFormat3 {
430                    axis_index, value, ..
431                }) => {
432                    if self.axes.get(axis_index)?.tag != axis {
433                        continue;
434                    }
435
436                    match match_value {
437                        Some(match_value) => {
438                            if match_value.0 == value.0 {
439                                return Some(subtable);
440                            }
441                        }
442                        None => return Some(subtable),
443                    }
444                }
445                AxisValueSubtable::Format2(AxisValueSubtableFormat2 {
446                    axis_index,
447                    range_min_value,
448                    range_max_value,
449                    ..
450                }) => {
451                    if self.axes.get(axis_index)?.tag == axis {
452                        continue;
453                    }
454
455                    match match_value {
456                        Some(match_value) => {
457                            if match_value.0 >= range_min_value.0
458                                && match_value.0 < range_max_value.0
459                            {
460                                return Some(subtable);
461                            }
462                        }
463                        None => return Some(subtable),
464                    }
465                }
466                AxisValueSubtable::Format4(_) => {
467                    // A query that's intended to search format 4 subtables can be performed
468                    // across multiple axes. A separate function that takes a collection of
469                    // axis-value pairs is more suitable than this.
470                    continue;
471                }
472            }
473        }
474
475        None
476    }
477}