1 //! Primary attributes typically used for font classification and selection. 2 3 use read_fonts::{ 4 tables::{ 5 head::{Head, MacStyle}, 6 os2::{Os2, SelectionFlags}, 7 post::Post, 8 }, 9 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)] 24 pub struct Attributes { 25 pub stretch: Stretch, 26 pub style: Style, 27 pub weight: Weight, 28 } 29 30 impl Attributes { 31 /// Extracts the stretch, style and weight attributes for the default 32 /// instance of the given font. new<'a>(font: &impl TableProvider<'a>) -> Self33 pub fn new<'a>(font: &impl TableProvider<'a>) -> 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 from_os2_post(os2: Os2, post: Option<Post>) -> Self46 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 from_head(head: Head) -> Self72 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)] 97 pub struct Stretch(f32); 98 99 impl 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 128 impl Stretch { 129 /// Creates a new stretch attribute with the given ratio. new(ratio: f32) -> Self130 pub 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. from_width_class(width_class: u16) -> Self137 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. ratio(self) -> f32157 pub 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. percentage(self) -> f32164 pub fn percentage(self) -> f32 { 165 self.0 * 100.0 166 } 167 } 168 169 impl Default for Stretch { default() -> Self170 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)] 182 pub 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)] 200 pub struct Weight(f32); 201 202 impl 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 237 impl Weight { 238 /// Creates a new weight attribute with the given value. new(weight: f32) -> Self239 pub fn new(weight: f32) -> Self { 240 Self(weight) 241 } 242 243 /// Returns the underlying weight value. value(self) -> f32244 pub fn value(self) -> f32 { 245 self.0 246 } 247 } 248 249 impl Default for Weight { default() -> Self250 fn default() -> Self { 251 Self::NORMAL 252 } 253 } 254 255 #[cfg(test)] 256 mod tests { 257 use super::*; 258 use crate::prelude::*; 259 260 #[test] missing_os2()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] so_stylish()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 } 278