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::{
9    avar::Avar,
10    variations::{DeltaSetIndex, FloatItemDeltaTarget},
11};
12pub use instance_record::InstanceRecord;
13
14impl<'a> Fvar<'a> {
15    /// Returns the array of variation axis records.
16    pub fn axes(&self) -> Result<&'a [VariationAxisRecord], ReadError> {
17        Ok(self.axis_instance_arrays()?.axes())
18    }
19
20    /// Returns the array of instance records.
21    pub fn instances(&self) -> Result<ComputedArray<'a, InstanceRecord<'a>>, ReadError> {
22        Ok(self.axis_instance_arrays()?.instances())
23    }
24
25    /// Converts user space coordinates provided by an unordered iterator
26    /// of `(tag, value)` pairs to normalized coordinates in axis list order.
27    ///
28    /// Stores the resulting normalized coordinates in the given slice.
29    ///
30    /// * User coordinate tags that don't match an axis are ignored.
31    /// * User coordinate values are clamped to the range of their associated
32    ///   axis before normalization.
33    /// * If more than one user coordinate is provided for the same tag, the
34    ///   last one is used.
35    /// * If no user coordinate for an axis is provided, the associated
36    ///   coordinate is set to the normalized value 0.0, representing the
37    ///   default location in variation space.
38    /// * The length of `normalized_coords` should equal the number of axes
39    ///   present in the this table. If the length is smaller, axes at
40    ///   out of bounds indices are ignored. If the length is larger, the
41    ///   excess entries will be filled with zeros.
42    ///
43    /// If the [`Avar`] table is provided, applies remapping of coordinates
44    /// according to the specification.
45    pub fn user_to_normalized(
46        &self,
47        avar: Option<&Avar>,
48        user_coords: impl IntoIterator<Item = (Tag, Fixed)>,
49        normalized_coords: &mut [F2Dot14],
50    ) {
51        normalized_coords.fill(F2Dot14::default());
52        let axes = self.axes().unwrap_or_default();
53        let avar_mappings = avar.map(|avar| avar.axis_segment_maps());
54        for user_coord in user_coords {
55            // To permit non-linear interpolation, iterate over all axes to ensure we match
56            // multiple axes with the same tag:
57            // https://github.com/PeterConstable/OT_Drafts/blob/master/NLI/UnderstandingNLI.md
58            // We accept quadratic behavior here to avoid dynamic allocation and with the assumption
59            // that fonts contain a relatively small number of axes.
60            for (i, axis) in axes
61                .iter()
62                .enumerate()
63                .filter(|(_, axis)| axis.axis_tag() == user_coord.0)
64            {
65                if let Some(target_coord) = normalized_coords.get_mut(i) {
66                    let coord = axis.normalize(user_coord.1);
67                    *target_coord = avar_mappings
68                        .as_ref()
69                        .and_then(|mappings| mappings.get(i).transpose().ok())
70                        .flatten()
71                        .map(|mapping| mapping.apply(coord))
72                        .unwrap_or(coord)
73                        .to_f2dot14();
74                }
75            }
76        }
77        let Some(avar) = avar else { return };
78        if avar.version() == MajorMinor::VERSION_1_0 {
79            return;
80        }
81        let var_store = avar.var_store();
82        let var_index_map = avar.axis_index_map();
83
84        let actual_len = axes.len().min(normalized_coords.len());
85        let mut new_coords = [F2Dot14::ZERO; 64];
86        if actual_len > 64 {
87            // No avar2 for monster fonts.
88            // <https://github.com/googlefonts/fontations/issues/1148>
89            return;
90        }
91
92        let new_coords = &mut new_coords[..actual_len];
93        let normalized_coords = &mut normalized_coords[..actual_len];
94        new_coords.copy_from_slice(normalized_coords);
95
96        for (i, v) in normalized_coords.iter().enumerate() {
97            let var_index = if let Some(Ok(ref map)) = var_index_map {
98                map.get(i as u32).ok()
99            } else {
100                Some(DeltaSetIndex {
101                    outer: 0,
102                    inner: i as u16,
103                })
104            };
105            if var_index.is_none() {
106                continue;
107            }
108            if let Some(Ok(varstore)) = var_store.as_ref() {
109                if let Ok(delta) =
110                    varstore.compute_float_delta(var_index.unwrap(), normalized_coords)
111                {
112                    new_coords[i] = F2Dot14::from_f32((*v).apply_float_delta(delta))
113                        .clamp(F2Dot14::MIN, F2Dot14::MAX);
114                }
115            }
116        }
117        normalized_coords.copy_from_slice(new_coords);
118    }
119}
120
121impl VariationAxisRecord {
122    /// Returns a normalized coordinate for the given value.
123    pub fn normalize(&self, mut value: Fixed) -> Fixed {
124        use core::cmp::Ordering::*;
125        let min_value = self.min_value();
126        let default_value = self.default_value();
127        // Make sure max is >= min to avoid potential panic in clamp.
128        let max_value = self.max_value().max(min_value);
129        value = value.clamp(min_value, max_value);
130        value = match value.cmp(&default_value) {
131            Less => {
132                -((default_value.saturating_sub(value)) / (default_value.saturating_sub(min_value)))
133            }
134            Greater => {
135                (value.saturating_sub(default_value)) / (max_value.saturating_sub(default_value))
136            }
137            Equal => Fixed::ZERO,
138        };
139        value.clamp(-Fixed::ONE, Fixed::ONE)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use crate::{FontRef, TableProvider};
146    use types::{F2Dot14, Fixed, NameId, Tag};
147
148    #[test]
149    fn axes() {
150        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
151        let fvar = font.fvar().unwrap();
152        assert_eq!(fvar.axis_count(), 1);
153        let wght = &fvar.axes().unwrap().first().unwrap();
154        assert_eq!(wght.axis_tag(), Tag::new(b"wght"));
155        assert_eq!(wght.min_value(), Fixed::from_f64(100.0));
156        assert_eq!(wght.default_value(), Fixed::from_f64(400.0));
157        assert_eq!(wght.max_value(), Fixed::from_f64(900.0));
158        assert_eq!(wght.flags(), 0);
159        assert_eq!(wght.axis_name_id(), NameId::new(257));
160    }
161
162    #[test]
163    fn instances() {
164        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
165        let fvar = font.fvar().unwrap();
166        assert_eq!(fvar.instance_count(), 9);
167        // There are 9 instances equally spaced from 100.0 to 900.0
168        // with name id monotonically increasing starting at 258.
169        let instances = fvar.instances().unwrap();
170        for i in 0..9 {
171            let value = 100.0 * (i + 1) as f64;
172            let name_id = NameId::new(258 + i as u16);
173            let instance = instances.get(i).unwrap();
174            assert_eq!(instance.coordinates.len(), 1);
175            assert_eq!(
176                instance.coordinates.first().unwrap().get(),
177                Fixed::from_f64(value)
178            );
179            assert_eq!(instance.subfamily_name_id, name_id);
180            assert_eq!(instance.post_script_name_id, None);
181        }
182    }
183
184    #[test]
185    fn normalize() {
186        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
187        let fvar = font.fvar().unwrap();
188        let axis = fvar.axes().unwrap().first().unwrap();
189        let values = [100.0, 220.0, 250.0, 400.0, 650.0, 900.0];
190        let expected = [-1.0, -0.60001, -0.5, 0.0, 0.5, 1.0];
191        for (value, expected) in values.into_iter().zip(expected) {
192            assert_eq!(
193                axis.normalize(Fixed::from_f64(value)),
194                Fixed::from_f64(expected)
195            );
196        }
197    }
198
199    #[test]
200    fn normalize_overflow() {
201        // From: https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=69787
202        // & https://oss-fuzz.com/testcase?key=6159008335986688
203        // fvar entry triggering overflow:
204        // min: -26335.87451171875 def 8224.12548828125 max 8224.12548828125
205        let test_case = &[
206            79, 84, 84, 79, 0, 1, 32, 32, 255, 32, 32, 32, 102, 118, 97, 114, 32, 32, 32, 32, 0, 0,
207            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,
208            32, 32, 0, 0, 0, 0, 153, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
209        ];
210        let font = FontRef::new(test_case).unwrap();
211        let fvar = font.fvar().unwrap();
212        let axis = fvar.axes().unwrap()[1];
213        // Should not panic with "attempt to subtract with overflow".
214        assert_eq!(
215            axis.normalize(Fixed::from_f64(0.0)),
216            Fixed::from_f64(-0.2509765625)
217        );
218    }
219
220    #[test]
221    fn user_to_normalized() {
222        let font = FontRef::from_index(font_test_data::VAZIRMATN_VAR, 0).unwrap();
223        let fvar = font.fvar().unwrap();
224        let avar = font.avar().ok();
225        let wght = Tag::new(b"wght");
226        let axis = fvar.axes().unwrap()[0];
227        let mut normalized_coords = [F2Dot14::default(); 1];
228        // avar table maps 0.8 to 0.83875
229        let avar_user = axis.default_value().to_f32()
230            + (axis.max_value().to_f32() - axis.default_value().to_f32()) * 0.8;
231        let avar_normalized = 0.83875;
232        #[rustfmt::skip]
233        let cases = [
234            // (user, normalized)
235            (-1000.0, -1.0f32),
236            (100.0, -1.0),
237            (200.0, -0.5),
238            (400.0, 0.0),
239            (900.0, 1.0),
240            (avar_user, avar_normalized),
241            (1251.5, 1.0),
242        ];
243        for (user, normalized) in cases {
244            fvar.user_to_normalized(
245                avar.as_ref(),
246                [(wght, Fixed::from_f64(user as f64))],
247                &mut normalized_coords,
248            );
249            assert_eq!(normalized_coords[0], F2Dot14::from_f32(normalized));
250        }
251    }
252
253    #[test]
254    fn avar2() {
255        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
256        let avar = font.avar().ok();
257        let fvar = font.fvar().unwrap();
258        let avar_axis = Tag::new(b"AVAR");
259        let avwk_axis = Tag::new(b"AVWK");
260        let mut normalized_coords = [F2Dot14::default(); 2];
261        let cases = [
262            ((100.0, 0.0), (1.0, 1.0)),
263            ((50.0, 0.0), (0.5, 0.5)),
264            ((0.0, 50.0), (0.0, 0.5)),
265        ];
266        for (user, expected) in cases {
267            fvar.user_to_normalized(
268                avar.as_ref(),
269                [
270                    (avar_axis, Fixed::from_f64(user.0)),
271                    (avwk_axis, Fixed::from_f64(user.1)),
272                ],
273                &mut normalized_coords,
274            );
275            assert_eq!(normalized_coords[0], F2Dot14::from_f32(expected.0));
276            assert_eq!(normalized_coords[1], F2Dot14::from_f32(expected.1));
277        }
278    }
279
280    #[test]
281    fn avar2_no_panic_with_wrong_size_coords_array() {
282        // this font has 2 axes
283        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
284        let avar = font.avar().ok();
285        let fvar = font.fvar().unwrap();
286        // output array too small
287        let mut normalized_coords = [F2Dot14::default(); 1];
288        fvar.user_to_normalized(avar.as_ref(), [], &mut normalized_coords);
289        // output array too large
290        let mut normalized_coords = [F2Dot14::default(); 4];
291        fvar.user_to_normalized(avar.as_ref(), [], &mut normalized_coords);
292    }
293}