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 = if mac_style.contains(MacStyle::ITALIC) {
75            Style::Italic
76        } else {
77            Default::default()
78        };
79        let weight = if mac_style.contains(MacStyle::BOLD) {
80            Weight::BOLD
81        } else {
82            Default::default()
83        };
84        Self {
85            stretch: Stretch::default(),
86            style,
87            weight,
88        }
89    }
90}
91
92/// Visual width of a font-- a relative change from the normal aspect
93/// ratio, typically in the range 0.5 to 2.0.
94///
95/// In variable fonts, this can be controlled with the `wdth` axis.
96///
97/// See <https://fonts.google.com/knowledge/glossary/width>
98#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
99pub struct Stretch(f32);
100
101impl Stretch {
102    /// Width that is 50% of normal.
103    pub const ULTRA_CONDENSED: Self = Self(0.5);
104
105    /// Width that is 62.5% of normal.
106    pub const EXTRA_CONDENSED: Self = Self(0.625);
107
108    /// Width that is 75% of normal.
109    pub const CONDENSED: Self = Self(0.75);
110
111    /// Width that is 87.5% of normal.
112    pub const SEMI_CONDENSED: Self = Self(0.875);
113
114    /// Width that is 100% of normal.
115    pub const NORMAL: Self = Self(1.0);
116
117    /// Width that is 112.5% of normal.
118    pub const SEMI_EXPANDED: Self = Self(1.125);
119
120    /// Width that is 125% of normal.
121    pub const EXPANDED: Self = Self(1.25);
122
123    /// Width that is 150% of normal.
124    pub const EXTRA_EXPANDED: Self = Self(1.5);
125
126    /// Width that is 200% of normal.
127    pub const ULTRA_EXPANDED: Self = Self(2.0);
128}
129
130impl Stretch {
131    /// Creates a new stretch attribute with the given ratio.
132    pub const fn new(ratio: f32) -> Self {
133        Self(ratio)
134    }
135
136    /// Creates a new stretch attribute from the
137    /// [usWidthClass](<https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass>)
138    /// field of the OS/2 table.
139    fn from_width_class(width_class: u16) -> Self {
140        // The specified range is 1-9 and Skia simply clamps out of range
141        // values. We follow.
142        // See <https://skia.googlesource.com/skia/+/21b7538fe0757d8cda31598bc9e5a6d0b4b54629/include/core/SkFontStyle.h#52>
143        match width_class {
144            0..=1 => Stretch::ULTRA_CONDENSED,
145            2 => Stretch::EXTRA_CONDENSED,
146            3 => Stretch::CONDENSED,
147            4 => Stretch::SEMI_CONDENSED,
148            5 => Stretch::NORMAL,
149            6 => Stretch::SEMI_EXPANDED,
150            7 => Stretch::EXPANDED,
151            8 => Stretch::EXTRA_EXPANDED,
152            _ => Stretch::ULTRA_EXPANDED,
153        }
154    }
155
156    /// Returns the stretch attribute as a ratio.
157    ///
158    /// This is a linear scaling factor with 1.0 being "normal" width.
159    pub const fn ratio(self) -> f32 {
160        self.0
161    }
162
163    /// Returns the stretch attribute as a percentage value.
164    ///
165    /// This is generally the value associated with the `wdth` axis.
166    pub fn percentage(self) -> f32 {
167        self.0 * 100.0
168    }
169}
170
171impl Default for Stretch {
172    fn default() -> Self {
173        Self::NORMAL
174    }
175}
176
177/// Visual style or 'slope' of a font.
178///
179/// In variable fonts, this can be controlled with the `ital`
180/// and `slnt` axes for italic and oblique styles, respectively.
181///
182/// See <https://fonts.google.com/knowledge/glossary/style>
183#[derive(Copy, Clone, PartialEq, Default, Debug)]
184pub enum Style {
185    /// An upright or "roman" style.
186    #[default]
187    Normal,
188    /// Generally a slanted style, originally based on semi-cursive forms.
189    /// This often has a different structure from the normal style.
190    Italic,
191    /// Oblique (or slanted) style with an optional angle in degrees,
192    /// counter-clockwise from the vertical.
193    Oblique(Option<f32>),
194}
195
196/// Visual weight class of a font, typically on a scale from 1.0 to 1000.0.
197///
198/// In variable fonts, this can be controlled with the `wght` axis.
199///
200/// See <https://fonts.google.com/knowledge/glossary/weight>
201#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
202pub struct Weight(f32);
203
204impl Weight {
205    /// Weight value of 100.
206    pub const THIN: Self = Self(100.0);
207
208    /// Weight value of 200.
209    pub const EXTRA_LIGHT: Self = Self(200.0);
210
211    /// Weight value of 300.
212    pub const LIGHT: Self = Self(300.0);
213
214    /// Weight value of 350.
215    pub const SEMI_LIGHT: Self = Self(350.0);
216
217    /// Weight value of 400.
218    pub const NORMAL: Self = Self(400.0);
219
220    /// Weight value of 500.
221    pub const MEDIUM: Self = Self(500.0);
222
223    /// Weight value of 600.
224    pub const SEMI_BOLD: Self = Self(600.0);
225
226    /// Weight value of 700.
227    pub const BOLD: Self = Self(700.0);
228
229    /// Weight value of 800.
230    pub const EXTRA_BOLD: Self = Self(800.0);
231
232    /// Weight value of 900.
233    pub const BLACK: Self = Self(900.0);
234
235    /// Weight value of 950.
236    pub const EXTRA_BLACK: Self = Self(950.0);
237}
238
239impl Weight {
240    /// Creates a new weight attribute with the given value.
241    pub const fn new(weight: f32) -> Self {
242        Self(weight)
243    }
244
245    /// Returns the underlying weight value.
246    pub const fn value(self) -> f32 {
247        self.0
248    }
249}
250
251impl Default for Weight {
252    fn default() -> Self {
253        Self::NORMAL
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::prelude::*;
261
262    #[test]
263    fn missing_os2() {
264        let font = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
265        let attrs = font.attributes();
266        assert_eq!(attrs.stretch, Stretch::NORMAL);
267        assert_eq!(attrs.style, Style::Italic);
268        assert_eq!(attrs.weight, Weight::BOLD);
269    }
270
271    #[test]
272    fn so_stylish() {
273        let font = FontRef::new(font_test_data::CMAP14_FONT1).unwrap();
274        let attrs = font.attributes();
275        assert_eq!(attrs.stretch, Stretch::SEMI_CONDENSED);
276        assert_eq!(attrs.style, Style::Oblique(Some(-14.0)));
277        assert_eq!(attrs.weight, Weight::EXTRA_BOLD);
278    }
279}