skrifa/
attribute.rs

1//! Primary attributes typically used for font classification and selection.
2
3use read_fonts::{
4    tables::{
5        head::{Head, MacStyle},
6        os2::{Os2, SelectionFlags},
7        post::Post,
8    },
9    FontRef, TableProvider,
10};
11
12/// Stretch, style and weight attributes of a font.
13///
14/// Variable fonts may contain axes that modify these attributes. The
15/// [new](Self::new) method on this type returns values for the default
16/// instance.
17///
18/// These are derived from values in the
19/// [OS/2](https://learn.microsoft.com/en-us/typography/opentype/spec/os2) if
20/// available. Otherwise, they are retrieved from the
21/// [head](https://learn.microsoft.com/en-us/typography/opentype/spec/head)
22/// table.
23#[derive(Copy, Clone, PartialEq, Debug, Default)]
24pub struct Attributes {
25    pub stretch: Stretch,
26    pub style: Style,
27    pub weight: Weight,
28}
29
30impl Attributes {
31    /// Extracts the stretch, style and weight attributes for the default
32    /// instance of the given font.
33    pub fn new(font: &FontRef) -> Self {
34        if let Ok(os2) = font.os2() {
35            // Prefer values from the OS/2 table if it exists. We also use
36            // the post table to extract the angle for oblique styles.
37            Self::from_os2_post(os2, font.post().ok())
38        } else if let Ok(head) = font.head() {
39            // Otherwise, fall back to the macStyle field of the head table.
40            Self::from_head(head)
41        } else {
42            Self::default()
43        }
44    }
45
46    fn from_os2_post(os2: Os2, post: Option<Post>) -> Self {
47        let stretch = Stretch::from_width_class(os2.us_width_class());
48        // Bits 1 and 9 of the fsSelection field signify italic and
49        // oblique, respectively.
50        // See: <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#fsselection>
51        let fs_selection = os2.fs_selection();
52        let style = if fs_selection.contains(SelectionFlags::ITALIC) {
53            Style::Italic
54        } else if fs_selection.contains(SelectionFlags::OBLIQUE) {
55            let angle = post.map(|post| post.italic_angle().to_f64() as f32);
56            Style::Oblique(angle)
57        } else {
58            Style::Normal
59        };
60        // The usWeightClass field is specified with a 1-1000 range, but
61        // we don't clamp here because variable fonts could potentially
62        // have a value outside of that range.
63        // See <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass>
64        let weight = Weight(os2.us_weight_class() as f32);
65        Self {
66            stretch,
67            style,
68            weight,
69        }
70    }
71
72    fn from_head(head: Head) -> Self {
73        let mac_style = head.mac_style();
74        let style = mac_style
75            .contains(MacStyle::ITALIC)
76            .then_some(Style::Italic)
77            .unwrap_or_default();
78        let weight = mac_style
79            .contains(MacStyle::BOLD)
80            .then_some(Weight::BOLD)
81            .unwrap_or_default();
82        Self {
83            stretch: Stretch::default(),
84            style,
85            weight,
86        }
87    }
88}
89
90/// Visual width of a font-- a relative change from the normal aspect
91/// ratio, typically in the range 0.5 to 2.0.
92///
93/// In variable fonts, this can be controlled with the `wdth` axis.
94///
95/// See <https://fonts.google.com/knowledge/glossary/width>
96#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
97pub struct Stretch(f32);
98
99impl Stretch {
100    /// Width that is 50% of normal.
101    pub const ULTRA_CONDENSED: Self = Self(0.5);
102
103    /// Width that is 62.5% of normal.
104    pub const EXTRA_CONDENSED: Self = Self(0.625);
105
106    /// Width that is 75% of normal.
107    pub const CONDENSED: Self = Self(0.75);
108
109    /// Width that is 87.5% of normal.
110    pub const SEMI_CONDENSED: Self = Self(0.875);
111
112    /// Width that is 100% of normal.
113    pub const NORMAL: Self = Self(1.0);
114
115    /// Width that is 112.5% of normal.
116    pub const SEMI_EXPANDED: Self = Self(1.125);
117
118    /// Width that is 125% of normal.
119    pub const EXPANDED: Self = Self(1.25);
120
121    /// Width that is 150% of normal.
122    pub const EXTRA_EXPANDED: Self = Self(1.5);
123
124    /// Width that is 200% of normal.
125    pub const ULTRA_EXPANDED: Self = Self(2.0);
126}
127
128impl Stretch {
129    /// Creates a new stretch attribute with the given ratio.
130    pub const fn new(ratio: f32) -> Self {
131        Self(ratio)
132    }
133
134    /// Creates a new stretch attribute from the
135    /// [usWidthClass](<https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass>)
136    /// field of the OS/2 table.
137    fn from_width_class(width_class: u16) -> Self {
138        // The specified range is 1-9 and Skia simply clamps out of range
139        // values. We follow.
140        // See <https://skia.googlesource.com/skia/+/21b7538fe0757d8cda31598bc9e5a6d0b4b54629/include/core/SkFontStyle.h#52>
141        match width_class {
142            0..=1 => Stretch::ULTRA_CONDENSED,
143            2 => Stretch::EXTRA_CONDENSED,
144            3 => Stretch::CONDENSED,
145            4 => Stretch::SEMI_CONDENSED,
146            5 => Stretch::NORMAL,
147            6 => Stretch::SEMI_EXPANDED,
148            7 => Stretch::EXPANDED,
149            8 => Stretch::EXTRA_EXPANDED,
150            _ => Stretch::ULTRA_EXPANDED,
151        }
152    }
153
154    /// Returns the stretch attribute as a ratio.
155    ///
156    /// This is a linear scaling factor with 1.0 being "normal" width.
157    pub const fn ratio(self) -> f32 {
158        self.0
159    }
160
161    /// Returns the stretch attribute as a percentage value.
162    ///
163    /// This is generally the value associated with the `wdth` axis.
164    pub fn percentage(self) -> f32 {
165        self.0 * 100.0
166    }
167}
168
169impl Default for Stretch {
170    fn default() -> Self {
171        Self::NORMAL
172    }
173}
174
175/// Visual style or 'slope' of a font.
176///
177/// In variable fonts, this can be controlled with the `ital`
178/// and `slnt` axes for italic and oblique styles, respectively.
179///
180/// See <https://fonts.google.com/knowledge/glossary/style>
181#[derive(Copy, Clone, PartialEq, Default, Debug)]
182pub enum Style {
183    /// An upright or "roman" style.
184    #[default]
185    Normal,
186    /// Generally a slanted style, originally based on semi-cursive forms.
187    /// This often has a different structure from the normal style.
188    Italic,
189    /// Oblique (or slanted) style with an optional angle in degrees,
190    /// counter-clockwise from the vertical.
191    Oblique(Option<f32>),
192}
193
194/// Visual weight class of a font, typically on a scale from 1.0 to 1000.0.
195///
196/// In variable fonts, this can be controlled with the `wght` axis.
197///
198/// See <https://fonts.google.com/knowledge/glossary/weight>
199#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
200pub struct Weight(f32);
201
202impl Weight {
203    /// Weight value of 100.
204    pub const THIN: Self = Self(100.0);
205
206    /// Weight value of 200.
207    pub const EXTRA_LIGHT: Self = Self(200.0);
208
209    /// Weight value of 300.
210    pub const LIGHT: Self = Self(300.0);
211
212    /// Weight value of 350.
213    pub const SEMI_LIGHT: Self = Self(350.0);
214
215    /// Weight value of 400.
216    pub const NORMAL: Self = Self(400.0);
217
218    /// Weight value of 500.
219    pub const MEDIUM: Self = Self(500.0);
220
221    /// Weight value of 600.
222    pub const SEMI_BOLD: Self = Self(600.0);
223
224    /// Weight value of 700.
225    pub const BOLD: Self = Self(700.0);
226
227    /// Weight value of 800.
228    pub const EXTRA_BOLD: Self = Self(800.0);
229
230    /// Weight value of 900.
231    pub const BLACK: Self = Self(900.0);
232
233    /// Weight value of 950.
234    pub const EXTRA_BLACK: Self = Self(950.0);
235}
236
237impl Weight {
238    /// Creates a new weight attribute with the given value.
239    pub const fn new(weight: f32) -> Self {
240        Self(weight)
241    }
242
243    /// Returns the underlying weight value.
244    pub const fn value(self) -> f32 {
245        self.0
246    }
247}
248
249impl Default for Weight {
250    fn default() -> Self {
251        Self::NORMAL
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::prelude::*;
259
260    #[test]
261    fn missing_os2() {
262        let font = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
263        let attrs = font.attributes();
264        assert_eq!(attrs.stretch, Stretch::NORMAL);
265        assert_eq!(attrs.style, Style::Italic);
266        assert_eq!(attrs.weight, Weight::BOLD);
267    }
268
269    #[test]
270    fn so_stylish() {
271        let font = FontRef::new(font_test_data::CMAP14_FONT1).unwrap();
272        let attrs = font.attributes();
273        assert_eq!(attrs.stretch, Stretch::SEMI_CONDENSED);
274        assert_eq!(attrs.style, Style::Oblique(Some(-14.0)));
275        assert_eq!(attrs.weight, Weight::EXTRA_BOLD);
276    }
277}