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}