typst_library/text/font/
variant.rs

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