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}