allsorts_subset_browser/tables/variable_fonts/
stat.rs

1#![deny(missing_docs)]
2
3//! `STAT` Style Attributes Table
4//!
5//! The style attributes table describes design attributes that distinguish
6//! font-style variants within a font family. It also provides associations
7//! between those attributes and name elements that may be used to present font
8//! options within application user interfaces. This information is especially
9//! important for variable fonts, but also relevant for non-variable fonts.
10//!
11//! <https://learn.microsoft.com/en-us/typography/opentype/spec/stat>
12
13use std::fmt;
14
15use bitflags::bitflags;
16
17use crate::binary::read::{
18    ReadArray, ReadBinary, ReadBinaryDep, ReadCtxt, ReadFixedSizeDep, ReadFrom, ReadScope,
19    ReadUnchecked,
20};
21use crate::binary::{U16Be, U32Be};
22use crate::error::ParseError;
23use crate::tables::Fixed;
24use crate::tag::DisplayTag;
25use crate::{size, SafeFrom};
26
27/// `STAT` Style Attributes Table
28///
29/// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#style-attributes-header>
30pub struct StatTable<'a> {
31    /// Major version number of the style attributes table.
32    pub major_version: u16,
33    /// Minor version number of the style attributes table.
34    pub minor_version: u16,
35    /// The size in bytes of each axis record.
36    design_axis_size: u16,
37    /// The number of axis records.
38    ///
39    /// In a font with an `fvar` table, this value must be greater than or
40    /// equal to the axisCount value in the `fvar` table. In all fonts, must be
41    /// greater than zero if the number of axis value tables is greater than
42    /// zero.
43    design_axis_count: u16,
44    /// The design axes records.
45    design_axes_array: &'a [u8],
46    /// A read scope from the beginning of the axis offsets array.
47    ///
48    /// Used for reading the axis value tables.
49    axis_value_scope: ReadScope<'a>,
50    /// The array of offsets to the axis value tables.
51    axis_value_offsets: ReadArray<'a, U16Be>,
52    /// Name ID used as fallback when projection of names into a particular font
53    /// model produces a subfamily name containing only elidable elements.
54    pub elided_fallback_name_id: Option<u16>,
55}
56
57/// Information about a single design axis.
58///
59/// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-records>
60#[derive(Eq, PartialEq, Copy, Clone)]
61pub struct AxisRecord {
62    /// A tag identifying the axis of design variation.
63    pub axis_tag: u32,
64    /// The name ID for entries in the `name` table that provide a display
65    /// string for this axis.
66    pub axis_name_id: u16,
67    /// A value that applications can use to determine primary sorting of face
68    /// names, or for ordering of labels when composing family or face
69    /// names.
70    pub axis_ordering: u16,
71}
72
73/// Axis value table.
74///
75/// Axis value tables provide details regarding a specific style-attribute value
76/// on some specific axis of design variation, or a combination of
77/// design-variation axis values, and the relationship of those values to labels
78/// used as elements in subfamily names.
79///
80/// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-tables>
81#[derive(Debug)]
82pub enum AxisValueTable<'a> {
83    /// Format 1 axis value table: name associated with a value.
84    Format1(AxisValueTableFormat1),
85    /// Format 2 axis value table: name associated with a range of values.
86    Format2(AxisValueTableFormat2),
87    /// Format 3 axis value table: name associated with a value and style-linked
88    /// mapping.
89    Format3(AxisValueTableFormat3),
90    /// Format 4 axis value table: name associated with a value for each design
91    /// axis.
92    Format4(AxisValueTableFormat4<'a>),
93}
94
95/// Format 1 axis value table: name associated with a value.
96///
97/// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1>
98#[derive(Debug, Eq, PartialEq)]
99pub struct AxisValueTableFormat1 {
100    /// Zero-base index into the axis record array identifying the axis of
101    /// design variation to which the axis value table applies.
102    pub axis_index: u16,
103    /// Flags.
104    flags: AxisValueTableFlags,
105    /// The name ID for entries in the `name` table that provide a display
106    /// string for this attribute value.
107    value_name_id: u16,
108    /// A numeric value for this attribute value.
109    pub value: Fixed,
110}
111
112/// Format 2 axis value table: name associated with a range of values.
113///
114/// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-2>
115#[derive(Debug, Eq, PartialEq)]
116pub struct AxisValueTableFormat2 {
117    /// Zero-base index into the axis record array identifying the axis of
118    /// design variation to which the axis value table applies.
119    pub axis_index: u16,
120    /// Flags.
121    flags: AxisValueTableFlags,
122    /// The name ID for entries in the `name` table that provide a display
123    /// string for this attribute value.
124    value_name_id: u16,
125    /// A nominal numeric value for this attribute value.
126    pub nominal_value: Fixed,
127    /// The minimum value for a range associated with the specified name ID.
128    pub range_min_value: Fixed,
129    /// The maximum value for a range associated with the specified name ID.
130    pub range_max_value: Fixed,
131}
132
133/// Format 3 axis value table: name associated with a value and style-linked
134/// mapping.
135///
136/// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-3>
137#[derive(Debug, Eq, PartialEq)]
138pub struct AxisValueTableFormat3 {
139    /// Zero-base index into the axis record array identifying the axis of
140    /// design variation to which the axis value table applies.
141    pub axis_index: u16,
142    /// Flags.
143    flags: AxisValueTableFlags,
144    /// The name ID for entries in the `name` table that provide a display
145    /// string for this attribute value.
146    value_name_id: u16,
147    /// A numeric value for this attribute value.
148    pub value: Fixed,
149    /// The numeric value for a style-linked mapping from this value.
150    pub linked_value: Fixed,
151}
152
153/// Format 4 axis value table: name associated with a value for each design
154/// axis.
155///
156/// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4>
157#[derive(Debug)]
158pub struct AxisValueTableFormat4<'a> {
159    /// Flags.
160    flags: AxisValueTableFlags,
161    /// The name ID for entries in the `name` table that provide a display
162    /// string for this combination of axis values.
163    value_name_id: u16,
164    /// Array of AxisValue records that provide the combination of axis values,
165    /// one for each contributing axis.
166    pub axis_values: ReadArray<'a, AxisValue>,
167}
168
169/// An axis value record from a format 4 axis value table.
170#[derive(Debug, Copy, Clone)]
171pub struct AxisValue {
172    /// Zero-base index into the axis record array identifying the axis to which
173    /// this value applies.
174    pub axis_index: u16,
175    /// A numeric value for this attribute value.
176    pub value: Fixed,
177}
178
179bitflags! {
180    /// Flags for axis value tables.
181    ///
182    /// <https://learn.microsoft.com/en-us/typography/opentype/spec/stat#flags>
183    pub struct AxisValueTableFlags: u16 {
184        /// If set, this axis value table provides axis value information that is applicable to
185        /// other fonts within the same font family. This is used if the other fonts were released
186        /// earlier and did not include information about values for some axis. If newer versions
187        /// of the other fonts include the information themselves and are present, then this table
188        /// is ignored.
189        const OLDER_SIBLING_FONT_ATTRIBUTE = 0x0001;
190        /// If set, it indicates that the axis value represents the “normal” value for the axis and
191        /// may be omitted when composing name strings.
192        const ELIDABLE_AXIS_VALUE_NAME = 0x0002;
193        // 0xFFFC 	Reserved 	Reserved for future use — set to zero.
194    }
195}
196
197/// Boolean value to indicate to [StatTable::name_for_axis_value] whether names
198/// from tables with the `ELIDABLE_AXIS_VALUE_NAME` flag set should be included
199/// or excluded in the result.
200#[derive(Copy, Clone, Eq, PartialEq, Debug)]
201pub enum ElidableName {
202    /// Include elidable names
203    Include,
204    /// Exclude elidable names
205    Exclude,
206}
207
208impl<'a> StatTable<'a> {
209    /// Iterate over the design axes.
210    pub fn design_axes(&'a self) -> impl Iterator<Item = Result<AxisRecord, ParseError>> + '_ {
211        (0..usize::from(self.design_axis_count)).map(move |i| self.design_axis(i))
212    }
213
214    /// Retrieve the design axis at the supplied index.
215    pub fn design_axis(&self, index: usize) -> Result<AxisRecord, ParseError> {
216        let design_axis_size = usize::from(self.design_axis_size);
217        let offset = index * design_axis_size;
218        self.design_axes_array
219            .get(offset..(offset + design_axis_size))
220            .ok_or(ParseError::BadIndex)
221            .and_then(|data| ReadScope::new(data).read::<AxisRecord>())
222    }
223
224    /// Iterate over the axis value tables.
225    pub fn axis_value_tables(
226        &'a self,
227    ) -> impl Iterator<Item = Result<AxisValueTable<'_>, ParseError>> {
228        self.axis_value_offsets.iter().filter_map(move |offset| {
229            let res = self
230                .axis_value_scope
231                .offset(usize::from(offset))
232                .read_dep::<AxisValueTable<'_>>(self.design_axis_count);
233            match res {
234                Ok(table) => Some(Ok(table)),
235                // "If the format is not recognized, then the axis value table can be ignored"
236                Err(ParseError::BadVersion) => None,
237                Err(err) => Some(Err(err)),
238            }
239        })
240    }
241
242    /// Find a name that best describes `value` in the axis at index
243    /// `axis_index`.
244    ///
245    /// `axis_index` is the index of the axis in
246    /// [design_axes](Self::design_axes).
247    pub fn name_for_axis_value(
248        &'a self,
249        axis_index: u16,
250        value: Fixed,
251        include_elidable: ElidableName,
252    ) -> Option<u16> {
253        // Find candidate entries
254        let mut best: Option<(Fixed, u16, bool)> = None;
255        for table in self.axis_value_tables() {
256            let Ok(table) = table else { continue };
257
258            match &table {
259                AxisValueTable::Format1(t) if t.axis_index == axis_index => consider(
260                    &mut best,
261                    t.value,
262                    t.value_name_id,
263                    table.is_elidable(),
264                    value,
265                ),
266                AxisValueTable::Format2(t) if t.axis_index == axis_index => {
267                    if (t.range_min_value..=t.range_max_value).contains(&value) {
268                        consider(
269                            &mut best,
270                            t.nominal_value,
271                            t.value_name_id,
272                            table.is_elidable(),
273                            value,
274                        )
275                    }
276                }
277                AxisValueTable::Format3(t) if t.axis_index == axis_index => consider(
278                    &mut best,
279                    t.value,
280                    t.value_name_id,
281                    table.is_elidable(),
282                    value,
283                ),
284                AxisValueTable::Format4(t) => {
285                    // NOTE: It's unclear if there be multiple entries for the same axis index
286                    let Some(axis_value) = t.axis_values.iter_res().find_map(|value| {
287                        value
288                            .ok()
289                            .and_then(|value| (value.axis_index == axis_index).then(|| value))
290                    }) else {
291                        continue;
292                    };
293                    consider(
294                        &mut best,
295                        axis_value.value,
296                        t.value_name_id,
297                        table.is_elidable(),
298                        value,
299                    )
300                }
301                AxisValueTable::Format1(_)
302                | AxisValueTable::Format2(_)
303                | AxisValueTable::Format3(_) => {}
304            }
305        }
306
307        best.and_then(|(_best_val, name, is_elidable)| match include_elidable {
308            ElidableName::Include => Some(name),
309            // If the best match is elidable and include_elidable is Exclude then return None
310            ElidableName::Exclude => (!is_elidable).then(|| name),
311        })
312    }
313}
314
315fn consider(
316    best: &mut Option<(Fixed, u16, bool)>,
317    candidate_value: Fixed,
318    candidate_name_id: u16,
319    is_elidable: bool,
320    value: Fixed,
321) {
322    match best {
323        Some((best_val, _name, _is_elidable)) => {
324            if (candidate_value - value).abs() < (*best_val - value).abs() {
325                *best = Some((candidate_value, candidate_name_id, is_elidable));
326            }
327        }
328        None => *best = Some((candidate_value, candidate_name_id, is_elidable)),
329    }
330}
331
332impl<'b> ReadBinary for StatTable<'b> {
333    type HostType<'a> = StatTable<'a>;
334
335    fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<StatTable<'a>, ParseError> {
336        let scope = ctxt.scope();
337        let major_version = ctxt.read_u16be()?;
338        ctxt.check_version(major_version == 1)?;
339        let minor_version = ctxt.read_u16be()?;
340        let design_axis_size = ctxt.read_u16be()?;
341        let design_axis_count = ctxt.read_u16be()?;
342        let design_axes_offset = ctxt.read_u32be()?;
343        let design_axes_array = if design_axis_count > 0 {
344            let design_axes_length = usize::from(design_axis_count) * usize::from(design_axis_size);
345            scope
346                .offset(usize::safe_from(design_axes_offset))
347                .ctxt()
348                .read_slice(design_axes_length)?
349        } else {
350            &[]
351        };
352
353        let axis_value_count = ctxt.read_u16be()?;
354        let offset_to_axis_value_offsets = ctxt.read_u32be()?;
355        let (axis_value_scope, axis_value_offsets) = if axis_value_count > 0 {
356            let axis_value_scope = scope.offset(usize::safe_from(offset_to_axis_value_offsets));
357            (
358                axis_value_scope,
359                axis_value_scope
360                    .ctxt()
361                    .read_array(usize::from(axis_value_count))?,
362            )
363        } else {
364            (ReadScope::new(&[]), ReadArray::empty())
365        };
366        let elided_fallback_name_id = (minor_version > 0).then(|| ctxt.read_u16be()).transpose()?;
367
368        Ok(StatTable {
369            major_version,
370            minor_version,
371            design_axis_size,
372            design_axis_count,
373            design_axes_array,
374            axis_value_scope,
375            axis_value_offsets,
376            elided_fallback_name_id,
377        })
378    }
379}
380
381impl ReadFrom for AxisRecord {
382    type ReadType = (U32Be, U16Be, U16Be);
383
384    fn read_from((axis_tag, axis_name_id, axis_ordering): (u32, u16, u16)) -> Self {
385        AxisRecord {
386            axis_tag,
387            axis_name_id,
388            axis_ordering,
389        }
390    }
391}
392
393impl ReadFrom for AxisValueTableFlags {
394    type ReadType = U16Be;
395
396    fn read_from(flag: u16) -> Self {
397        AxisValueTableFlags::from_bits_truncate(flag)
398    }
399}
400
401impl AxisValueTable<'_> {
402    /// Retrieve the flags for this axis value table.
403    pub fn flags(&self) -> AxisValueTableFlags {
404        match self {
405            AxisValueTable::Format1(AxisValueTableFormat1 { flags, .. })
406            | AxisValueTable::Format2(AxisValueTableFormat2 { flags, .. })
407            | AxisValueTable::Format3(AxisValueTableFormat3 { flags, .. })
408            | AxisValueTable::Format4(AxisValueTableFormat4 { flags, .. }) => *flags,
409        }
410    }
411
412    /// Retrieve the name id in the `NAME` table for this value.
413    pub fn value_name_id(&self) -> u16 {
414        match self {
415            AxisValueTable::Format1(AxisValueTableFormat1 { value_name_id, .. })
416            | AxisValueTable::Format2(AxisValueTableFormat2 { value_name_id, .. })
417            | AxisValueTable::Format3(AxisValueTableFormat3 { value_name_id, .. })
418            | AxisValueTable::Format4(AxisValueTableFormat4 { value_name_id, .. }) => {
419                *value_name_id
420            }
421        }
422    }
423
424    /// If set, it indicates that the axis value represents the “normal” value
425    /// for the axis and may be omitted when composing name strings.
426    pub fn is_elidable(&self) -> bool {
427        self.flags()
428            .contains(AxisValueTableFlags::ELIDABLE_AXIS_VALUE_NAME)
429    }
430}
431
432impl ReadBinaryDep for AxisValueTable<'_> {
433    type Args<'a> = u16;
434    type HostType<'a> = AxisValueTable<'a>;
435
436    fn read_dep<'a>(
437        ctxt: &mut ReadCtxt<'a>,
438        design_axis_count: u16,
439    ) -> Result<Self::HostType<'a>, ParseError> {
440        let format = ctxt.read_u16be()?;
441        match format {
442            1 => {
443                let axis_index = ctxt.read_u16be()?;
444                ctxt.check_index(axis_index < design_axis_count)?;
445                let flags = ctxt.read::<AxisValueTableFlags>()?;
446                let value_name_id = ctxt.read_u16be()?;
447                let value = ctxt.read::<Fixed>()?;
448                Ok(AxisValueTable::Format1(AxisValueTableFormat1 {
449                    axis_index,
450                    flags,
451                    value_name_id,
452                    value,
453                }))
454            }
455            2 => {
456                let axis_index = ctxt.read_u16be()?;
457                ctxt.check_index(axis_index < design_axis_count)?;
458                let flags = ctxt.read::<AxisValueTableFlags>()?;
459                let value_name_id = ctxt.read_u16be()?;
460                let nominal_value = ctxt.read::<Fixed>()?;
461                let range_min_value = ctxt.read::<Fixed>()?;
462                let range_max_value = ctxt.read::<Fixed>()?;
463                Ok(AxisValueTable::Format2(AxisValueTableFormat2 {
464                    axis_index,
465                    flags,
466                    value_name_id,
467                    nominal_value,
468                    range_min_value,
469                    range_max_value,
470                }))
471            }
472            3 => {
473                let axis_index = ctxt.read_u16be()?;
474                ctxt.check_index(axis_index < design_axis_count)?;
475                let flags = ctxt.read::<AxisValueTableFlags>()?;
476                let value_name_id = ctxt.read_u16be()?;
477                let value = ctxt.read::<Fixed>()?;
478                let linked_value = ctxt.read::<Fixed>()?;
479                Ok(AxisValueTable::Format3(AxisValueTableFormat3 {
480                    axis_index,
481                    flags,
482                    value_name_id,
483                    value,
484                    linked_value,
485                }))
486            }
487            4 => {
488                let axis_count = ctxt.read_u16be()?;
489                let flags = ctxt.read::<AxisValueTableFlags>()?;
490                let value_name_id = ctxt.read_u16be()?;
491                let axis_values =
492                    ctxt.read_array_dep(usize::from(axis_count), design_axis_count)?;
493                Ok(AxisValueTable::Format4(AxisValueTableFormat4 {
494                    flags,
495                    value_name_id,
496                    axis_values,
497                }))
498            }
499            _ => Err(ParseError::BadVersion),
500        }
501    }
502}
503
504impl ReadBinaryDep for AxisValue {
505    type Args<'a> = u16;
506    type HostType<'a> = AxisValue;
507
508    fn read_dep<'a>(
509        ctxt: &mut ReadCtxt<'a>,
510        design_axis_count: u16,
511    ) -> Result<Self::HostType<'a>, ParseError> {
512        let axis_index = ctxt.read_u16be()?;
513        ctxt.check_index(axis_index < design_axis_count)?;
514        let value = ctxt.read::<Fixed>()?;
515        Ok(AxisValue { axis_index, value })
516    }
517}
518
519impl ReadFixedSizeDep for AxisValue {
520    fn size(_args: Self::Args<'_>) -> usize {
521        size::U16 + Fixed::SIZE
522    }
523}
524
525impl fmt::Debug for AxisRecord {
526    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527        let tag = format!("{:?} ({})", self.axis_tag, DisplayTag(self.axis_tag));
528        f.debug_struct("AxisRecord")
529            .field("axis_tag", &tag)
530            .field("axis_name_id", &self.axis_name_id)
531            .field("axis_ordering", &self.axis_ordering)
532            .finish()
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::font_data::FontData;
540    use crate::tables::{FontTableProvider, NameTable};
541    use crate::tag;
542    use crate::tests::read_fixture;
543
544    #[test]
545    fn stat() {
546        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
547        let scope = ReadScope::new(&buffer);
548        let font_file = scope
549            .read::<FontData<'_>>()
550            .expect("unable to parse font file");
551        let table_provider = font_file
552            .table_provider(0)
553            .expect("unable to create font provider");
554        let stat_data = table_provider
555            .read_table_data(tag::STAT)
556            .expect("unable to read fvar table data");
557        let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>().unwrap();
558        let name_table_data = table_provider
559            .read_table_data(tag::NAME)
560            .expect("unable to read name table data");
561        let name_table = ReadScope::new(&name_table_data)
562            .read::<NameTable<'_>>()
563            .unwrap();
564
565        let expected = [
566            AxisRecord {
567                axis_tag: tag!(b"wght"),
568                axis_name_id: 261,
569                axis_ordering: 0,
570            },
571            AxisRecord {
572                axis_tag: tag!(b"wdth"),
573                axis_name_id: 271,
574                axis_ordering: 1,
575            },
576            AxisRecord {
577                axis_tag: tag!(b"CTGR"),
578                axis_name_id: 276,
579                axis_ordering: 2,
580            },
581        ];
582        let design_axes = stat.design_axes().collect::<Result<Vec<_>, _>>().unwrap();
583        assert_eq!(design_axes, expected);
584
585        let axis_value_tables = stat
586            .axis_value_tables()
587            .collect::<Result<Vec<_>, _>>()
588            .unwrap();
589        assert_eq!(axis_value_tables.len(), 15);
590        let first = axis_value_tables.first().unwrap();
591        let AxisValueTable::Format1(table) = first else {
592            panic!("expected AxisValueTableFormat1")
593        };
594        let expected = AxisValueTableFormat1 {
595            axis_index: 0,
596            flags: AxisValueTableFlags::empty(),
597            value_name_id: 262,
598            value: <Fixed as From<f32>>::from(100.),
599        };
600        assert_eq!(table, &expected);
601        let value_name = name_table.string_for_id(first.value_name_id()).unwrap();
602        assert_eq!(value_name, "Thin");
603
604        let last = axis_value_tables.last().unwrap();
605        let AxisValueTable::Format1(table) = last else {
606            panic!("expected AxisValueTableFormat1")
607        };
608        let expected = AxisValueTableFormat1 {
609            axis_index: 2,
610            flags: AxisValueTableFlags::empty(),
611            value_name_id: 278,
612            value: <Fixed as From<f32>>::from(100.),
613        };
614        assert_eq!(table, &expected);
615        let value_name = name_table.string_for_id(last.value_name_id()).unwrap();
616        assert_eq!(value_name, "Display");
617    }
618}