Skip to main content

read_fonts/model/
variation.rs

1//! Variation axes and named instances.
2//!
3//! [`Font::axes`] returns the variation axes of a [`Font`], and
4//! [`Font::named_instances`] returns its named instances. A [`Variation`] is
5//! a value on one axis in user space. [`Font::instance_builder`] creates a
6//! font at a location.
7
8use super::Font;
9use crate::{
10    ps::type1::MultipleMaster,
11    tables::fvar::{Fvar, VariationAxisRecord},
12    types::{BigEndian, Fixed, NameId, Tag},
13    FontData, FontRead, TableProvider,
14};
15use alloc::boxed::Box;
16use core::str::FromStr;
17use smallvec::SmallVec;
18
19/// A normalized variation coordinate in 2.14 fixed point in the range
20/// [-1.0, 1.0].
21pub type NormalizedCoord = types::F2Dot14;
22
23/// A variation setting for a font instance.
24///
25/// The tag identifies the axis, and the value is the desired value for that
26/// axis in user space.
27#[derive(Copy, Clone, PartialEq, Debug)]
28pub struct Variation {
29    /// The tag that identifies the axis.
30    pub tag: Tag,
31    /// The value for the axis in user space.
32    pub value: f32,
33}
34
35impl Variation {
36    /// Creates a new font variation with the given tag and value.
37    pub fn new(tag: Tag, value: f32) -> Self {
38        Self { tag, value }
39    }
40}
41
42// Various conversions for Variation that have proven to be ergonomically
43// useful in practice. These allow, for example, passing &[("wght", 700.0)]
44// directly to the variations() method of InstanceBuilder without needing
45// to manually construct Variation objects or tags.
46
47impl From<&'_ Variation> for Variation {
48    fn from(value: &'_ Variation) -> Self {
49        *value
50    }
51}
52
53impl From<(Tag, f32)> for Variation {
54    fn from(value: (Tag, f32)) -> Self {
55        Self::new(value.0, value.1)
56    }
57}
58
59impl From<&(Tag, f32)> for Variation {
60    fn from(value: &(Tag, f32)) -> Self {
61        Self::new(value.0, value.1)
62    }
63}
64
65impl From<(&str, f32)> for Variation {
66    fn from(value: (&str, f32)) -> Self {
67        Self::new(Tag::from_str(value.0).unwrap_or_default(), value.1)
68    }
69}
70
71impl From<&(&str, f32)> for Variation {
72    fn from(value: &(&str, f32)) -> Self {
73        Self::new(Tag::from_str(value.0).unwrap_or_default(), value.1)
74    }
75}
76
77/// A variation axis, such as weight or width, that a user can change in a
78/// variable font.
79///
80/// See <https://fonts.google.com/knowledge/glossary/axis_in_variable_fonts>
81#[derive(Copy, Clone, Debug)]
82pub struct Axis {
83    index: u32,
84    record: VariationAxisRecord,
85}
86
87impl Axis {
88    /// Returns the tag that identifies the axis.
89    pub fn tag(&self) -> Tag {
90        self.record.axis_tag()
91    }
92
93    /// Returns the index of the axis in the font's list of axes.
94    pub fn index(&self) -> u32 {
95        self.index
96    }
97
98    /// Returns the `name` table ID for the axis name.
99    pub fn name(&self) -> NameId {
100        self.record.axis_name_id()
101    }
102
103    /// Returns `true` if the axis should be hidden in user interfaces.
104    pub fn is_hidden(&self) -> bool {
105        const HIDDEN_AXIS: u16 = 0x1;
106        self.record.flags() & HIDDEN_AXIS != 0
107    }
108
109    /// Returns the minimum value of the axis, in user space.
110    pub fn min_value(&self) -> f32 {
111        self.record.min_value().to_f32()
112    }
113
114    /// Returns the default value of the axis, in user space.
115    pub fn default_value(&self) -> f32 {
116        self.record.default_value().to_f32()
117    }
118
119    /// Returns the maximum value of the axis, in user space.
120    pub fn max_value(&self) -> f32 {
121        self.record.max_value().to_f32()
122    }
123
124    /// Clamps `value` to the range of the axis, as a font instance does.
125    ///
126    /// A maximum below the minimum is raised to it, as in normalization.
127    pub(crate) fn clamp(&self, value: f32) -> f32 {
128        let min = self.min_value();
129        let max = self.max_value().max(min);
130        value.max(min).min(max)
131    }
132}
133
134/// A collection of variation axes.
135///
136/// This is empty if the font is not variable.
137#[derive(Clone, Copy)]
138pub struct Axes<'a> {
139    records: &'a [VariationAxisRecord],
140}
141
142impl<'a> Axes<'a> {
143    pub(crate) fn new(font: &'a Font) -> Self {
144        let records = match font.type1_axes() {
145            Some(records) => records,
146            None => fvar(font)
147                .and_then(|fvar| fvar.axes().ok())
148                .unwrap_or_default(),
149        };
150        Self { records }
151    }
152
153    /// The number of axes.
154    pub fn len(&self) -> u32 {
155        self.records.len() as u32
156    }
157
158    /// Returns `true` if the font has no axes.
159    pub fn is_empty(&self) -> bool {
160        self.records.is_empty()
161    }
162
163    /// Returns the axis at `index`.
164    pub fn get(&self, index: u32) -> Option<Axis> {
165        let record = *self.records.get(index as usize)?;
166        Some(Axis { index, record })
167    }
168
169    /// Returns the first axis with the given tag.
170    pub fn get_by_tag(&self, tag: Tag) -> Option<Axis> {
171        self.iter().find(|axis| axis.tag() == tag)
172    }
173
174    /// Returns the values in `variations` that apply to these axes.
175    ///
176    /// The rules are the same as those used when building a font instance:
177    ///
178    /// - A variation for an axis the font does not have is dropped.
179    /// - If an axis is given more than once, the last value is used.
180    /// - Each value is clamped to the range of its axis.
181    ///
182    /// The result is in the font's axis order and contains only the axes
183    /// that were given.
184    ///
185    /// ```
186    /// # use read_fonts::{model::{Font, Variation}, types::Tag};
187    /// # fn wrapper(font: &Font) {
188    /// // For a font with one `wght` axis from 100 to 900:
189    /// let filtered: Vec<_> = font
190    ///     .axes()
191    ///     .filter([("wght", 400.0), ("opsz", 100.0), ("wght", 1200.0)])
192    ///     .collect();
193    /// assert_eq!(filtered, [Variation::new(Tag::new(b"wght"), 900.0)]);
194    /// # }
195    /// ```
196    pub fn filter<V>(&self, variations: V) -> impl Iterator<Item = Variation> + Clone
197    where
198        V: IntoIterator,
199        V::Item: Into<Variation>,
200    {
201        // One entry per axis, with the value given for it, if any.
202        let mut values: SmallVec<[(Axis, Option<f32>); 8]> =
203            self.iter().map(|axis| (axis, None)).collect();
204        for variation in variations {
205            let variation = variation.into();
206            for (axis, value) in values.iter_mut() {
207                if axis.tag() == variation.tag {
208                    *value = Some(axis.clamp(variation.value));
209                }
210            }
211        }
212        values
213            .into_iter()
214            .filter_map(|(axis, value)| Some(Variation::new(axis.tag(), value?)))
215    }
216
217    /// Returns an iterator over the axes in font order.
218    pub fn iter(&self) -> impl Iterator<Item = Axis> + 'a + Clone {
219        self.records.iter().enumerate().map(|(index, record)| Axis {
220            index: index as u32,
221            record: *record,
222        })
223    }
224}
225
226/// A named location in a variable font, such as "Bold" or "Condensed
227/// Light".
228///
229/// It has one user space value per axis. Use
230/// [`InstanceBuilder::named_instance`](super::InstanceBuilder::named_instance)
231/// to create a font at this location.
232///
233/// See <https://fonts.google.com/knowledge/glossary/instance>
234#[derive(Clone, Copy)]
235pub struct NamedInstance<'a> {
236    index: u32,
237    axes: &'a [VariationAxisRecord],
238    coordinates: &'a [BigEndian<Fixed>],
239    subfamily_name: NameId,
240    postscript_name: Option<NameId>,
241}
242
243impl<'a> NamedInstance<'a> {
244    /// Returns the index of the instance in the font's list of named
245    /// instances.
246    pub fn index(&self) -> u32 {
247        self.index
248    }
249
250    /// Returns the `name` table ID for the instance's subfamily name.
251    pub fn subfamily_name(&self) -> NameId {
252        self.subfamily_name
253    }
254
255    /// Returns the `name` table ID for the instance's PostScript name, if it
256    /// has one.
257    pub fn postscript_name(&self) -> Option<NameId> {
258        self.postscript_name
259    }
260
261    /// Returns the instance's value on each axis, in font axis order.
262    pub fn user_coords(&self) -> impl Iterator<Item = f32> + 'a + Clone {
263        self.coordinates.iter().map(|coord| coord.get().to_f32())
264    }
265
266    /// Returns the instance's value on each axis, paired with the axis tag.
267    pub fn variations(&self) -> impl Iterator<Item = Variation> + 'a + Clone {
268        self.axes
269            .iter()
270            .zip(self.user_coords())
271            .map(|(axis, value)| Variation::new(axis.axis_tag(), value))
272    }
273}
274
275/// A collection of named instances.
276///
277/// This is empty if the font is not variable.
278#[derive(Clone, Copy)]
279pub struct NamedInstances<'a> {
280    /// The data of the `fvar` table, if the font has one.
281    fvar: Option<FontData<'a>>,
282}
283
284impl<'a> NamedInstances<'a> {
285    pub(crate) fn new(font: &'a Font) -> Self {
286        Self {
287            fvar: fvar(font).map(|fvar| fvar.offset_data()),
288        }
289    }
290
291    /// The number of named instances.
292    pub fn len(&self) -> u32 {
293        self.fvar().map_or(0, |fvar| fvar.instance_count() as u32)
294    }
295
296    /// Returns `true` if the font has no named instances.
297    pub fn is_empty(&self) -> bool {
298        self.len() == 0
299    }
300
301    /// Returns the named instance at `index`.
302    pub fn get(&self, index: u32) -> Option<NamedInstance<'a>> {
303        let fvar = self.fvar()?;
304        let record = fvar.instances().ok()?.get(index as usize).ok()?;
305        Some(NamedInstance {
306            index,
307            axes: fvar.axes().ok()?,
308            coordinates: record.coordinates,
309            subfamily_name: record.subfamily_name_id,
310            postscript_name: record.post_script_name_id,
311        })
312    }
313
314    /// Reads the `fvar` table from its data.
315    fn fvar(&self) -> Option<Fvar<'a>> {
316        Fvar::read(self.fvar?).ok()
317    }
318
319    /// Returns an iterator over the named instances in font order.
320    pub fn iter(&self) -> impl Iterator<Item = NamedInstance<'a>> + 'a + Clone {
321        let instances = *self;
322        (0..self.len()).filter_map(move |index| instances.get(index))
323    }
324}
325
326/// Returns the `fvar` table of an SFNT font.
327///
328/// Fonts of other kinds have no `fvar` table.
329fn fvar(font: &Font) -> Option<Fvar<'_>> {
330    font.tables().fvar().ok()
331}
332
333/// Returns axis records for the axes of a Type 1 multiple master font.
334///
335/// The values are design coordinates. An axis named `Weight`, `Width`,
336/// `OpticalSize`, `Slant` or `Italic` takes the registered tag, as in
337/// FreeType, and any other the tag `MMA` followed by its index. The axes
338/// take name IDs from 256, which the font's names resolve to the axis names
339/// in its dictionary.
340pub(crate) fn type1_axis_records(mm: &MultipleMaster) -> Box<[VariationAxisRecord]> {
341    let mut default = [Fixed::ZERO; 4];
342    mm.default_blend(&mut default);
343    mm.axes()
344        .iter()
345        .enumerate()
346        .map(|(index, axis)| {
347            let tag = match axis.name() {
348                Some("Weight") => Tag::new(b"wght"),
349                Some("Width") => Tag::new(b"wdth"),
350                Some("OpticalSize") => Tag::new(b"opsz"),
351                Some("Slant") => Tag::new(b"slnt"),
352                Some("Italic") => Tag::new(b"ital"),
353                _ => Tag::new(&[b'M', b'M', b'A', b'0' + index as u8]),
354            };
355            // Design coordinates beyond what a `Fixed` holds saturate.
356            let design = |value: i32| BigEndian::from(Fixed::from_i32(value.clamp(-32768, 32767)));
357            VariationAxisRecord {
358                axis_tag: BigEndian::from(tag),
359                min_value: design(axis.min_design()),
360                default_value: design(axis.blend_to_design(default[index])),
361                max_value: design(axis.max_design()),
362                flags: BigEndian::from(0),
363                axis_name_id: BigEndian::from(NameId::new(
364                    super::name::TYPE1_AXIS_NAMES + index as u16,
365                )),
366            }
367        })
368        .collect()
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use font_test_data::VAZIRMATN_VAR;
375
376    #[test]
377    fn axes() {
378        let font = Font::new(VAZIRMATN_VAR, 0).unwrap();
379        let axes = font.axes();
380        assert_eq!(axes.len(), 1);
381        let axis = axes.get(0).unwrap();
382        assert_eq!(axis.index(), 0);
383        assert_eq!(axis.tag(), Tag::new(b"wght"));
384        assert_eq!(
385            (axis.min_value(), axis.default_value(), axis.max_value()),
386            (100.0, 400.0, 900.0)
387        );
388        assert_eq!(axis.name(), NameId::new(257));
389        assert!(!axis.is_hidden());
390        assert_eq!(
391            axes.get_by_tag(Tag::new(b"wght")).unwrap().index(),
392            axis.index()
393        );
394        assert!(axes.get_by_tag(Tag::new(b"wdth")).is_none());
395        assert!(axes.get(1).is_none());
396        assert_eq!(axes.iter().count(), 1);
397    }
398
399    #[test]
400    fn filter() {
401        let font = Font::new(VAZIRMATN_VAR, 0).unwrap();
402        // One wght axis from 100 to 900, with a default of 400.
403        let axes = font.axes();
404        let filter = |variations: &[(&str, f32)]| axes.filter(variations).collect::<Vec<_>>();
405        // Axes the font does not have are dropped.
406        assert_eq!(filter(&[("slnt", 25.0), ("wdth", 50.0)]), []);
407        // Values are clamped to the axis.
408        assert_eq!(filter(&[("wght", 50.0)]), [("wght", 100.0).into()]);
409        assert_eq!(
410            filter(&[("slnt", 25.0), ("wght", 1000.0)]),
411            [("wght", 900.0).into()]
412        );
413        // The last value for an axis wins.
414        assert_eq!(
415            filter(&[("wght", 400.0), ("opsz", 100.0), ("wght", 120.5)]),
416            [("wght", 120.5).into()]
417        );
418    }
419
420    // Clamping agrees with normalization, which builds the instance, even
421    // for an axis whose maximum is below its minimum.
422    #[test]
423    fn clamp_matches_normalization() {
424        use crate::types::{BigEndian, Fixed};
425        let axis = |min: f32, default: f32, max: f32| Axis {
426            index: 0,
427            record: VariationAxisRecord {
428                axis_tag: BigEndian::from(Tag::new(b"wght")),
429                min_value: BigEndian::from(Fixed::from_f64(min as f64)),
430                default_value: BigEndian::from(Fixed::from_f64(default as f64)),
431                max_value: BigEndian::from(Fixed::from_f64(max as f64)),
432                flags: BigEndian::from(0),
433                axis_name_id: BigEndian::from(NameId::new(256)),
434            },
435        };
436        let inverted = axis(500.0, 400.0, 100.0);
437        assert_eq!(inverted.clamp(300.0), 500.0);
438        assert_eq!(inverted.clamp(700.0), 500.0);
439        for axis in [inverted, axis(100.0, 400.0, 900.0)] {
440            for value in [-1000.0, 0.0, 100.0, 300.0, 400.0, 500.0, 900.0, 2000.0] {
441                let clamped = Fixed::from_f64(axis.clamp(value) as f64);
442                let normalize = |value: Fixed| axis.record.normalize(value);
443                assert_eq!(
444                    normalize(clamped),
445                    normalize(Fixed::from_f64(value as f64)),
446                    "{value}"
447                );
448            }
449        }
450    }
451
452    #[test]
453    fn named_instances() {
454        let font = Font::new(VAZIRMATN_VAR, 0).unwrap();
455        let instances = font.named_instances();
456        assert_eq!(instances.len() as usize, instances.iter().count());
457        let bold = instances.get(6).unwrap();
458        assert_eq!(bold.index(), 6);
459        assert_eq!(bold.subfamily_name(), NameId::new(264));
460        assert_eq!(bold.user_coords().collect::<Vec<_>>(), [700.0]);
461        assert_eq!(
462            bold.variations().collect::<Vec<_>>(),
463            [Variation::new(Tag::new(b"wght"), 700.0)]
464        );
465        assert!(instances.get(instances.len()).is_none());
466        // The same location the builder reaches by index.
467        let by_index = font.instance_builder().named_instance(6).build();
468        let by_variations = font
469            .instance_builder()
470            .variations(bold.variations())
471            .build();
472        assert_eq!(
473            by_index.normalized_coords(),
474            by_variations.normalized_coords()
475        );
476    }
477
478    #[test]
479    fn a_static_font_has_no_axes() {
480        let font = Font::new(font_test_data::TINOS_SUBSET, 0).unwrap();
481        assert!(font.axes().is_empty());
482        assert!(font.named_instances().is_empty());
483        assert!(font.named_instances().get(0).is_none());
484    }
485}