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}