allsorts_subset_browser/tables/variable_fonts/
mvar.rs

1//! `MVAR` Metrics Variations Table
2//!
3//! <https://learn.microsoft.com/en-us/typography/opentype/spec/mvar>
4
5use crate::binary::read::{ReadArray, ReadBinary, ReadCtxt, ReadFrom, ReadUnchecked};
6use crate::binary::{U16Be, U32Be};
7use crate::error::ParseError;
8use crate::tables::variable_fonts::{DeltaSetIndexMapEntry, ItemVariationStore, OwnedTuple};
9
10/// `MVAR` Metrics Variations Table
11pub struct MvarTable<'a> {
12    /// Major version number of the metrics variations table.
13    pub major_version: u16,
14    /// Minor version number of the metrics variations table.
15    pub minor_version: u16,
16    /// The item variation data, `None` if `value_records.len()` is zero.
17    item_variation_store: Option<ItemVariationStore<'a>>,
18    /// Array of value records that identify target items and the associated
19    /// delta-set index for each.
20    ///
21    /// The valueTag records must be in binary order of their valueTag field.
22    value_records: ReadArray<'a, ValueRecord>,
23}
24
25/// Identifies target items by tag their associated delta-set index.
26#[derive(Copy, Clone)]
27pub struct ValueRecord {
28    /// Four-byte tag identifying a font-wide measure.
29    pub value_tag: u32,
30    /// A delta-set outer index.
31    ///
32    /// Used to select an item variation data sub-table within the item
33    /// variation store.
34    delta_set_outer_index: u16,
35    /// A delta-set inner index.
36    ///
37    /// Used to select a delta-set row within an item variation data sub-table.
38    delta_set_inner_index: u16,
39}
40
41impl<'a> MvarTable<'a> {
42    /// Retrieve the delta for the supplied
43    /// [value tag](https://learn.microsoft.com/en-us/typography/opentype/spec/mvar#value-tags).
44    pub fn lookup(&self, tag: u32, instance: &OwnedTuple) -> Option<f32> {
45        let item_variation_store = self.item_variation_store.as_ref()?;
46        let value_record = self
47            .value_records
48            .binary_search_by(|record| record.value_tag.cmp(&tag))
49            .ok()
50            .map(|index| self.value_records.get_item(index))?;
51        // To compute the interpolated instance value for a given target item, the
52        // application first obtains the delta-set index for that item. It uses
53        // the outer-level index portion to select an item variation data
54        // sub-table within the item variation store, and the inner-level index
55        // portion to select a delta-set row within that sub-table.
56        //
57        // The delta set contains one delta for each region referenced by the sub-table,
58        // in order of the region indices given in the regionIndices array. The
59        // application uses the regionIndices array for that sub-table to
60        // identify applicable regions and to compute a scalar for each of these
61        // regions based on the selected instance. Each of the scalars is
62        // then applied to the corresponding delta within the delta set to derive a
63        // scaled adjustment. The scaled adjustments for the row are then
64        // combined to obtain the overall adjustment for the item.
65        item_variation_store
66            .adjustment(value_record.into(), instance)
67            .ok()
68    }
69
70    /// Iterator over the [ValueRecords][ValueRecord] in this `MVAR` table.
71    pub fn value_records(&self) -> impl Iterator<Item = ValueRecord> + 'a {
72        self.value_records.iter()
73    }
74
75    /// The number of [ValueRecords][ValueRecord] in this `MVAR` table.
76    pub fn value_records_len(&self) -> u16 {
77        // NOTE(cast): Safe as value_records was contructed from u16 value_record_count.
78        self.value_records.len() as u16
79    }
80}
81
82impl ReadBinary for MvarTable<'_> {
83    type HostType<'a> = MvarTable<'a>;
84
85    fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
86        let scope = ctxt.scope();
87        let major_version = ctxt.read_u16be()?;
88        ctxt.check_version(major_version == 1)?;
89        let minor_version = ctxt.read_u16be()?;
90        let _reserved = ctxt.read_u16be()?;
91        let value_record_size = ctxt.read_u16be()?;
92        let value_record_count = ctxt.read_u16be()?;
93        let item_variation_store_offset = ctxt.read_u16be()?;
94        let value_records = if value_record_count > 0 {
95            // The spec says that value_record_size must be greater than zero but a font
96            // was encountered (DecovarAlpha) where it was zero. However the count was also
97            // zero so we accept this.
98            ctxt.check(usize::from(value_record_size) >= ValueRecord::SIZE)?;
99            ctxt.read_array_stride::<ValueRecord>(
100                usize::from(value_record_count),
101                usize::from(value_record_size),
102            )?
103        } else {
104            ReadArray::empty()
105        };
106        let item_variation_store = (item_variation_store_offset > 0)
107            .then(|| {
108                scope
109                    .offset(usize::from(item_variation_store_offset))
110                    .read::<ItemVariationStore<'_>>()
111            })
112            .transpose()?;
113
114        Ok(MvarTable {
115            major_version,
116            minor_version,
117            item_variation_store,
118            value_records,
119        })
120    }
121}
122
123impl ReadFrom for ValueRecord {
124    type ReadType = (U32Be, U16Be, U16Be);
125
126    fn read_from(
127        (value_tag, delta_set_outer_index, delta_set_inner_index): (u32, u16, u16),
128    ) -> Self {
129        ValueRecord {
130            value_tag,
131            delta_set_outer_index,
132            delta_set_inner_index,
133        }
134    }
135}
136
137impl From<ValueRecord> for DeltaSetIndexMapEntry {
138    fn from(record: ValueRecord) -> DeltaSetIndexMapEntry {
139        DeltaSetIndexMapEntry {
140            outer_index: record.delta_set_outer_index,
141            inner_index: record.delta_set_inner_index,
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::binary::read::ReadScope;
150    use crate::font_data::FontData;
151    use crate::tables::variable_fonts::fvar::FvarTable;
152    use crate::tables::{Fixed, FontTableProvider};
153    use crate::tag;
154    use crate::tests::{assert_close, read_fixture};
155
156    #[test]
157    fn lookup_value() {
158        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
159        let scope = ReadScope::new(&buffer);
160        let font_file = scope
161            .read::<FontData<'_>>()
162            .expect("unable to parse font file");
163        let table_provider = font_file
164            .table_provider(0)
165            .expect("unable to create font provider");
166        let fvar_data = table_provider
167            .read_table_data(tag::FVAR)
168            .expect("unable to read fvar table data");
169        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
170        let mvar_data = table_provider
171            .read_table_data(tag::MVAR)
172            .expect("unable to read mvar table data");
173        let mvar = ReadScope::new(&mvar_data).read::<MvarTable<'_>>().unwrap();
174        //  axis="wght" value="900.0", axis="wdth" value="62.5", axis="CTGR"
175        // value="100.0"
176        let user_tuple = [Fixed::from(900), Fixed::from(62.5), Fixed::from(100)];
177        let instance = fvar.normalize(user_tuple.iter().copied(), None).unwrap();
178        let val = mvar.lookup(tag!(b"xhgt"), &instance).unwrap();
179        // Value verified by creating a static instance of the font with fonttools,
180        // dumping it with ttx and then observing the OS/2.sxHeight = 553, which
181        // is 17 more than the default of 536 in the original. fonttools
182        // invocation: fonttools varLib.mutator
183        // src/fonts/allsorts/tests/fonts/opentype/NotoSans-VF.abc.ttf  wght=900
184        // wdth=62.5 CTGR=100
185        assert_close(val, 17.0);
186    }
187}