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