skrifa/
variation.rs

1//! Axes of variation in a variable font.
2
3use read_fonts::{
4    tables::avar::Avar,
5    tables::fvar::{self, Fvar},
6    types::{Fixed, Tag},
7    FontRef, TableProvider,
8};
9
10use crate::{
11    collections::SmallVec,
12    instance::{Location, NormalizedCoord},
13    setting::VariationSetting,
14    string::StringId,
15};
16
17/// Axis of variation in a variable font.
18///
19/// In variable fonts, an axis usually refers to a single aspect of a
20/// typeface's design that can be altered by the user.
21///
22/// See <https://fonts.google.com/knowledge/glossary/axis_in_variable_fonts>
23#[derive(Clone)]
24pub struct Axis {
25    index: usize,
26    record: fvar::VariationAxisRecord,
27}
28
29impl Axis {
30    /// Returns the tag that identifies the axis.
31    pub fn tag(&self) -> Tag {
32        self.record.axis_tag()
33    }
34
35    /// Returns the index of the axis in its owning collection.
36    pub fn index(&self) -> usize {
37        self.index
38    }
39
40    /// Returns the localized string identifier for the name of the axis.
41    pub fn name_id(&self) -> StringId {
42        self.record.axis_name_id()
43    }
44
45    /// Returns true if the axis should be hidden in user interfaces.
46    pub fn is_hidden(&self) -> bool {
47        const AXIS_HIDDEN_FLAG: u16 = 0x1;
48        self.record.flags() & AXIS_HIDDEN_FLAG != 0
49    }
50
51    /// Returns the minimum value of the axis.
52    pub fn min_value(&self) -> f32 {
53        self.record.min_value().to_f64() as _
54    }
55
56    /// Returns the default value of the axis.
57    pub fn default_value(&self) -> f32 {
58        self.record.default_value().to_f64() as _
59    }
60
61    /// Returns the maximum value of the axis.
62    pub fn max_value(&self) -> f32 {
63        self.record.max_value().to_f64() as _
64    }
65
66    /// Returns a normalized coordinate for the given user coordinate.
67    ///
68    /// The value will be clamped to the range specified by the minimum
69    /// and maximum values.
70    ///    
71    /// This does not apply any axis variation remapping.
72    pub fn normalize(&self, coord: f32) -> NormalizedCoord {
73        self.record
74            .normalize(Fixed::from_f64(coord as _))
75            .to_f2dot14()
76    }
77}
78
79/// Collection of axes in a variable font.
80///
81/// Converts user ([fvar](https://learn.microsoft.com/en-us/typography/opentype/spec/fvar))
82/// locations to normalized locations. See [`Self::location`].
83///
84/// See the [`Axis`] type for more detail.
85#[derive(Clone)]
86pub struct AxisCollection<'a> {
87    fvar: Option<Fvar<'a>>,
88    avar: Option<Avar<'a>>,
89}
90
91impl<'a> AxisCollection<'a> {
92    /// Creates a new axis collection from the given font.
93    pub fn new(font: &FontRef<'a>) -> Self {
94        let fvar = font.fvar().ok();
95        let avar = font.avar().ok();
96        Self { fvar, avar }
97    }
98
99    /// Returns the number of variation axes in the font.
100    pub fn len(&self) -> usize {
101        self.fvar
102            .as_ref()
103            .map(|fvar| fvar.axis_count() as usize)
104            .unwrap_or(0)
105    }
106
107    /// Returns true if the collection is empty.
108    pub fn is_empty(&self) -> bool {
109        self.len() == 0
110    }
111
112    /// Returns the axis at the given index.
113    pub fn get(&self, index: usize) -> Option<Axis> {
114        let record = *self.fvar.as_ref()?.axes().ok()?.get(index)?;
115        Some(Axis { index, record })
116    }
117
118    /// Returns the axis with the given tag.
119    ///
120    /// # Examples
121    ///
122    /// ```rust
123    /// # use skrifa::prelude::*;
124    /// # fn wrapper(font: &FontRef) {
125    /// let opsz = Tag::new(b"opsz");
126    /// assert_eq!(font.axes().get_by_tag(opsz).unwrap().tag(), opsz);
127    /// # }
128    /// ```
129    pub fn get_by_tag(&self, tag: Tag) -> Option<Axis> {
130        self.iter().find(|axis| axis.tag() == tag)
131    }
132
133    /// Given an iterator of variation settings in user space, computes an
134    /// ordered sequence of normalized coordinates.
135    ///
136    /// * Setting selectors that don't match an axis are ignored.
137    /// * Setting values are clamped to the range of their associated axis
138    ///   before normalization.
139    /// * If more than one setting for an axis is provided, the last one is
140    ///   used.
141    /// * Omitted settings are set to 0.0, representing the default position
142    ///   in variation space.
143    ///
144    /// # Examples
145    ///
146    /// ```rust
147    /// # use skrifa::prelude::*;
148    /// # fn wrapper(font: &FontRef) {
149    /// let location = font.axes().location([("wght", 250.0), ("wdth", 75.0)]);
150    /// # }
151    /// ```
152    pub fn location<I>(&self, settings: I) -> Location
153    where
154        I: IntoIterator,
155        I::Item: Into<VariationSetting>,
156    {
157        let mut location = Location::new(self.len());
158        self.location_to_slice(settings, location.coords_mut());
159        location
160    }
161
162    /// Given an iterator of variation settings in user space, computes an
163    /// ordered sequence of normalized coordinates and stores them in the
164    /// target slice.
165    ///
166    /// * Setting selectors that don't match an axis are ignored.
167    /// * Setting values are clamped to the range of their associated axis
168    ///   before normalization.
169    /// * If more than one setting for an axis is provided, the last one is
170    ///   used.
171    /// * If no setting for an axis is provided, the associated coordinate is
172    ///   set to the normalized value 0.0, representing the default position
173    ///   in variation space.
174    ///
175    /// # Examples
176    ///
177    /// ```rust
178    /// # use skrifa::prelude::*;
179    /// # fn wrapper(font: &FontRef) {
180    /// let axes = font.axes();
181    /// let mut location = vec![NormalizedCoord::default(); axes.len()];
182    /// axes.location_to_slice([("wght", 250.0), ("wdth", 75.0)], &mut location);
183    /// # }
184    /// ```
185    pub fn location_to_slice<I>(&self, settings: I, location: &mut [NormalizedCoord])
186    where
187        I: IntoIterator,
188        I::Item: Into<VariationSetting>,
189    {
190        if let Some(fvar) = self.fvar.as_ref() {
191            fvar.user_to_normalized(
192                self.avar.as_ref(),
193                settings
194                    .into_iter()
195                    .map(|setting| setting.into())
196                    .map(|setting| (setting.selector, Fixed::from_f64(setting.value as f64))),
197                location,
198            );
199        } else {
200            location.fill(NormalizedCoord::default());
201        }
202    }
203
204    /// Given an iterator of variation settings in user space, returns a
205    /// new iterator yielding those settings that are valid for this axis
206    /// collection.
207    ///
208    /// * Setting selectors that don't match an axis are dropped.
209    /// * If more than one setting for an axis is provided, the last one is
210    ///   retained.
211    /// * Setting values are clamped to the range of their associated axis.
212    ///
213    /// # Examples
214    ///
215    /// ```rust
216    /// # use skrifa::prelude::*;
217    /// # fn wrapper(font: &FontRef) {
218    /// // Assuming a font contains a single "wght" (weight) axis with range
219    /// // 100-900:
220    /// let axes = font.axes();
221    /// let filtered: Vec<_> = axes
222    ///     .filter([("wght", 400.0), ("opsz", 100.0), ("wght", 1200.0)])
223    ///     .collect();
224    /// // The first "wght" and "opsz" settings are dropped and the final
225    /// // "wght" axis is clamped to the maximum value of 900.
226    /// assert_eq!(&filtered, &[("wght", 900.0).into()]);
227    /// # }
228    /// ```
229    pub fn filter<I>(&self, settings: I) -> impl Iterator<Item = VariationSetting> + Clone
230    where
231        I: IntoIterator,
232        I::Item: Into<VariationSetting>,
233    {
234        #[derive(Copy, Clone, Default)]
235        struct Entry {
236            tag: Tag,
237            min: f32,
238            max: f32,
239            value: f32,
240            present: bool,
241        }
242        let mut results = SmallVec::<_, 8>::with_len(self.len(), Entry::default());
243        for (axis, result) in self.iter().zip(results.as_mut_slice()) {
244            result.tag = axis.tag();
245            result.min = axis.min_value();
246            result.max = axis.max_value();
247            result.value = axis.default_value();
248        }
249        for setting in settings {
250            let setting = setting.into();
251            for entry in results.as_mut_slice() {
252                if entry.tag == setting.selector {
253                    entry.value = setting.value.max(entry.min).min(entry.max);
254                    entry.present = true;
255                }
256            }
257        }
258        results
259            .into_iter()
260            .filter(|entry| entry.present)
261            .map(|entry| VariationSetting::new(entry.tag, entry.value))
262    }
263
264    /// Returns an iterator over the axes in the collection.
265    pub fn iter(&self) -> impl Iterator<Item = Axis> + 'a + Clone {
266        let copy = self.clone();
267        (0..self.len()).filter_map(move |i| copy.get(i))
268    }
269}
270
271/// Named instance of a variation.
272///
273/// A set of fixed axis positions selected by the type designer and assigned a
274/// name.
275///
276/// See <https://fonts.google.com/knowledge/glossary/instance>
277#[derive(Clone)]
278pub struct NamedInstance<'a> {
279    axes: AxisCollection<'a>,
280    record: fvar::InstanceRecord<'a>,
281}
282
283impl<'a> NamedInstance<'a> {
284    /// Returns the string identifier for the subfamily name of the instance.
285    pub fn subfamily_name_id(&self) -> StringId {
286        self.record.subfamily_name_id
287    }
288
289    /// Returns the string identifier for the PostScript name of the instance.
290    pub fn postscript_name_id(&self) -> Option<StringId> {
291        self.record.post_script_name_id
292    }
293
294    /// Returns an iterator over the ordered sequence of user space coordinates
295    /// that define the instance, one coordinate per axis.
296    pub fn user_coords(&self) -> impl Iterator<Item = f32> + 'a + Clone {
297        self.record
298            .coordinates
299            .iter()
300            .map(|coord| coord.get().to_f64() as _)
301    }
302
303    /// Computes a location in normalized variation space for this instance.
304    ///
305    /// # Examples
306    ///
307    /// ```rust
308    /// # use skrifa::prelude::*;
309    /// # fn wrapper(font: &FontRef) {
310    /// let location = font.named_instances().get(0).unwrap().location();
311    /// # }
312    /// ```
313    pub fn location(&self) -> Location {
314        let mut location = Location::new(self.axes.len());
315        self.location_to_slice(location.coords_mut());
316        location
317    }
318
319    /// Computes a location in normalized variation space for this instance and
320    /// stores the result in the given slice.
321    ///
322    /// # Examples
323    ///
324    /// ```rust
325    /// # use skrifa::prelude::*;
326    /// # fn wrapper(font: &FontRef) {
327    /// let instance = font.named_instances().get(0).unwrap();
328    /// let mut location = vec![NormalizedCoord::default(); instance.user_coords().count()];
329    /// instance.location_to_slice(&mut location);
330    /// # }
331    /// ```
332    pub fn location_to_slice(&self, location: &mut [NormalizedCoord]) {
333        let settings = self
334            .axes
335            .iter()
336            .map(|axis| axis.tag())
337            .zip(self.user_coords());
338        self.axes.location_to_slice(settings, location);
339    }
340}
341
342/// Collection of named instances in a variable font.
343///
344/// See the [`NamedInstance`] type for more detail.
345#[derive(Clone)]
346pub struct NamedInstanceCollection<'a> {
347    axes: AxisCollection<'a>,
348}
349
350impl<'a> NamedInstanceCollection<'a> {
351    /// Creates a new instance collection from the given font.
352    pub fn new(font: &FontRef<'a>) -> Self {
353        Self {
354            axes: AxisCollection::new(font),
355        }
356    }
357
358    /// Returns the number of instances in the collection.
359    pub fn len(&self) -> usize {
360        self.axes
361            .fvar
362            .as_ref()
363            .map(|fvar| fvar.instance_count() as usize)
364            .unwrap_or(0)
365    }
366
367    /// Returns true if the collection is empty.
368    pub fn is_empty(&self) -> bool {
369        self.len() == 0
370    }
371
372    /// Returns the instance at the given index.
373    pub fn get(&self, index: usize) -> Option<NamedInstance<'a>> {
374        let record = self.axes.fvar.as_ref()?.instances().ok()?.get(index).ok()?;
375        Some(NamedInstance {
376            axes: self.axes.clone(),
377            record,
378        })
379    }
380
381    /// Returns an iterator over the instances in the collection.
382    pub fn iter(&self) -> impl Iterator<Item = NamedInstance<'a>> + 'a + Clone {
383        let copy = self.clone();
384        (0..self.len()).filter_map(move |i| copy.get(i))
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use crate::MetadataProvider as _;
392    use font_test_data::VAZIRMATN_VAR;
393    use read_fonts::FontRef;
394    use std::str::FromStr;
395
396    #[test]
397    fn axis() {
398        let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
399        let axis = font.axes().get(0).unwrap();
400        assert_eq!(axis.index(), 0);
401        assert_eq!(axis.tag(), Tag::new(b"wght"));
402        assert_eq!(axis.min_value(), 100.0);
403        assert_eq!(axis.default_value(), 400.0);
404        assert_eq!(axis.max_value(), 900.0);
405        assert_eq!(axis.name_id(), StringId::new(257));
406        assert_eq!(
407            font.localized_strings(axis.name_id())
408                .english_or_first()
409                .unwrap()
410                .to_string(),
411            "Weight"
412        );
413    }
414
415    #[test]
416    fn named_instances() {
417        let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
418        let named_instances = font.named_instances();
419        let thin = named_instances.get(0).unwrap();
420        assert_eq!(thin.subfamily_name_id(), StringId::new(258));
421        assert_eq!(
422            font.localized_strings(thin.subfamily_name_id())
423                .english_or_first()
424                .unwrap()
425                .to_string(),
426            "Thin"
427        );
428        assert_eq!(thin.location().coords(), &[NormalizedCoord::from_f32(-1.0)]);
429        let regular = named_instances.get(3).unwrap();
430        assert_eq!(regular.subfamily_name_id(), StringId::new(261));
431        assert_eq!(
432            font.localized_strings(regular.subfamily_name_id())
433                .english_or_first()
434                .unwrap()
435                .to_string(),
436            "Regular"
437        );
438        assert_eq!(
439            regular.location().coords(),
440            &[NormalizedCoord::from_f32(0.0)]
441        );
442        let bold = named_instances.get(6).unwrap();
443        assert_eq!(bold.subfamily_name_id(), StringId::new(264));
444        assert_eq!(
445            font.localized_strings(bold.subfamily_name_id())
446                .english_or_first()
447                .unwrap()
448                .to_string(),
449            "Bold"
450        );
451        assert_eq!(
452            bold.location().coords(),
453            &[NormalizedCoord::from_f32(0.6776123)]
454        );
455    }
456
457    #[test]
458    fn location() {
459        let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
460        let axes = font.axes();
461        let axis = axes.get_by_tag(Tag::from_str("wght").unwrap()).unwrap();
462        assert_eq!(
463            axes.location([("wght", -1000.0)]).coords(),
464            &[NormalizedCoord::from_f32(-1.0)]
465        );
466        assert_eq!(
467            axes.location([("wght", 100.0)]).coords(),
468            &[NormalizedCoord::from_f32(-1.0)]
469        );
470        assert_eq!(
471            axes.location([("wght", 200.0)]).coords(),
472            &[NormalizedCoord::from_f32(-0.5)]
473        );
474        assert_eq!(
475            axes.location([("wght", 400.0)]).coords(),
476            &[NormalizedCoord::from_f32(0.0)]
477        );
478        // avar table maps 0.8 to 0.83875
479        assert_eq!(
480            axes.location(&[(
481                "wght",
482                axis.default_value() + (axis.max_value() - axis.default_value()) * 0.8,
483            )])
484            .coords(),
485            &[NormalizedCoord::from_f32(0.83875)]
486        );
487        assert_eq!(
488            axes.location([("wght", 900.0)]).coords(),
489            &[NormalizedCoord::from_f32(1.0)]
490        );
491        assert_eq!(
492            axes.location([("wght", 1251.5)]).coords(),
493            &[NormalizedCoord::from_f32(1.0)]
494        );
495    }
496
497    #[test]
498    fn filter() {
499        let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
500        // This font contains one wght axis with the range 100-900 and default
501        // value of 400.
502        let axes = font.axes();
503        // Drop axes that are not present in the font
504        let drop_missing: Vec<_> = axes.filter(&[("slnt", 25.0), ("wdth", 50.0)]).collect();
505        assert_eq!(&drop_missing, &[]);
506        // Clamp an out of range value
507        let clamp: Vec<_> = axes.filter(&[("wght", 50.0)]).collect();
508        assert_eq!(&clamp, &[("wght", 100.0).into()]);
509        // Combination of the above two: drop the missing axis and clamp out of range value
510        let drop_missing_and_clamp: Vec<_> =
511            axes.filter(&[("slnt", 25.0), ("wght", 1000.0)]).collect();
512        assert_eq!(&drop_missing_and_clamp, &[("wght", 900.0).into()]);
513        // Ensure we take the later value in the case of duplicates
514        let drop_duplicate_and_missing: Vec<_> = axes
515            .filter(&[("wght", 400.0), ("opsz", 100.0), ("wght", 120.5)])
516            .collect();
517        assert_eq!(&drop_duplicate_and_missing, &[("wght", 120.5).into()]);
518    }
519}