Skip to main content

typst_library/text/font/
variant.rs

1use std::fmt::{self, Debug, Display, Formatter};
2
3use ecow::EcoString;
4use serde::{Deserialize, Serialize};
5
6use crate::foundations::{Cast, IntoValue, Repr, cast};
7use crate::layout::Ratio;
8use crate::text::AxisValue;
9
10/// Properties that distinguish a font from other fonts in the same family.
11#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
12#[derive(Serialize, Deserialize)]
13pub struct FontVariant {
14    /// The style of the font (normal / italic / oblique).
15    pub style: FontStyle,
16    /// How heavy the font is (100 - 900).
17    pub weight: FontWeight,
18    /// How condensed or expanded the font is (0.5 - 2.0).
19    pub stretch: FontStretch,
20}
21
22impl FontVariant {
23    /// Create a variant from its three components.
24    pub fn new(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self {
25        Self { style, weight, stretch }
26    }
27}
28
29impl Debug for FontVariant {
30    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
31        write!(f, "{:?}-{:?}-{:?}", self.style, self.weight, self.stretch)
32    }
33}
34
35/// The style of a font.
36#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
37#[derive(Cast, Serialize, Deserialize)]
38#[serde(rename_all = "kebab-case")]
39pub enum FontStyle {
40    /// The default, typically upright style.
41    #[default]
42    Normal,
43    /// A cursive style with custom letterform.
44    Italic,
45    /// Just a slanted version of the normal style.
46    Oblique,
47}
48
49impl FontStyle {
50    /// The conceptual distance between the styles, expressed as a number.
51    pub fn distance(self, other: Self) -> u16 {
52        if self == other {
53            // Both are the same.
54            0
55        } else if self != Self::Normal && other != Self::Normal {
56            // One is italic and one is oblique. Better than if one were normal.
57            1
58        } else {
59            // One is normal and one is italic or oblique.
60            2
61        }
62    }
63}
64
65impl From<usvg::FontStyle> for FontStyle {
66    fn from(style: usvg::FontStyle) -> Self {
67        match style {
68            usvg::FontStyle::Normal => Self::Normal,
69            usvg::FontStyle::Italic => Self::Italic,
70            usvg::FontStyle::Oblique => Self::Oblique,
71        }
72    }
73}
74
75/// The weight of a font.
76#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
77#[derive(Serialize, Deserialize)]
78#[serde(transparent)]
79pub struct FontWeight(pub(super) u16);
80
81/// Font weight names and numbers.
82/// See `<https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight#common_weight_name_mapping>`
83impl FontWeight {
84    /// Thin weight (100).
85    pub const THIN: Self = Self(100);
86
87    /// Extra light weight (200).
88    pub const EXTRALIGHT: Self = Self(200);
89
90    /// Light weight (300).
91    pub const LIGHT: Self = Self(300);
92
93    /// Regular weight (400).
94    pub const REGULAR: Self = Self(400);
95
96    /// Medium weight (500).
97    pub const MEDIUM: Self = Self(500);
98
99    /// Semibold weight (600).
100    pub const SEMIBOLD: Self = Self(600);
101
102    /// Bold weight (700).
103    pub const BOLD: Self = Self(700);
104
105    /// Extrabold weight (800).
106    pub const EXTRABOLD: Self = Self(800);
107
108    /// Black weight (900).
109    pub const BLACK: Self = Self(900);
110
111    /// Create a font weight from a number between 100 and 900, clamping it if
112    /// necessary.
113    pub fn from_number(weight: u16) -> Self {
114        Self(weight.clamp(100, 900))
115    }
116
117    /// Maps an OpenType `wght` font variation value to a weight.
118    pub fn from_wght(value: AxisValue) -> Self {
119        Self::from_number(value.0 as u16)
120    }
121
122    /// The number between 100 and 900.
123    pub fn to_number(self) -> u16 {
124        self.0
125    }
126
127    /// Maps a weight to an OpenType `wght` font variation value.
128    pub fn to_wght(self) -> AxisValue {
129        AxisValue(self.to_number() as f32)
130    }
131
132    /// Add (or remove) weight, saturating at the boundaries of 100 and 900.
133    pub fn thicken(self, delta: i16) -> Self {
134        Self((self.0 as i16).saturating_add(delta).clamp(100, 900) as u16)
135    }
136
137    /// The absolute number distance between this and another font weight.
138    pub fn distance(self, other: Self) -> u16 {
139        (self.0 as i16 - other.0 as i16).unsigned_abs()
140    }
141}
142
143impl Default for FontWeight {
144    fn default() -> Self {
145        Self::REGULAR
146    }
147}
148
149impl Display for FontWeight {
150    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
151        write!(f, "{}", self.0)
152    }
153}
154
155impl From<fontdb::Weight> for FontWeight {
156    fn from(weight: fontdb::Weight) -> Self {
157        Self::from_number(weight.0)
158    }
159}
160
161cast! {
162    FontWeight,
163    self => IntoValue::into_value(match self {
164        FontWeight::THIN => "thin",
165        FontWeight::EXTRALIGHT => "extralight",
166        FontWeight::LIGHT => "light",
167        FontWeight::REGULAR => "regular",
168        FontWeight::MEDIUM => "medium",
169        FontWeight::SEMIBOLD => "semibold",
170        FontWeight::BOLD => "bold",
171        FontWeight::EXTRABOLD => "extrabold",
172        FontWeight::BLACK => "black",
173        _ => return self.to_number().into_value(),
174    }),
175    v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
176    /// Thin weight (100).
177    "thin" => Self::THIN,
178    /// Extra light weight (200).
179    "extralight" => Self::EXTRALIGHT,
180    /// Light weight (300).
181    "light" => Self::LIGHT,
182    /// Regular weight (400).
183    "regular" => Self::REGULAR,
184    /// Medium weight (500).
185    "medium" => Self::MEDIUM,
186    /// Semibold weight (600).
187    "semibold" => Self::SEMIBOLD,
188    /// Bold weight (700).
189    "bold" => Self::BOLD,
190    /// Extrabold weight (800).
191    "extrabold" => Self::EXTRABOLD,
192    /// Black weight (900).
193    "black" => Self::BLACK,
194}
195
196/// The width of a font.
197#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
198#[derive(Serialize, Deserialize)]
199#[serde(transparent)]
200pub struct FontStretch(pub(super) u16);
201
202impl FontStretch {
203    /// Ultra-condensed stretch (50%).
204    pub const ULTRA_CONDENSED: Self = Self(500);
205
206    /// Extra-condensed stretch weight (62.5%).
207    pub const EXTRA_CONDENSED: Self = Self(625);
208
209    /// Condensed stretch (75%).
210    pub const CONDENSED: Self = Self(750);
211
212    /// Semi-condensed stretch (87.5%).
213    pub const SEMI_CONDENSED: Self = Self(875);
214
215    /// Normal stretch (100%).
216    pub const NORMAL: Self = Self(1000);
217
218    /// Semi-expanded stretch (112.5%).
219    pub const SEMI_EXPANDED: Self = Self(1125);
220
221    /// Expanded stretch (125%).
222    pub const EXPANDED: Self = Self(1250);
223
224    /// Extra-expanded stretch (150%).
225    pub const EXTRA_EXPANDED: Self = Self(1500);
226
227    /// Ultra-expanded stretch (200%).
228    pub const ULTRA_EXPANDED: Self = Self(2000);
229
230    /// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if
231    /// necessary.
232    pub fn from_ratio(ratio: Ratio) -> Self {
233        Self((ratio.get().clamp(0.5, 2.0) * 1000.0) as u16)
234    }
235
236    /// Create a font stretch from an OpenType-style number between 1 and 9,
237    /// clamping it if necessary.
238    pub fn from_number(stretch: u16) -> Self {
239        match stretch {
240            0 | 1 => Self::ULTRA_CONDENSED,
241            2 => Self::EXTRA_CONDENSED,
242            3 => Self::CONDENSED,
243            4 => Self::SEMI_CONDENSED,
244            5 => Self::NORMAL,
245            6 => Self::SEMI_EXPANDED,
246            7 => Self::EXPANDED,
247            8 => Self::EXTRA_EXPANDED,
248            _ => Self::ULTRA_EXPANDED,
249        }
250    }
251
252    /// Maps an OpenType `wdth` font variation value to a stretch.
253    pub fn from_wdth(value: AxisValue) -> Self {
254        Self::from_ratio(Ratio::new(value.0 as f64 / 100.0))
255    }
256
257    /// The ratio between 0.5 and 2.0 corresponding to this stretch.
258    pub fn to_ratio(self) -> Ratio {
259        Ratio::new(self.0 as f64 / 1000.0)
260    }
261
262    /// Maps an OpenType `wdth` font variation value to a stretch.
263    pub fn to_wdth(self) -> AxisValue {
264        AxisValue(self.to_ratio().get() as f32 * 100.0)
265    }
266
267    /// Round to one of the pre-defined variants.
268    pub fn round(self) -> Self {
269        match self.0 {
270            ..=562 => Self::ULTRA_CONDENSED,
271            563..=687 => Self::EXTRA_CONDENSED,
272            688..=812 => Self::CONDENSED,
273            813..=937 => Self::SEMI_CONDENSED,
274            938..=1062 => Self::NORMAL,
275            1063..=1187 => Self::SEMI_EXPANDED,
276            1188..=1374 => Self::EXPANDED,
277            1375..=1749 => Self::EXTRA_EXPANDED,
278            1750.. => Self::ULTRA_EXPANDED,
279        }
280    }
281
282    /// The absolute ratio distance between this and another font stretch.
283    pub fn distance(self, other: Self) -> Ratio {
284        (self.to_ratio() - other.to_ratio()).abs()
285    }
286}
287
288impl Default for FontStretch {
289    fn default() -> Self {
290        Self::NORMAL
291    }
292}
293
294impl Repr for FontStretch {
295    fn repr(&self) -> EcoString {
296        self.to_ratio().repr()
297    }
298}
299
300impl Display for FontStretch {
301    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
302        let int_part = self.0 / 10;
303        let dec_part = self.0 % 10;
304        if dec_part == 0 {
305            write!(f, "{int_part}%")
306        } else {
307            write!(f, "{int_part}.{dec_part}%")
308        }
309    }
310}
311
312impl From<usvg::FontStretch> for FontStretch {
313    fn from(stretch: usvg::FontStretch) -> Self {
314        match stretch {
315            usvg::FontStretch::UltraCondensed => Self::ULTRA_CONDENSED,
316            usvg::FontStretch::ExtraCondensed => Self::EXTRA_CONDENSED,
317            usvg::FontStretch::Condensed => Self::CONDENSED,
318            usvg::FontStretch::SemiCondensed => Self::SEMI_CONDENSED,
319            usvg::FontStretch::Normal => Self::NORMAL,
320            usvg::FontStretch::SemiExpanded => Self::SEMI_EXPANDED,
321            usvg::FontStretch::Expanded => Self::EXPANDED,
322            usvg::FontStretch::ExtraExpanded => Self::EXTRA_EXPANDED,
323            usvg::FontStretch::UltraExpanded => Self::ULTRA_EXPANDED,
324        }
325    }
326}
327
328cast! {
329    FontStretch,
330    self => self.to_ratio().into_value(),
331    v: Ratio => Self::from_ratio(v),
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_font_weight_distance() {
340        let d = |a, b| FontWeight(a).distance(FontWeight(b));
341        assert_eq!(d(500, 200), 300);
342        assert_eq!(d(500, 500), 0);
343        assert_eq!(d(500, 900), 400);
344        assert_eq!(d(10, 100), 90);
345    }
346
347    #[test]
348    fn test_font_stretch_debug() {
349        assert_eq!(FontStretch::EXPANDED.repr(), "125%")
350    }
351
352    #[test]
353    fn text_font_stretch_fmt() {
354        assert_eq!(format!("{}", FontStretch(0)), "0%");
355        assert_eq!(format!("{}", FontStretch(1)), "0.1%");
356        assert_eq!(format!("{}", FontStretch(10)), "1%");
357        assert_eq!(format!("{}", FontStretch(100)), "10%");
358        assert_eq!(format!("{}", FontStretch(666)), "66.6%");
359        assert_eq!(format!("{}", FontStretch(1000)), "100%");
360        assert_eq!(format!("{}", FontStretch(1120)), "112%");
361        assert_eq!(format!("{}", FontStretch(u16::MAX)), "6553.5%");
362    }
363}