Skip to main content

read_fonts/model/
attribute.rs

1//! The width, style and weight of a font.
2//!
3//! [`Font::default_attributes`] returns the attributes of the default
4//! instance. [`Attributes::from_variations`] and the named instance
5//! constructors return the attributes at other locations. They take the
6//! same user space values as [`InstanceBuilder`](super::InstanceBuilder).
7
8use 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/// The width, style and weight of a font at one location.
20#[derive(Copy, Clone, PartialEq, Debug, Default)]
21pub struct Attributes {
22    /// How wide the design is relative to its normal width.
23    pub width: Width,
24    /// Whether the design is upright, italic or oblique.
25    pub style: Style,
26    /// How heavy the design is.
27    pub weight: Weight,
28}
29
30impl Attributes {
31    /// Returns the attributes at the location given by `variations`, in user
32    /// space.
33    ///
34    /// Each attribute starts from the font's default attributes. Values for
35    /// the `wdth`, `ital`, `slnt` and `wght` axes override them. Variations
36    /// are applied the same way as when building an instance. A variation
37    /// for an axis the font does not have is ignored, the last value for an
38    /// axis is used, and each value is clamped to the range of its axis.
39    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            // As in `Axes::filter`, a value applies to every axis with the
60            // tag, each clamped to its own range; the last such axis wins.
61            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    /// Returns the attributes of the named instance at `index`.
76    ///
77    /// Returns the font's default attributes if there is no such instance.
78    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    /// Returns the attributes of the named instance at `index`, with
83    /// `overrides` applied after the instance's own values.
84    ///
85    /// If there is no such instance, only the overrides apply.
86    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    /// Returns the attributes of the default instance of `font`.
97    ///
98    /// An SFNT font defines them in its tables. A Type 1 or standalone CFF
99    /// font has only a weight name and an italic angle, which are read the
100    /// same way as FreeType reads them. These fonts define no width.
101    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    /// Returns the attributes defined by a PostScript font's `Weight` name
114    /// and `ItalicAngle`.
115    ///
116    /// A nonzero italic angle means italic, matching how FreeType sets its
117    /// italic flag.
118    /// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/type1/t1objs.c#L462>
119    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    /// Reads the attributes of the default instance from the font's tables.
132    ///
133    /// These come from OS/2, with the `post` italic angle for an oblique
134    /// style. If there is no OS/2 table, they come from `head`.
135    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        // Bits 0 and 9 of fsSelection mark italic and oblique.
148        // <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#fsselection>
149        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        // usWeightClass is specified from 1 to 1000, but a variable font can
158        // state a value outside it, so it is not clamped.
159        // <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass>
160        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
193/// Returns the style at a location with the given `italic` and `slant`
194/// values, starting from the font's `default` style.
195///
196/// An `ital` value of 0.5 or more is italic, and a smaller value is not
197/// italic. A nonzero `slnt` value is oblique by that angle unless the style
198/// is italic. A `slnt` value of zero is not oblique.
199fn 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/// How wide a design is relative to its normal width, typically from 0.5 to
216/// 2.0.
217///
218/// In variable fonts, this can be controlled with the `wdth` axis, which
219/// expresses the width as a percentage.
220///
221/// See <https://fonts.google.com/knowledge/glossary/width>
222#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
223pub struct Width(f32);
224
225impl Width {
226    /// Width that is 50% of normal.
227    pub const ULTRA_CONDENSED: Self = Self(0.5);
228
229    /// Width that is 62.5% of normal.
230    pub const EXTRA_CONDENSED: Self = Self(0.625);
231
232    /// Width that is 75% of normal.
233    pub const CONDENSED: Self = Self(0.75);
234
235    /// Width that is 87.5% of normal.
236    pub const SEMI_CONDENSED: Self = Self(0.875);
237
238    /// Width that is 100% of normal.
239    pub const NORMAL: Self = Self(1.0);
240
241    /// Width that is 112.5% of normal.
242    pub const SEMI_EXPANDED: Self = Self(1.125);
243
244    /// Width that is 125% of normal.
245    pub const EXPANDED: Self = Self(1.25);
246
247    /// Width that is 150% of normal.
248    pub const EXTRA_EXPANDED: Self = Self(1.5);
249
250    /// Width that is 200% of normal.
251    pub const ULTRA_EXPANDED: Self = Self(2.0);
252
253    /// Creates a width from its ratio to the normal width.
254    pub const fn new(ratio: f32) -> Self {
255        Self(ratio)
256    }
257
258    /// Creates a width from the
259    /// [usWidthClass](https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass)
260    /// field of the OS/2 table.
261    fn from_width_class(width_class: u16) -> Self {
262        // The specified range is 1 to 9, and Skia clamps values outside it.
263        // <https://skia.googlesource.com/skia/+/21b7538fe0757d8cda31598bc9e5a6d0b4b54629/include/core/SkFontStyle.h#52>
264        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    /// Returns the width as a ratio, where 1.0 is the normal width.
278    pub const fn ratio(self) -> f32 {
279        self.0
280    }
281
282    /// Returns the width as a percentage, which is the value used by the
283    /// `wdth` axis.
284    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/// Whether a design is upright, italic or oblique.
296///
297/// In variable fonts, this can be controlled with the `ital` and `slnt`
298/// axes for italic and oblique styles.
299///
300/// See <https://fonts.google.com/knowledge/glossary/style>
301#[derive(Copy, Clone, PartialEq, Default, Debug)]
302pub enum Style {
303    /// An upright or "roman" style.
304    #[default]
305    Normal,
306    /// Generally a slanted style, originally based on semi-cursive forms.
307    /// This often has a different structure from the normal style.
308    Italic,
309    /// Oblique (or slanted) style with an optional angle in degrees,
310    /// counter-clockwise from the vertical.
311    Oblique(Option<f32>),
312}
313
314/// How heavy a design is, typically from 1.0 to 1000.0.
315///
316/// In variable fonts, this can be controlled with the `wght` axis.
317///
318/// See <https://fonts.google.com/knowledge/glossary/weight>
319#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
320pub struct Weight(f32);
321
322impl Weight {
323    /// Weight value of 100.
324    pub const THIN: Self = Self(100.0);
325
326    /// Weight value of 200.
327    pub const EXTRA_LIGHT: Self = Self(200.0);
328
329    /// Weight value of 300.
330    pub const LIGHT: Self = Self(300.0);
331
332    /// Weight value of 350.
333    pub const SEMI_LIGHT: Self = Self(350.0);
334
335    /// Weight value of 400.
336    pub const NORMAL: Self = Self(400.0);
337
338    /// Weight value of 500.
339    pub const MEDIUM: Self = Self(500.0);
340
341    /// Weight value of 600.
342    pub const SEMI_BOLD: Self = Self(600.0);
343
344    /// Weight value of 700.
345    pub const BOLD: Self = Self(700.0);
346
347    /// Weight value of 800.
348    pub const EXTRA_BOLD: Self = Self(800.0);
349
350    /// Weight value of 900.
351    pub const BLACK: Self = Self(900.0);
352
353    /// Weight value of 950.
354    pub const EXTRA_BLACK: Self = Self(950.0);
355
356    /// Creates a weight with the given value.
357    pub const fn new(weight: f32) -> Self {
358        Self(weight)
359    }
360
361    /// Returns the weight for a PostScript `Weight` name, such as "Bold" or
362    /// "SemiBold".
363    ///
364    /// Names are matched without regard to case, spaces, hyphens or
365    /// underscores. An unknown name returns the normal weight.
366    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    /// Returns the weight's value.
402    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        // No variations: the default attributes.
441        assert_eq!(
442            Attributes::from_variations(&font, core::iter::empty::<Variation>()),
443            default
444        );
445        // wght is an axis of the font, clamped to it, and the last value wins.
446        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        // The font has no wdth, ital or slnt axis, so those are ignored.
454        let ignored = at(&[("wdth", 75.0), ("ital", 1.0), ("slnt", -10.0)]);
455        assert_eq!(ignored, default);
456        // A static font has no axes at all.
457        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        // Instance 6 is Bold.
468        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        // No such instance: the default attributes, then the overrides.
477        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    // A Type 1 font states its weight by name and its style by angle, as
490    // FreeType reads them; it states no width.
491    #[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        // A standalone CFF font that names no weight is normal.
505        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        // Nothing given keeps the default style.
536        assert_eq!(style_at(oblique, None, None), oblique);
537        // ital from one half up is italic, and it wins over slnt.
538        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        // A nonzero slnt is oblique by that angle, and zero is upright.
545        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        // A default italic stays italic whatever the slant.
551        assert_eq!(style_at(Style::Italic, None, Some(-10.0)), Style::Italic);
552    }
553}