1use super::{variation::Variation, Font, Kind};
9use crate::{
10 tables::{
11 head::{Head, MacStyle},
12 os2::{Os2, SelectionFlags},
13 post::Post,
14 },
15 types::Tag,
16 TableProvider,
17};
18
19#[derive(Copy, Clone, PartialEq, Debug, Default)]
21pub struct Attributes {
22 pub width: Width,
24 pub style: Style,
26 pub weight: Weight,
28}
29
30impl Attributes {
31 pub fn from_variations<V>(font: &Font, variations: V) -> Self
40 where
41 V: IntoIterator,
42 V::Item: Into<Variation>,
43 {
44 let mut attributes = font.default_attributes();
45 let axes = font.axes();
46 if axes.is_empty() {
47 return attributes;
48 }
49 let (mut width, mut italic, mut slant, mut weight) = (None, None, None, None);
50 for variation in variations {
51 let variation = variation.into();
52 let slot = match variation.tag {
53 WDTH => &mut width,
54 ITAL => &mut italic,
55 SLNT => &mut slant,
56 WGHT => &mut weight,
57 _ => continue,
58 };
59 for axis in axes.iter().filter(|axis| axis.tag() == variation.tag) {
62 *slot = Some(axis.clamp(variation.value));
63 }
64 }
65 if let Some(width) = width {
66 attributes.width = Width::new(width / 100.0);
67 }
68 attributes.style = style_at(attributes.style, italic, slant);
69 if let Some(weight) = weight {
70 attributes.weight = Weight::new(weight);
71 }
72 attributes
73 }
74
75 pub fn from_named_instance(font: &Font, index: u32) -> Self {
79 Self::from_named_instance_with_overrides(font, index, core::iter::empty::<Variation>())
80 }
81
82 pub fn from_named_instance_with_overrides<V>(font: &Font, index: u32, overrides: V) -> Self
87 where
88 V: IntoIterator,
89 V::Item: Into<Variation>,
90 {
91 let instance = font.named_instances().get(index);
92 let named = instance.iter().flat_map(|instance| instance.variations());
93 Self::from_variations(font, named.chain(overrides.into_iter().map(Into::into)))
94 }
95
96 pub(crate) fn from_font(font: &Font) -> Self {
102 match font.kind() {
103 Kind::Sfnt(..) => Self::from_sfnt(&font.tables()),
104 Kind::Type1(type1) => {
105 Self::from_postscript(type1.weight(), type1.italic_angle() as f32)
106 }
107 Kind::Cff(cff, _) => cff.metadata().map_or_else(Self::default, |metadata| {
108 Self::from_postscript(metadata.weight(), metadata.italic_angle().to_f32())
109 }),
110 }
111 }
112
113 fn from_postscript(weight: Option<&str>, italic_angle: f32) -> Self {
120 Self {
121 width: Width::default(),
122 style: if italic_angle != 0.0 {
123 Style::Italic
124 } else {
125 Style::Normal
126 },
127 weight: weight.map_or_else(Weight::default, Weight::from_name),
128 }
129 }
130
131 fn from_sfnt<'a>(tables: &impl TableProvider<'a>) -> Self {
136 if let Ok(os2) = tables.os2() {
137 Self::from_os2_post(os2, tables.post().ok())
138 } else if let Ok(head) = tables.head() {
139 Self::from_head(head)
140 } else {
141 Self::default()
142 }
143 }
144
145 fn from_os2_post(os2: Os2, post: Option<Post>) -> Self {
146 let width = Width::from_width_class(os2.us_width_class());
147 let fs_selection = os2.fs_selection();
150 let style = if fs_selection.contains(SelectionFlags::ITALIC) {
151 Style::Italic
152 } else if fs_selection.contains(SelectionFlags::OBLIQUE) {
153 Style::Oblique(post.map(|post| post.italic_angle().to_f32()))
154 } else {
155 Style::Normal
156 };
157 let weight = Weight(os2.us_weight_class() as f32);
161 Self {
162 width,
163 style,
164 weight,
165 }
166 }
167
168 fn from_head(head: Head) -> Self {
169 let mac_style = head.mac_style();
170 let style = if mac_style.contains(MacStyle::ITALIC) {
171 Style::Italic
172 } else {
173 Style::default()
174 };
175 let weight = if mac_style.contains(MacStyle::BOLD) {
176 Weight::BOLD
177 } else {
178 Weight::default()
179 };
180 Self {
181 width: Width::default(),
182 style,
183 weight,
184 }
185 }
186}
187
188const WDTH: Tag = Tag::new(b"wdth");
189const ITAL: Tag = Tag::new(b"ital");
190const SLNT: Tag = Tag::new(b"slnt");
191const WGHT: Tag = Tag::new(b"wght");
192
193fn style_at(default: Style, italic: Option<f32>, slant: Option<f32>) -> Style {
200 let mut style = default;
201 match italic {
202 Some(italic) if italic >= 0.5 => style = Style::Italic,
203 Some(_) if style == Style::Italic => style = Style::Normal,
204 _ => {}
205 }
206 match slant {
207 _ if style == Style::Italic => {}
208 Some(slant) if slant != 0.0 => style = Style::Oblique(Some(slant)),
209 Some(_) if matches!(style, Style::Oblique(_)) => style = Style::Normal,
210 _ => {}
211 }
212 style
213}
214
215#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
223pub struct Width(f32);
224
225impl Width {
226 pub const ULTRA_CONDENSED: Self = Self(0.5);
228
229 pub const EXTRA_CONDENSED: Self = Self(0.625);
231
232 pub const CONDENSED: Self = Self(0.75);
234
235 pub const SEMI_CONDENSED: Self = Self(0.875);
237
238 pub const NORMAL: Self = Self(1.0);
240
241 pub const SEMI_EXPANDED: Self = Self(1.125);
243
244 pub const EXPANDED: Self = Self(1.25);
246
247 pub const EXTRA_EXPANDED: Self = Self(1.5);
249
250 pub const ULTRA_EXPANDED: Self = Self(2.0);
252
253 pub const fn new(ratio: f32) -> Self {
255 Self(ratio)
256 }
257
258 fn from_width_class(width_class: u16) -> Self {
262 match width_class {
265 0..=1 => Self::ULTRA_CONDENSED,
266 2 => Self::EXTRA_CONDENSED,
267 3 => Self::CONDENSED,
268 4 => Self::SEMI_CONDENSED,
269 5 => Self::NORMAL,
270 6 => Self::SEMI_EXPANDED,
271 7 => Self::EXPANDED,
272 8 => Self::EXTRA_EXPANDED,
273 _ => Self::ULTRA_EXPANDED,
274 }
275 }
276
277 pub const fn ratio(self) -> f32 {
279 self.0
280 }
281
282 pub fn percentage(self) -> f32 {
285 self.0 * 100.0
286 }
287}
288
289impl Default for Width {
290 fn default() -> Self {
291 Self::NORMAL
292 }
293}
294
295#[derive(Copy, Clone, PartialEq, Default, Debug)]
302pub enum Style {
303 #[default]
305 Normal,
306 Italic,
309 Oblique(Option<f32>),
312}
313
314#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
320pub struct Weight(f32);
321
322impl Weight {
323 pub const THIN: Self = Self(100.0);
325
326 pub const EXTRA_LIGHT: Self = Self(200.0);
328
329 pub const LIGHT: Self = Self(300.0);
331
332 pub const SEMI_LIGHT: Self = Self(350.0);
334
335 pub const NORMAL: Self = Self(400.0);
337
338 pub const MEDIUM: Self = Self(500.0);
340
341 pub const SEMI_BOLD: Self = Self(600.0);
343
344 pub const BOLD: Self = Self(700.0);
346
347 pub const EXTRA_BOLD: Self = Self(800.0);
349
350 pub const BLACK: Self = Self(900.0);
352
353 pub const EXTRA_BLACK: Self = Self(950.0);
355
356 pub const fn new(weight: f32) -> Self {
358 Self(weight)
359 }
360
361 fn from_name(name: &str) -> Self {
367 const NAMES: &[(&str, Weight)] = &[
368 ("thin", Weight::THIN),
369 ("hairline", Weight::THIN),
370 ("extralight", Weight::EXTRA_LIGHT),
371 ("ultralight", Weight::EXTRA_LIGHT),
372 ("light", Weight::LIGHT),
373 ("semilight", Weight::SEMI_LIGHT),
374 ("demilight", Weight::SEMI_LIGHT),
375 ("book", Weight::NORMAL),
376 ("regular", Weight::NORMAL),
377 ("normal", Weight::NORMAL),
378 ("roman", Weight::NORMAL),
379 ("medium", Weight::MEDIUM),
380 ("semibold", Weight::SEMI_BOLD),
381 ("demibold", Weight::SEMI_BOLD),
382 ("demi", Weight::SEMI_BOLD),
383 ("bold", Weight::BOLD),
384 ("extrabold", Weight::EXTRA_BOLD),
385 ("ultrabold", Weight::EXTRA_BOLD),
386 ("black", Weight::BLACK),
387 ("heavy", Weight::BLACK),
388 ("extrablack", Weight::EXTRA_BLACK),
389 ("ultrablack", Weight::EXTRA_BLACK),
390 ];
391 let letters = name
392 .bytes()
393 .filter(|byte| !matches!(byte, b' ' | b'-' | b'_'))
394 .map(|byte| byte.to_ascii_lowercase());
395 NAMES
396 .iter()
397 .find(|(known, _)| letters.clone().eq(known.bytes()))
398 .map_or(Self::NORMAL, |(_, weight)| *weight)
399 }
400
401 pub const fn value(self) -> f32 {
403 self.0
404 }
405}
406
407impl Default for Weight {
408 fn default() -> Self {
409 Self::NORMAL
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use font_test_data::VAZIRMATN_VAR;
417
418 #[test]
419 fn missing_os2() {
420 let font = Font::new(font_test_data::CMAP12_FONT1, 0).unwrap();
421 let attributes = font.default_attributes();
422 assert_eq!(attributes.width, Width::NORMAL);
423 assert_eq!(attributes.style, Style::Italic);
424 assert_eq!(attributes.weight, Weight::BOLD);
425 }
426
427 #[test]
428 fn so_stylish() {
429 let font = Font::new(font_test_data::CMAP14_FONT1, 0).unwrap();
430 let attributes = font.default_attributes();
431 assert_eq!(attributes.width, Width::SEMI_CONDENSED);
432 assert_eq!(attributes.style, Style::Oblique(Some(-14.0)));
433 assert_eq!(attributes.weight, Weight::EXTRA_BOLD);
434 }
435
436 #[test]
437 fn variations_override_the_axes_the_font_has() {
438 let font = Font::new(VAZIRMATN_VAR, 0).unwrap();
439 let default = font.default_attributes();
440 assert_eq!(
442 Attributes::from_variations(&font, core::iter::empty::<Variation>()),
443 default
444 );
445 let at = |variations: &[(&str, f32)]| Attributes::from_variations(&font, variations);
447 assert_eq!(at(&[("wght", 650.0)]).weight, Weight::new(650.0));
448 assert_eq!(at(&[("wght", 2000.0)]).weight, Weight::new(900.0));
449 assert_eq!(
450 at(&[("wght", 300.0), ("wght", 500.0)]).weight,
451 Weight::MEDIUM
452 );
453 let ignored = at(&[("wdth", 75.0), ("ital", 1.0), ("slnt", -10.0)]);
455 assert_eq!(ignored, default);
456 let tinos = Font::new(font_test_data::TINOS_SUBSET, 0).unwrap();
458 assert_eq!(
459 Attributes::from_variations(&tinos, [("wght", 700.0)]),
460 tinos.default_attributes()
461 );
462 }
463
464 #[test]
465 fn named_instances_give_their_own_attributes() {
466 let font = Font::new(VAZIRMATN_VAR, 0).unwrap();
467 assert_eq!(
469 Attributes::from_named_instance(&font, 6).weight,
470 Weight::BOLD
471 );
472 assert_eq!(
473 Attributes::from_named_instance_with_overrides(&font, 6, [("wght", 800.0)]).weight,
474 Weight::EXTRA_BOLD
475 );
476 let missing = font.named_instances().len();
478 assert_eq!(
479 Attributes::from_named_instance(&font, missing),
480 font.default_attributes()
481 );
482 assert_eq!(
483 Attributes::from_named_instance_with_overrides(&font, missing, [("wght", 300.0)])
484 .weight,
485 Weight::LIGHT
486 );
487 }
488
489 #[test]
492 fn a_type1_font_states_its_weight_name_and_angle() {
493 let regular = Font::new(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA, 0).unwrap();
494 assert_eq!(regular.default_attributes(), Attributes::default());
495 let header = core::str::from_utf8(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA)
496 .unwrap()
497 .replace("/Weight (Book)", "/Weight (Bold)")
498 .replace("/ItalicAngle 0 def", "/ItalicAngle -12 def");
499 let bold_italic = Font::new(header.into_bytes(), 0).unwrap();
500 let attributes = bold_italic.default_attributes();
501 assert_eq!(attributes.weight, Weight::BOLD);
502 assert_eq!(attributes.style, Style::Italic);
503 assert_eq!(attributes.width, Width::NORMAL);
504 let sfnt = crate::FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
506 let cff = sfnt.cff().unwrap().offset_data().as_bytes().to_vec();
507 let cff = Font::new(cff, 0).unwrap();
508 assert!(matches!(cff.kind(), Kind::Cff(..)));
509 assert_eq!(cff.default_attributes(), Attributes::default());
510 }
511
512 #[test]
513 fn weight_names() {
514 for (name, weight) in [
515 ("Bold", Weight::BOLD),
516 ("bold", Weight::BOLD),
517 ("Black", Weight::BLACK),
518 ("Semi Bold", Weight::SEMI_BOLD),
519 ("Demi-Bold", Weight::SEMI_BOLD),
520 ("ExtraLight", Weight::EXTRA_LIGHT),
521 ("Book", Weight::NORMAL),
522 ("Roman", Weight::NORMAL),
523 ("Medium", Weight::MEDIUM),
524 ("Heavy", Weight::BLACK),
525 ("Unheard Of", Weight::NORMAL),
526 ("", Weight::NORMAL),
527 ] {
528 assert_eq!(Weight::from_name(name), weight, "{name:?}");
529 }
530 }
531
532 #[test]
533 fn italic_and_slant_axes_choose_the_style() {
534 let oblique = Style::Oblique(Some(-12.0));
535 assert_eq!(style_at(oblique, None, None), oblique);
537 assert_eq!(
539 style_at(Style::Normal, Some(1.0), Some(-10.0)),
540 Style::Italic
541 );
542 assert_eq!(style_at(Style::Normal, Some(0.5), None), Style::Italic);
543 assert_eq!(style_at(Style::Italic, Some(0.0), None), Style::Normal);
544 assert_eq!(
546 style_at(Style::Normal, None, Some(-10.0)),
547 Style::Oblique(Some(-10.0))
548 );
549 assert_eq!(style_at(oblique, None, Some(0.0)), Style::Normal);
550 assert_eq!(style_at(Style::Italic, None, Some(-10.0)), Style::Italic);
552 }
553}