Skip to main content

write_fonts/tables/
fvar.rs

1//! The [fvar](https://learn.microsoft.com/en-us/typography/opentype/spec/fvar) table
2
3#[path = "./instance_record.rs"]
4mod instance_record;
5
6pub use instance_record::InstanceRecord;
7
8include!("../../generated/generated_fvar.rs");
9
10impl Fvar {
11    /// We need everyone to have, or not have, post_script name so we can have a single record size.
12    fn check_instances(&self, ctx: &mut ValidationCtx) {
13        let sum: i32 = self
14            .axis_instance_arrays
15            .instances
16            .iter()
17            .map(|ir| ir.post_script_name_id.map(|_| 1).unwrap_or(-1))
18            .sum();
19        if sum.unsigned_abs() as usize != self.axis_instance_arrays.instances.len() {
20            ctx.report("All or none of the instances must have post_script_name_id set. Use Some(0xFFFF) if you need to set it where you have no value.");
21        }
22
23        let axis_count = self.axis_count();
24        let uncoordinated_instances = self
25            .axis_instance_arrays
26            .instances
27            .iter()
28            .filter(|ir| ir.coordinates.len() != axis_count as usize)
29            .count();
30        if uncoordinated_instances > 0 {
31            ctx.report(format!(
32                "{uncoordinated_instances} instances do not have axis_count ({axis_count}) coordinates",
33            ));
34        }
35    }
36
37    fn axis_count(&self) -> u16 {
38        self.axis_instance_arrays.axes.len().try_into().unwrap()
39    }
40
41    fn instance_count(&self) -> u16 {
42        self.axis_instance_arrays
43            .instances
44            .len()
45            .try_into()
46            .unwrap()
47    }
48
49    fn instance_size(&self) -> u16 {
50        // https://learn.microsoft.com/en-us/typography/opentype/spec/fvar#fvar-header
51        let mut instance_size = self.axis_count() * Fixed::RAW_BYTE_LEN as u16 + 4;
52        if self
53            .axis_instance_arrays
54            .instances
55            .iter()
56            .any(|i| i.post_script_name_id.is_some())
57        {
58            instance_size += 2;
59        }
60        instance_size
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use read_fonts::{FontRef, TableProvider};
67
68    use super::*;
69
70    fn wdth_wght_fvar() -> Fvar {
71        let mut fvar = Fvar::default();
72
73        fvar.axis_instance_arrays.axes.push(VariationAxisRecord {
74            axis_tag: Tag::new(b"wght"),
75            min_value: Fixed::from_i32(300),
76            default_value: Fixed::from_i32(400),
77            max_value: Fixed::from_i32(700),
78            ..Default::default()
79        });
80        fvar.axis_instance_arrays.axes.push(VariationAxisRecord {
81            axis_tag: Tag::new(b"wdth"),
82            min_value: Fixed::from_f64(75.0),
83            default_value: Fixed::from_f64(100.0),
84            max_value: Fixed::from_f64(125.0),
85            ..Default::default()
86        });
87        fvar
88    }
89
90    fn assert_wdth_wght_test_values(fvar: &read_fonts::tables::fvar::Fvar) {
91        assert_eq!(fvar.version(), MajorMinor::VERSION_1_0);
92        assert_eq!(fvar.axis_count(), 2);
93        assert_eq!(
94            vec![
95                (Tag::new(b"wght"), 300.0, 400.0, 700.0),
96                (Tag::new(b"wdth"), 75.0, 100.0, 125.0),
97            ],
98            fvar.axis_instance_arrays()
99                .unwrap()
100                .axes()
101                .iter()
102                .map(|var| (
103                    var.axis_tag.get(),
104                    var.min_value().to_f64(),
105                    var.default_value().to_f64(),
106                    var.max_value().to_f64()
107                ))
108                .collect::<Vec<_>>()
109        );
110    }
111
112    fn get_only_instance(
113        fvar: read_fonts::tables::fvar::Fvar,
114    ) -> read_fonts::tables::fvar::InstanceRecord {
115        let instances = fvar.axis_instance_arrays().unwrap().instances();
116        assert_eq!(1, instances.len());
117        instances.get(0).unwrap()
118    }
119
120    fn nameless_instance_record(coordinates: Vec<Fixed>) -> InstanceRecord {
121        InstanceRecord {
122            subfamily_name_id: NameId::TYPOGRAPHIC_SUBFAMILY_NAME,
123            coordinates,
124            ..Default::default()
125        }
126    }
127
128    fn named_instance_record(coordinates: Vec<Fixed>, name_id: u16) -> InstanceRecord {
129        let mut rec = nameless_instance_record(coordinates);
130        rec.post_script_name_id = Some(NameId::new(name_id));
131        rec
132    }
133
134    #[test]
135    fn write_read_no_instances() {
136        let fvar = wdth_wght_fvar();
137        let bytes = crate::write::dump_table(&fvar).unwrap();
138        let loaded = read_fonts::tables::fvar::Fvar::read(FontData::new(&bytes)).unwrap();
139        assert_wdth_wght_test_values(&loaded);
140    }
141
142    #[test]
143    fn write_read_short_instance() {
144        let mut fvar = wdth_wght_fvar();
145        let coordinates = vec![Fixed::from_i32(420), Fixed::from_f64(101.5)];
146        fvar.axis_instance_arrays
147            .instances
148            .push(nameless_instance_record(coordinates.clone()));
149        assert_eq!(2 * 4 + 4, fvar.instance_size());
150
151        let bytes = crate::write::dump_table(&fvar).unwrap();
152        let loaded = read_fonts::tables::fvar::Fvar::read(FontData::new(&bytes)).unwrap();
153        assert_wdth_wght_test_values(&loaded);
154        assert_eq!(fvar.instance_size(), loaded.instance_size());
155
156        let instance = get_only_instance(loaded);
157        assert_eq!(None, instance.post_script_name_id);
158        assert_eq!(
159            coordinates,
160            instance
161                .coordinates
162                .iter()
163                .map(|v| v.get())
164                .collect::<Vec<_>>()
165        );
166    }
167
168    #[test]
169    fn write_read_long_instance() {
170        let mut fvar = wdth_wght_fvar();
171        let coordinates = vec![Fixed::from_i32(650), Fixed::from_i32(420)];
172        fvar.axis_instance_arrays
173            .instances
174            .push(named_instance_record(coordinates.clone(), 256));
175        assert_eq!(2 * 4 + 6, fvar.instance_size());
176
177        let bytes = crate::write::dump_table(&fvar).unwrap();
178        let loaded = read_fonts::tables::fvar::Fvar::read(FontData::new(&bytes)).unwrap();
179        assert_wdth_wght_test_values(&loaded);
180        assert_eq!(fvar.instance_size(), loaded.instance_size());
181
182        let instance = get_only_instance(loaded);
183        assert_eq!(Some(NameId::new(256)), instance.post_script_name_id);
184        assert_eq!(
185            coordinates,
186            instance
187                .coordinates
188                .iter()
189                .map(|v| v.get())
190                .collect::<Vec<_>>()
191        );
192    }
193
194    #[test]
195    fn round_trip() {
196        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
197        let read_testdata = font.fvar().unwrap();
198
199        let fvar = Fvar::from_table_ref(&read_testdata);
200        let bytes = crate::write::dump_table(&fvar).unwrap();
201        let loaded = read_fonts::tables::fvar::Fvar::read(FontData::new(&bytes)).unwrap();
202
203        assert_eq!(read_testdata.version(), loaded.version());
204        assert_eq!(read_testdata.axis_count(), loaded.axis_count());
205    }
206
207    #[test]
208    fn inconsistent_instance_size_fails() {
209        let mut fvar = wdth_wght_fvar();
210        let coordinates = vec![Fixed::from_i32(650), Fixed::from_i32(420)];
211        // OMG no, inconsistent sizing!
212        fvar.axis_instance_arrays
213            .instances
214            .push(nameless_instance_record(coordinates.clone()));
215        fvar.axis_instance_arrays
216            .instances
217            .push(named_instance_record(coordinates, 256));
218        assert!(fvar.validate().is_err());
219    }
220
221    #[test]
222    fn wrong_number_of_coordinates_fails() {
223        let mut fvar = wdth_wght_fvar();
224        let coordinates = vec![Fixed::from_i32(650)];
225        fvar.axis_instance_arrays
226            .instances
227            .push(nameless_instance_record(coordinates));
228        assert!(fvar.validate().is_err());
229    }
230}