Skip to main content

read_fonts/tables/
fvar.rs

1//! The [Font Variations](https://docs.microsoft.com/en-us/typography/opentype/spec/fvar) table
2
3include!("../../generated/generated_fvar.rs");
4
5#[path = "./instance_record.rs"]
6mod instance_record;
7
8use super::{avar::Avar, variations::DeltaSetIndex};
9use alloc::vec::Vec;
10
11pub use instance_record::InstanceRecord;
12
13const MAX_INLINE_AVAR2_AXES: usize = 64;
14
15#[inline]
16fn round_f64_to_i32(value: f64) -> i32 {
17    if value >= 0.0 {
18        (value + 0.5) as i32
19    } else {
20        (value - 0.5) as i32
21    }
22}
23
24#[inline]
25fn apply_avar2_delta(coord: Fixed, delta_2dot14: f64) -> Fixed {
26    // HarfBuzz keeps the avar1 result in 16.16 through the avar2 add, and
27    // converts the avar2 delta from 2.14 units by multiplying by four.
28    Fixed::from_bits(
29        coord
30            .to_bits()
31            .wrapping_add(round_f64_to_i32(delta_2dot14 * 4.0)),
32    )
33}
34
35fn normalize_user_coords<T>(
36    axes: &[VariationAxisRecord],
37    user_coords: impl IntoIterator<Item = (Tag, Fixed)>,
38    coords: &mut [T],
39    convert: impl Fn(Fixed) -> T,
40) {
41    for user_coord in user_coords {
42        // To permit non-linear interpolation, iterate over all axes to ensure we match
43        // multiple axes with the same tag:
44        // https://github.com/PeterConstable/OT_Drafts/blob/master/NLI/UnderstandingNLI.md
45        // We accept quadratic behavior here to avoid dynamic allocation and with the assumption
46        // that fonts contain a relatively small number of axes.
47        for (axis, coord) in axes
48            .iter()
49            .zip(coords.iter_mut())
50            .filter(|(axis, _)| axis.axis_tag() == user_coord.0)
51        {
52            *coord = convert(axis.normalize(user_coord.1));
53        }
54    }
55}
56
57fn apply_avar_mappings<T>(
58    avar: Option<&Avar>,
59    coords: &mut [T],
60    to_fixed: impl Fn(&T) -> Fixed,
61    from_fixed: impl Fn(Fixed) -> T,
62) {
63    let avar_mappings = avar.map(|avar| avar.axis_segment_maps());
64    for (i, coord) in coords.iter_mut().enumerate() {
65        if let Some(mapping) = avar_mappings
66            .as_ref()
67            .and_then(|mappings| mappings.get(i).transpose().ok())
68            .flatten()
69        {
70            *coord = from_fixed(mapping.apply(to_fixed(coord)));
71        }
72    }
73}
74
75fn to_normalized_coords(fixed_coords: &[Fixed], normalized_coords: &mut [F2Dot14]) {
76    for (target_coord, coord) in normalized_coords.iter_mut().zip(fixed_coords.iter()) {
77        *target_coord = coord.to_f2dot14();
78    }
79}
80
81impl<'a> Fvar<'a> {
82    /// Returns the array of variation axis records.
83    pub fn axes(&self) -> Result<&'a [VariationAxisRecord], ReadError> {
84        Ok(self.axis_instance_arrays()?.axes())
85    }
86
87    /// Returns the array of instance records.
88    pub fn instances(&self) -> Result<ComputedArray<'a, InstanceRecord<'a>>, ReadError> {
89        Ok(self.axis_instance_arrays()?.instances())
90    }
91
92    /// Converts user space coordinates provided by an unordered iterator
93    /// of `(tag, value)` pairs to normalized coordinates in axis list order.
94    ///
95    /// Stores the resulting normalized coordinates in the given slice.
96    ///
97    /// * User coordinate tags that don't match an axis are ignored.
98    /// * User coordinate values are clamped to the range of their associated
99    ///   axis before normalization.
100    /// * If more than one user coordinate is provided for the same tag, the
101    ///   last one is used.
102    /// * If no user coordinate for an axis is provided, the associated
103    ///   coordinate is set to the normalized value 0.0, representing the
104    ///   default location in variation space.
105    /// * The length of `normalized_coords` should equal the number of axes
106    ///   present in the this table. If the length is smaller, axes at
107    ///   out of bounds indices are ignored. If the length is larger, the
108    ///   excess entries will be filled with zeros.
109    ///
110    /// If the [`Avar`] table is provided, applies remapping of coordinates
111    /// according to the specification.
112    pub fn user_to_normalized(
113        &self,
114        avar: Option<&Avar>,
115        user_coords: impl IntoIterator<Item = (Tag, Fixed)>,
116        normalized_coords: &mut [F2Dot14],
117    ) {
118        normalized_coords.fill(F2Dot14::ZERO);
119        let axes = self.axes().unwrap_or_default();
120        let actual_len = axes.len().min(normalized_coords.len());
121        let normalized_coords = &mut normalized_coords[..actual_len];
122
123        let mut stack_fixed_coords = [Fixed::ZERO; MAX_INLINE_AVAR2_AXES];
124        let mut heap_fixed_coords = Vec::new();
125        let fixed_coords = if actual_len > MAX_INLINE_AVAR2_AXES {
126            heap_fixed_coords.resize(actual_len, Fixed::ZERO);
127            heap_fixed_coords.as_mut_slice()
128        } else {
129            &mut stack_fixed_coords[..actual_len]
130        };
131        normalize_user_coords(axes, user_coords, fixed_coords, core::convert::identity);
132        apply_avar_mappings(avar, fixed_coords, |coord| *coord, core::convert::identity);
133
134        let Some(avar) = avar else {
135            to_normalized_coords(fixed_coords, normalized_coords);
136            return;
137        };
138        if avar.version() == MajorMinor::VERSION_1_0 {
139            to_normalized_coords(fixed_coords, normalized_coords);
140            return;
141        }
142
143        let var_store = avar.var_store();
144        let var_index_map = avar.axis_index_map();
145
146        let mut stack_coords_2dot14 = [F2Dot14::ZERO; MAX_INLINE_AVAR2_AXES];
147        let mut heap_coords_2dot14 = Vec::new();
148        let coords_2dot14 = if actual_len > MAX_INLINE_AVAR2_AXES {
149            heap_coords_2dot14.resize(actual_len, F2Dot14::ZERO);
150            heap_coords_2dot14.as_mut_slice()
151        } else {
152            &mut stack_coords_2dot14[..actual_len]
153        };
154        for (coord_2dot14, coord) in coords_2dot14.iter_mut().zip(fixed_coords.iter()) {
155            *coord_2dot14 = coord.to_f2dot14();
156        }
157
158        if let Some(Ok(varstore)) = var_store.as_ref() {
159            for (i, coord) in fixed_coords.iter_mut().enumerate() {
160                let var_index = if let Some(Ok(ref map)) = var_index_map {
161                    match map.get(i as u32) {
162                        Ok(index) => index,
163                        Err(_) => continue,
164                    }
165                } else {
166                    DeltaSetIndex {
167                        outer: 0,
168                        inner: i as u16,
169                    }
170                };
171                if let Ok(delta) = varstore.compute_float_delta(var_index, coords_2dot14) {
172                    *coord =
173                        apply_avar2_delta(*coord, delta.to_f64()).clamp(Fixed::NEG_ONE, Fixed::ONE);
174                }
175            }
176        }
177
178        to_normalized_coords(fixed_coords, normalized_coords);
179    }
180}
181
182impl VariationAxisRecord {
183    /// Returns a normalized coordinate for the given value.
184    pub fn normalize(&self, mut value: Fixed) -> Fixed {
185        use core::cmp::Ordering::*;
186        let min_value = self.min_value();
187        let default_value = self.default_value();
188        // Make sure max is >= min to avoid potential panic in clamp.
189        let max_value = self.max_value().max(min_value);
190        value = value.clamp(min_value, max_value);
191        value = match value.cmp(&default_value) {
192            Less => {
193                -((default_value.saturating_sub(value)) / (default_value.saturating_sub(min_value)))
194            }
195            Greater => {
196                (value.saturating_sub(default_value)) / (max_value.saturating_sub(default_value))
197            }
198            Equal => Fixed::ZERO,
199        };
200        value.clamp(Fixed::NEG_ONE, Fixed::ONE)
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use crate::{FontRef, TableProvider};
207    use types::{F2Dot14, Fixed, NameId, Tag};
208
209    #[test]
210    fn axes() {
211        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
212        let fvar = font.fvar().unwrap();
213        assert_eq!(fvar.axis_count(), 1);
214        let wght = &fvar.axes().unwrap().first().unwrap();
215        assert_eq!(wght.axis_tag(), Tag::new(b"wght"));
216        assert_eq!(wght.min_value(), Fixed::from_f64(100.0));
217        assert_eq!(wght.default_value(), Fixed::from_f64(400.0));
218        assert_eq!(wght.max_value(), Fixed::from_f64(900.0));
219        assert_eq!(wght.flags(), 0);
220        assert_eq!(wght.axis_name_id(), NameId::new(257));
221    }
222
223    #[test]
224    fn instances() {
225        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
226        let fvar = font.fvar().unwrap();
227        assert_eq!(fvar.instance_count(), 9);
228        // There are 9 instances equally spaced from 100.0 to 900.0
229        // with name id monotonically increasing starting at 258.
230        let instances = fvar.instances().unwrap();
231        for i in 0..9 {
232            let value = 100.0 * (i + 1) as f64;
233            let name_id = NameId::new(258 + i as u16);
234            let instance = instances.get(i).unwrap();
235            assert_eq!(instance.coordinates.len(), 1);
236            assert_eq!(
237                instance.coordinates.first().unwrap().get(),
238                Fixed::from_f64(value)
239            );
240            assert_eq!(instance.subfamily_name_id, name_id);
241            assert_eq!(instance.post_script_name_id, None);
242        }
243    }
244
245    #[test]
246    fn normalize() {
247        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
248        let fvar = font.fvar().unwrap();
249        let axis = fvar.axes().unwrap().first().unwrap();
250        let values = [100.0, 220.0, 250.0, 400.0, 650.0, 900.0];
251        let expected = [-1.0, -0.60001, -0.5, 0.0, 0.5, 1.0];
252        for (value, expected) in values.into_iter().zip(expected) {
253            assert_eq!(
254                axis.normalize(Fixed::from_f64(value)),
255                Fixed::from_f64(expected)
256            );
257        }
258    }
259
260    #[test]
261    fn normalize_overflow() {
262        // From: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=69787
263        // & https://oss-fuzz.com/testcase?key=6159008335986688
264        // fvar entry triggering overflow:
265        // min: -26335.87451171875 def 8224.12548828125 max 8224.12548828125
266        let test_case = &[
267            79, 84, 84, 79, 0, 1, 32, 32, 255, 32, 32, 32, 102, 118, 97, 114, 32, 32, 32, 32, 0, 0,
268            0, 28, 0, 0, 0, 41, 32, 0, 0, 0, 0, 1, 32, 32, 0, 2, 32, 32, 32, 32, 0, 0, 32, 32, 32,
269            32, 32, 0, 0, 0, 0, 153, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
270        ];
271        let font = FontRef::new(test_case).unwrap();
272        let fvar = font.fvar().unwrap();
273        let axis = fvar.axes().unwrap()[1];
274        // Should not panic with "attempt to subtract with overflow".
275        assert_eq!(
276            axis.normalize(Fixed::from_f64(0.0)),
277            Fixed::from_f64(-0.2509765625)
278        );
279    }
280
281    #[test]
282    fn user_to_normalized() {
283        let font = FontRef::from_index(font_test_data::VAZIRMATN_VAR, 0).unwrap();
284        let fvar = font.fvar().unwrap();
285        let avar = font.avar().ok();
286        let wght = Tag::new(b"wght");
287        let axis = fvar.axes().unwrap()[0];
288        let mut normalized_coords = [F2Dot14::default(); 1];
289        // avar table maps 0.8 to 0.83875
290        let avar_user = axis.default_value().to_f32()
291            + (axis.max_value().to_f32() - axis.default_value().to_f32()) * 0.8;
292        let avar_normalized = 0.83875;
293        #[rustfmt::skip]
294        let cases = [
295            // (user, normalized)
296            (-1000.0, -1.0f32),
297            (100.0, -1.0),
298            (200.0, -0.5),
299            (400.0, 0.0),
300            (900.0, 1.0),
301            (avar_user, avar_normalized),
302            (1251.5, 1.0),
303        ];
304        for (user, normalized) in cases {
305            fvar.user_to_normalized(
306                avar.as_ref(),
307                [(wght, Fixed::from_f64(user as f64))],
308                &mut normalized_coords,
309            );
310            assert_eq!(normalized_coords[0], F2Dot14::from_f32(normalized));
311        }
312    }
313
314    #[test]
315    fn avar2() {
316        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
317        let avar = font.avar().ok();
318        let fvar = font.fvar().unwrap();
319        let avar_axis = Tag::new(b"AVAR");
320        let avwk_axis = Tag::new(b"AVWK");
321        let mut normalized_coords = [F2Dot14::default(); 2];
322        let cases = [
323            ((100.0, 0.0), (1.0, 1.0)),
324            ((50.0, 0.0), (0.5, 0.5)),
325            ((0.0, 50.0), (0.0, 0.5)),
326        ];
327        for (user, expected) in cases {
328            fvar.user_to_normalized(
329                avar.as_ref(),
330                [
331                    (avar_axis, Fixed::from_f64(user.0)),
332                    (avwk_axis, Fixed::from_f64(user.1)),
333                ],
334                &mut normalized_coords,
335            );
336            assert_eq!(normalized_coords[0], F2Dot14::from_f32(expected.0));
337            assert_eq!(normalized_coords[1], F2Dot14::from_f32(expected.1));
338        }
339    }
340
341    #[test]
342    fn avar2_no_panic_with_wrong_size_coords_array() {
343        // this font has 2 axes
344        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
345        let avar = font.avar().ok();
346        let fvar = font.fvar().unwrap();
347        // output array too small
348        let mut normalized_coords = [F2Dot14::default(); 1];
349        fvar.user_to_normalized(avar.as_ref(), [], &mut normalized_coords);
350        // output array too large
351        let mut normalized_coords = [F2Dot14::default(); 4];
352        fvar.user_to_normalized(avar.as_ref(), [], &mut normalized_coords);
353    }
354
355    #[test]
356    fn avar2_preserves_16_16_precision_until_final_rounding() {
357        // Quantizing to 2.14 before applying the avar2 delta would produce 0x0002
358        // here, but HarfBuzz's 16.16 path produces 0x0001.
359        let coord = Fixed::from_bits(3);
360        assert_eq!(
361            super::apply_avar2_delta(coord, 0.5).to_f2dot14(),
362            F2Dot14::from_bits(1)
363        );
364    }
365
366    #[test]
367    fn avar2_clamps_hidden_axis_for_amstelvar_repro() {
368        let font = FontRef::new(font_test_data::AMSTELVAR_AVAR2_A).unwrap();
369        let avar = font.avar().ok();
370        let fvar = font.fvar().unwrap();
371        let mut normalized_coords = [F2Dot14::ZERO; 12];
372
373        fvar.user_to_normalized(
374            avar.as_ref(),
375            [(Tag::new(b"wght"), Fixed::from_f64(1000.0))],
376            &mut normalized_coords,
377        );
378
379        assert_eq!(normalized_coords[1], F2Dot14::from_bits(-5370)); // XTRA
380        assert_eq!(normalized_coords[2], F2Dot14::ONE); // XOPQ clamped from > 1
381        assert_eq!(normalized_coords[3], F2Dot14::from_bits(2254)); // YOPQ
382        assert_eq!(normalized_coords[11], F2Dot14::ONE); // wght
383    }
384}