skrifa/
instance.rs

1//! Helpers for selecting a font size and location in variation space.
2
3use read_fonts::types::Fixed;
4
5use crate::collections::SmallVec;
6
7/// Type for a normalized variation coordinate.
8pub type NormalizedCoord = read_fonts::types::F2Dot14;
9
10/// Font size in pixels per em units.
11///
12/// Sizes in this crate are represented as a ratio of pixels to the size of
13/// the em square defined by the font. This is equivalent to the `px` unit
14/// in CSS (assuming a DPI scale factor of 1.0).
15///
16/// To retrieve metrics and outlines in font units, use the [unscaled](Self::unscaled)
17/// constructor on this type.
18#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
19pub struct Size(Option<f32>);
20
21impl Size {
22    /// Creates a new font size from the given value in pixels per em units.
23    pub fn new(ppem: f32) -> Self {
24        Self(Some(ppem))
25    }
26
27    /// Creates a new font size for generating unscaled metrics or outlines in
28    /// font units.
29    pub fn unscaled() -> Self {
30        Self(None)
31    }
32
33    /// Returns the raw size in pixels per em units.
34    ///
35    /// Results in `None` if the size is unscaled.
36    pub fn ppem(self) -> Option<f32> {
37        self.0
38    }
39
40    /// Computes a linear scale factor for this font size and the given units
41    /// per em value which can be retrieved from the [Metrics](crate::metrics::Metrics)
42    /// type or from the [head](read_fonts::tables::head::Head) table.
43    ///
44    /// Returns 1.0 for an unscaled size or when `units_per_em` is 0.
45    pub fn linear_scale(self, units_per_em: u16) -> f32 {
46        match self.0 {
47            Some(ppem) if units_per_em != 0 => ppem / units_per_em as f32,
48            _ => 1.0,
49        }
50    }
51
52    /// Computes a fixed point linear scale factor that matches FreeType.
53    pub(crate) fn fixed_linear_scale(self, units_per_em: u16) -> Fixed {
54        // FreeType computes a 16.16 scale factor that converts to 26.6.
55        // This is done in two steps, assuming use of FT_Set_Pixel_Size:
56        // 1) height is multiplied by 64:
57        //    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/49781ab72b2dfd0f78172023921d08d08f323ade/src/base/ftobjs.c#L3596>
58        // 2) this value is divided by UPEM:
59        //    (here, scaled_h=height and h=upem)
60        //    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/49781ab72b2dfd0f78172023921d08d08f323ade/src/base/ftobjs.c#L3312>
61        match self.0 {
62            Some(ppem) if units_per_em > 0 => {
63                Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(units_per_em as i32)
64            }
65            _ => {
66                // This is an identity scale for the pattern
67                // `mul_div(value, scale, 64)`
68                Fixed::from_bits(0x10000 * 64)
69            }
70        }
71    }
72}
73
74/// Reference to an ordered sequence of normalized variation coordinates.
75///
76/// To convert from user coordinates see [`crate::AxisCollection::location`].
77///
78/// This type represents a position in the variation space where each
79/// coordinate corresponds to an axis (in the same order as the `fvar` table)
80/// and is a normalized value in the range `[-1..1]`.
81///
82/// See [Coordinate Scales and Normalization](https://learn.microsoft.com/en-us/typography/opentype/spec/otvaroverview#coordinate-scales-and-normalization)
83/// for further details.
84///
85/// If the array is larger in length than the number of axes, extraneous
86/// values are ignored. If it is smaller, unrepresented axes are assumed to be
87/// at their default positions (i.e. 0).
88///
89/// A value of this type constructed with `default()` represents the default
90/// position for each axis.
91///
92/// Normalized coordinates are ignored for non-variable fonts.
93#[derive(Copy, Clone, Default, Debug)]
94pub struct LocationRef<'a>(&'a [NormalizedCoord]);
95
96impl<'a> LocationRef<'a> {
97    /// Creates a new sequence of normalized coordinates from the given array.
98    pub fn new(coords: &'a [NormalizedCoord]) -> Self {
99        Self(coords)
100    }
101
102    /// Returns the underlying array of normalized coordinates.
103    pub fn coords(&self) -> &'a [NormalizedCoord] {
104        self.0
105    }
106
107    /// Returns true if this represents the default location in variation
108    /// space.
109    ///
110    /// This is represented a set of normalized coordinates that is either
111    /// empty or contains all zeroes.
112    pub fn is_default(&self) -> bool {
113        self.0.is_empty() || self.0.iter().all(|coord| *coord == NormalizedCoord::ZERO)
114    }
115
116    /// Returns the underlying coordinate array if any of the entries are
117    /// non-zero. Otherwise returns the empty slice.
118    ///
119    /// This allows internal routines to bypass expensive variation code
120    /// paths by just checking for an empty slice.
121    pub(crate) fn effective_coords(&self) -> &'a [NormalizedCoord] {
122        if self.is_default() {
123            &[]
124        } else {
125            self.0
126        }
127    }
128}
129
130impl<'a> From<&'a [NormalizedCoord]> for LocationRef<'a> {
131    fn from(value: &'a [NormalizedCoord]) -> Self {
132        Self(value)
133    }
134}
135
136impl<'a> IntoIterator for LocationRef<'a> {
137    type IntoIter = core::slice::Iter<'a, NormalizedCoord>;
138    type Item = &'a NormalizedCoord;
139
140    fn into_iter(self) -> Self::IntoIter {
141        self.0.iter()
142    }
143}
144
145impl<'a> IntoIterator for &'_ LocationRef<'a> {
146    type IntoIter = core::slice::Iter<'a, NormalizedCoord>;
147    type Item = &'a NormalizedCoord;
148
149    fn into_iter(self) -> Self::IntoIter {
150        self.0.iter()
151    }
152}
153
154/// Maximum number of coords to store inline in a `Location` object.
155///
156/// This value was chosen to maximize use of space in the underlying
157/// `SmallVec` storage.
158const MAX_INLINE_COORDS: usize = 8;
159
160/// Ordered sequence of normalized variation coordinates.
161///
162/// To produce from user coordinates see [`crate::AxisCollection::location`].
163///
164/// This is an owned version of [`LocationRef`]. See the documentation on that
165/// type for more detail.
166#[derive(Clone, Debug, Hash, Eq, PartialEq)]
167pub struct Location {
168    coords: SmallVec<NormalizedCoord, MAX_INLINE_COORDS>,
169}
170
171impl Location {
172    /// Creates a new location with the given number of normalized coordinates.
173    ///
174    /// Each element will be initialized to the default value (0.0).
175    pub fn new(len: usize) -> Self {
176        Self {
177            coords: SmallVec::with_len(len, NormalizedCoord::default()),
178        }
179    }
180
181    /// Returns the underlying slice of normalized coordinates.
182    pub fn coords(&self) -> &[NormalizedCoord] {
183        self.coords.as_slice()
184    }
185
186    /// Returns a mutable reference to the underlying slice of normalized
187    /// coordinates.
188    pub fn coords_mut(&mut self) -> &mut [NormalizedCoord] {
189        self.coords.as_mut_slice()
190    }
191}
192
193impl Default for Location {
194    fn default() -> Self {
195        Self {
196            coords: SmallVec::new(),
197        }
198    }
199}
200
201impl<'a> From<&'a Location> for LocationRef<'a> {
202    fn from(value: &'a Location) -> Self {
203        LocationRef(value.coords())
204    }
205}
206
207impl<'a> IntoIterator for &'a Location {
208    type IntoIter = core::slice::Iter<'a, NormalizedCoord>;
209    type Item = &'a NormalizedCoord;
210
211    fn into_iter(self) -> Self::IntoIter {
212        self.coords().iter()
213    }
214}
215
216impl<'a> IntoIterator for &'a mut Location {
217    type IntoIter = core::slice::IterMut<'a, NormalizedCoord>;
218    type Item = &'a mut NormalizedCoord;
219
220    fn into_iter(self) -> Self::IntoIter {
221        self.coords_mut().iter_mut()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::{FontRef, MetadataProvider};
229
230    #[test]
231    fn effective_coords() {
232        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
233        let location = font.axes().location([("AVAR", 50.0), ("AVWK", 75.0)]);
234        let loc_ref = LocationRef::from(&location);
235        assert!(!loc_ref.is_default());
236        assert_eq!(loc_ref.effective_coords().len(), 2);
237    }
238
239    #[test]
240    fn effective_coords_for_default() {
241        let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
242        let location = font.axes().location([("AVAR", 0.0), ("AVWK", 0.0)]);
243        let loc_ref = LocationRef::from(&location);
244        assert!(loc_ref.is_default());
245        assert_eq!(loc_ref.effective_coords().len(), 0);
246        assert_eq!(loc_ref.coords().len(), 2);
247    }
248}