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 coords_2dot14 = [F2Dot14::ZERO; MAX_INLINE_AVAR2_AXES];
147        let coords_2dot14 = &mut coords_2dot14[..actual_len];
148        for (coord_2dot14, coord) in coords_2dot14.iter_mut().zip(fixed_coords.iter()) {
149            *coord_2dot14 = coord.to_f2dot14();
150        }
151
152        if let Some(Ok(varstore)) = var_store.as_ref() {
153            for (i, coord) in fixed_coords.iter_mut().enumerate() {
154                let var_index = if let Some(Ok(ref map)) = var_index_map {
155                    match map.get(i as u32) {
156                        Ok(index) => index,
157                        Err(_) => continue,
158                    }
159                } else {
160                    DeltaSetIndex {
161                        outer: 0,
162                        inner: i as u16,
163                    }
164                };
165                if let Ok(delta) = varstore.compute_float_delta(var_index, coords_2dot14) {
166                    *coord =
167                        apply_avar2_delta(*coord, delta.to_f64()).clamp(Fixed::NEG_ONE, Fixed::ONE);
168                }
169            }
170        }
171
172        to_normalized_coords(fixed_coords, normalized_coords);
173    }
174}
175
176impl VariationAxisRecord {
177    /// Returns a normalized coordinate for the given value.
178    pub fn normalize(&self, mut value: Fixed) -> Fixed {
179        use core::cmp::Ordering::*;
180        let min_value = self.min_value();
181        let default_value = self.default_value();
182        // Make sure max is >= min to avoid potential panic in clamp.
183        let max_value = self.max_value().max(min_value);
184        value = value.clamp(min_value, max_value);
185        value = match value.cmp(&default_value) {
186            Less => {
187                -((default_value.saturating_sub(value)) / (default_value.saturating_sub(min_value)))
188            }
189            Greater => {
190                (value.saturating_sub(default_value)) / (max_value.saturating_sub(default_value))
191            }
192            Equal => Fixed::ZERO,
193        };
194        value.clamp(Fixed::NEG_ONE, Fixed::ONE)
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use crate::{FontRef, TableProvider};
201    use types::{F2Dot14, Fixed, NameId, Tag};
202
203    #[test]
204    fn axes() {
205        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
206        let fvar = font.fvar().unwrap();
207        assert_eq!(fvar.axis_count(), 1);
208        let wght = &fvar.axes().unwrap().first().unwrap();
209        assert_eq!(wght.axis_tag(), Tag::new(b"wght"));
210        assert_eq!(wght.min_value(), Fixed::from_f64(100.0));
211        assert_eq!(wght.default_value(), Fixed::from_f64(400.0));
212        assert_eq!(wght.max_value(), Fixed::from_f64(900.0));
213        assert_eq!(wght.flags(), 0);
214        assert_eq!(wght.axis_name_id(), NameId::new(257));
215    }
216
217    #[test]
218    fn instances() {
219        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
220        let fvar = font.fvar().unwrap();
221        assert_eq!(fvar.instance_count(), 9);
222        // There are 9 instances equally spaced from 100.0 to 900.0
223        // with name id monotonically increasing starting at 258.
224        let instances = fvar.instances().unwrap();
225        for i in 0..9 {
226            let value = 100.0 * (i + 1) as f64;
227            let name_id = NameId::new(258 + i as u16);
228            let instance = instances.get(i).unwrap();
229            assert_eq!(instance.coordinates.len(), 1);
230            assert_eq!(
231                instance.coordinates.first().unwrap().get(),
232                Fixed::from_f64(value)
233            );
234            assert_eq!(instance.subfamily_name_id, name_id);
235            assert_eq!(instance.post_script_name_id, None);
236        }
237    }
238
239    #[test]
240    fn normalize() {
241        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
242        let fvar = font.fvar().unwrap();
243        let axis = fvar.axes().unwrap().first().unwrap();
244        let values = [100.0, 220.0, 250.0, 400.0, 650.0, 900.0];
245        let expected = [-1.0, -0.60001, -0.5, 0.0, 0.5, 1.0];
246        for (value, expected) in values.into_iter().zip(expected) {
247            assert_eq!(
248                axis.normalize(Fixed::from_f64(value)),
249                Fixed::from_f64(expected)
250            );
251        }
252    }
253
254    #[test]
255    fn normalize_overflow() {
256        // From: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=69787
257        // & https://oss-fuzz.com/testcase?key=6159008335986688
258        // fvar entry triggering overflow:
259        // min: -26335.87451171875 def 8224.12548828125 max 8224.12548828125
260        let test_case = &[
261            79, 84, 84, 79, 0, 1, 32, 32, 255, 32, 32, 32, 102, 118, 97, 114, 32, 32, 32, 32, 0, 0,
262            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,
263            32, 32, 0, 0, 0, 0, 153, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
264        ];
265        let font = FontRef::new(test_case).unwrap();
266        let fvar = font.fvar().unwrap();
267        let axis = fvar.axes().unwrap()[1];
268        // Should not panic with "attempt to subtract with overflow".
269        assert_eq!(
270            axis.normalize(Fixed::from_f64(0.0)),
271            Fixed::from_f64(-0.2509765625)
272        );
273    }
274
275    #[test]
276    fn user_to_normalized() {
277        let font = FontRef::from_index(font_test_data::VAZIRMATN_VAR, 0).unwrap();
278        let fvar = font.fvar().unwrap();
279        let avar = font.avar().ok();
280        let wght = Tag::new(b"wght");
281        let axis = fvar.axes().unwrap()[0];
282        let mut normalized_coords = [F2Dot14::default(); 1];
283        // avar table maps 0.8 to 0.83875
284        let avar_user = axis.default_value().to_f32()
285            + (axis.max_value().to_f32() - axis.default_value().to_f32()) * 0.8;
286        let avar_normalized = 0.83875;
287        #[rustfmt::skip]
288        let cases = [
289            // (user, normalized)
290            (-1000.0, -1.0f32),
291            (100.0, -1.0),
292            (200.0, -0.5),
293            (400.0, 0.0),
294            (900.0, 1.0),
295            (avar_user, avar_normalized),
296            (1251.5, 1.0),
297        ];
298        for (user, normalized) in cases {
299            fvar.user_to_normalized(
300                avar.as_ref(),
301                [(wght, Fixed::from_f64(user as f64))],
302                &mut normalized_coords,
303            );
304            assert_eq!(normalized_coords[0], F2Dot14::from_f32(normalized));
305        }
306    }
307
308    #[test]
309    fn avar2() {
310        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
311        let avar = font.avar().ok();
312        let fvar = font.fvar().unwrap();
313        let avar_axis = Tag::new(b"AVAR");
314        let avwk_axis = Tag::new(b"AVWK");
315        let mut normalized_coords = [F2Dot14::default(); 2];
316        let cases = [
317            ((100.0, 0.0), (1.0, 1.0)),
318            ((50.0, 0.0), (0.5, 0.5)),
319            ((0.0, 50.0), (0.0, 0.5)),
320        ];
321        for (user, expected) in cases {
322            fvar.user_to_normalized(
323                avar.as_ref(),
324                [
325                    (avar_axis, Fixed::from_f64(user.0)),
326                    (avwk_axis, Fixed::from_f64(user.1)),
327                ],
328                &mut normalized_coords,
329            );
330            assert_eq!(normalized_coords[0], F2Dot14::from_f32(expected.0));
331            assert_eq!(normalized_coords[1], F2Dot14::from_f32(expected.1));
332        }
333    }
334
335    #[test]
336    fn avar2_no_panic_with_wrong_size_coords_array() {
337        // this font has 2 axes
338        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
339        let avar = font.avar().ok();
340        let fvar = font.fvar().unwrap();
341        // output array too small
342        let mut normalized_coords = [F2Dot14::default(); 1];
343        fvar.user_to_normalized(avar.as_ref(), [], &mut normalized_coords);
344        // output array too large
345        let mut normalized_coords = [F2Dot14::default(); 4];
346        fvar.user_to_normalized(avar.as_ref(), [], &mut normalized_coords);
347    }
348
349    #[test]
350    fn avar2_preserves_16_16_precision_until_final_rounding() {
351        // Quantizing to 2.14 before applying the avar2 delta would produce 0x0002
352        // here, but HarfBuzz's 16.16 path produces 0x0001.
353        let coord = Fixed::from_bits(3);
354        assert_eq!(
355            super::apply_avar2_delta(coord, 0.5).to_f2dot14(),
356            F2Dot14::from_bits(1)
357        );
358    }
359
360    #[test]
361    fn avar2_clamps_hidden_axis_for_amstelvar_repro() {
362        let font = FontRef::new(font_test_data::AMSTELVAR_AVAR2_A).unwrap();
363        let avar = font.avar().ok();
364        let fvar = font.fvar().unwrap();
365        let mut normalized_coords = [F2Dot14::ZERO; 12];
366
367        fvar.user_to_normalized(
368            avar.as_ref(),
369            [(Tag::new(b"wght"), Fixed::from_f64(1000.0))],
370            &mut normalized_coords,
371        );
372
373        assert_eq!(normalized_coords[1], F2Dot14::from_bits(-5370)); // XTRA
374        assert_eq!(normalized_coords[2], F2Dot14::ONE); // XOPQ clamped from > 1
375        assert_eq!(normalized_coords[3], F2Dot14::from_bits(2254)); // YOPQ
376        assert_eq!(normalized_coords[11], F2Dot14::ONE); // wght
377    }
378}