1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! The [MVAR](https://learn.microsoft.com/en-us/typography/opentype/spec/mvar) table

include!("../../generated/generated_mvar.rs");

use super::variations::ItemVariationStore;
use std::mem::size_of;

impl Mvar {
    /// Construct a new `MVAR` table.
    pub fn new(
        version: MajorMinor,
        item_variation_store: Option<ItemVariationStore>,
        value_records: Vec<ValueRecord>,
    ) -> Self {
        Self {
            version,
            value_record_size: size_of::<ValueRecord>() as u16,
            value_record_count: value_records.len() as u16,
            item_variation_store: item_variation_store.into(),
            value_records: value_records.into_iter().map(Into::into).collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use font_types::{F2Dot14, Tag};
    use read_fonts::tables::mvar as read_mvar;

    use crate::dump_table;
    use crate::tables::variations::{
        ivs_builder::VariationStoreBuilder, RegionAxisCoordinates, VariationRegion,
    };

    use super::*;

    #[test]
    fn empty_smoke_test() {
        let table = Mvar::new(MajorMinor::new(1, 0), None, vec![]);

        let bytes = dump_table(&table).unwrap();
        let read = read_mvar::Mvar::read(FontData::new(&bytes)).unwrap();

        assert_eq!(read.version(), table.version);
        assert_eq!(read.value_record_count(), 0);
        assert_eq!(read.value_record_size(), 8);
        assert!(read.item_variation_store().is_none());
        assert_eq!(read.value_records().len(), 0);
    }

    fn reg_coords(min: f32, default: f32, max: f32) -> RegionAxisCoordinates {
        RegionAxisCoordinates {
            start_coord: F2Dot14::from_f32(min),
            peak_coord: F2Dot14::from_f32(default),
            end_coord: F2Dot14::from_f32(max),
        }
    }

    fn test_regions() -> [VariationRegion; 3] {
        [
            VariationRegion::new(vec![reg_coords(0.0, 1.0, 1.0)]),
            VariationRegion::new(vec![reg_coords(0.0, 0.5, 1.0)]),
            VariationRegion::new(vec![reg_coords(0.5, 1.0, 1.0)]),
        ]
    }

    fn read_metric_delta(mvar: &read_mvar::Mvar, tag: &[u8; 4], coords: &[f32]) -> f64 {
        let coords = coords
            .iter()
            .map(|c| F2Dot14::from_f32(*c))
            .collect::<Vec<_>>();
        mvar.metric_delta(Tag::new(tag), &coords).unwrap().to_f64()
    }

    fn assert_value_record(actual: &read_mvar::ValueRecord, expected: ValueRecord) {
        assert_eq!(actual.value_tag(), expected.value_tag);
        assert_eq!(
            actual.delta_set_outer_index(),
            expected.delta_set_outer_index
        );
        assert_eq!(
            actual.delta_set_inner_index(),
            expected.delta_set_inner_index
        );
    }

    #[test]
    fn simple_smoke_test() {
        let [r1, r2, r3] = test_regions();
        let mut builder = VariationStoreBuilder::new();
        let delta_ids = vec![
            // deltas for horizontal ascender 'hasc' only defined for 1 region
            builder.add_deltas(vec![(r1, 10)]),
            // deltas for horizontal descender 'hdsc' defined for 2 regions
            builder.add_deltas(vec![(r2, -20), (r3, -30)]),
        ];
        let (varstore, index_map) = builder.build();

        let mut value_records = Vec::new();
        for (tag, temp_id) in [b"hasc", b"hdsc"].into_iter().zip(delta_ids.into_iter()) {
            let varidx = index_map.get(temp_id).unwrap();
            let value_record = ValueRecord::new(
                Tag::new(tag),
                varidx.delta_set_outer_index,
                varidx.delta_set_inner_index,
            );
            value_records.push(value_record);
        }

        let table = Mvar::new(MajorMinor::new(1, 0), Some(varstore), value_records);

        let bytes = dump_table(&table).unwrap();
        let read = read_mvar::Mvar::read(FontData::new(&bytes)).unwrap();

        assert_eq!(read.version(), table.version);
        assert_eq!(read.value_record_count(), 2);
        assert_eq!(read.value_record_size(), 8);
        assert!(read.item_variation_store().is_some());

        assert_value_record(
            &read.value_records()[0],
            ValueRecord::new(Tag::new(b"hasc"), 0, 1),
        );
        assert_eq!(read_metric_delta(&read, b"hasc", &[0.0]), 0.0);
        // at axis coord 0.5, the interpolated delta will be half of r1's delta
        assert_eq!(read_metric_delta(&read, b"hasc", &[0.5]), 5.0);
        assert_eq!(read_metric_delta(&read, b"hasc", &[1.0]), 10.0);

        assert_value_record(
            &read.value_records()[1],
            ValueRecord::new(Tag::new(b"hdsc"), 0, 0),
        );
        assert_eq!(read_metric_delta(&read, b"hdsc", &[0.0]), 0.0);
        // this coincides with the peak of intermediate region r2, hence != 30.0/2
        assert_eq!(read_metric_delta(&read, b"hdsc", &[0.5]), -20.0);
        assert_eq!(read_metric_delta(&read, b"hdsc", &[1.0]), -30.0);
    }
}