Skip to main content

typst_library/text/font/
variations.rs

1use std::fmt::{self, Display, Formatter};
2use std::hash::{Hash, Hasher};
3
4use ecow::{EcoString, eco_format};
5use serde::{Deserialize, Serialize};
6use smallvec::SmallVec;
7use typst_utils::Rdedup;
8
9use crate::diag::{Hint, HintedStrResult};
10use crate::foundations::{Dict, Fold, IntoValue, Repr, cast};
11use crate::layout::Abs;
12use crate::text::{FontStyle, FontVariant, Tag};
13
14/// A variation axis in a font.
15#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)]
16pub struct FontAxis {
17    /// The OpenType tag that identifies the axis. May be a standard tag like
18    /// `wght` or a custom tag.
19    pub tag: Tag,
20    /// The minimum value the font supports for this axis.
21    pub min: AxisValue,
22    /// The maximum value the font supports for this axis.
23    pub max: AxisValue,
24    /// The default value the font has set for this axis.
25    pub default: AxisValue,
26}
27
28impl FontAxis {
29    /// Determines the distance from the `target` value to the closest value
30    /// that lies within the axis' range.
31    pub(super) fn distance<T, N>(
32        &self,
33        target: T,
34        parse: impl Fn(AxisValue) -> T,
35        distance: impl Fn(T, T) -> N,
36    ) -> N
37    where
38        T: Ord,
39        N: Default,
40    {
41        let min = parse(self.min);
42        let max = parse(self.max);
43        if target < min {
44            distance(min, target)
45        } else if target < max {
46            N::default()
47        } else {
48            distance(target, max)
49        }
50    }
51}
52
53/// A value for an OpenType font variation.
54#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
55#[serde(transparent)]
56pub struct AxisValue(pub f32);
57
58impl AxisValue {
59    /// Clamps this value into the allowed range for the given `axis`.
60    pub fn clamp(self, axis: &FontAxis) -> Self {
61        AxisValue(self.0.clamp(axis.min.0, axis.max.0))
62    }
63}
64
65impl Display for AxisValue {
66    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
67        write!(f, "{}", typst_utils::round_with_precision(self.0.into(), 2))
68    }
69}
70
71impl Hash for AxisValue {
72    fn hash<H: Hasher>(&self, state: &mut H) {
73        self.0.to_bits().hash(state);
74    }
75}
76
77cast! {
78    AxisValue,
79    self => (self.0 as f64).into_value(),
80    v: f64 => Self(v as f32),
81}
82
83/// Well-known variation axes that are used for font selection and/or affected
84/// by text properties during instantiation.
85#[derive(Default, Copy, Clone)]
86pub struct StandardAxes<'a> {
87    pub ital: Option<&'a FontAxis>,
88    pub slnt: Option<&'a FontAxis>,
89    pub wght: Option<&'a FontAxis>,
90    pub wdth: Option<&'a FontAxis>,
91    pub opsz: Option<&'a FontAxis>,
92}
93
94impl<'a> StandardAxes<'a> {
95    pub const ITAL: Tag = Tag::from_bytes(b"ital");
96    pub const SLNT: Tag = Tag::from_bytes(b"slnt");
97    pub const WGHT: Tag = Tag::from_bytes(b"wght");
98    pub const WDTH: Tag = Tag::from_bytes(b"wdth");
99    pub const OPSZ: Tag = Tag::from_bytes(b"opsz");
100
101    pub const LIST: [Tag; 5] =
102        [Self::ITAL, Self::SLNT, Self::WGHT, Self::WDTH, Self::OPSZ];
103
104    /// Extracts the standard axes from the given axes.
105    pub fn parse(axes: &'a [FontAxis]) -> Self {
106        let mut this = StandardAxes::default();
107        for axis in axes {
108            match axis.tag {
109                Self::ITAL => this.ital = Some(axis),
110                Self::SLNT => this.slnt = Some(axis),
111                Self::WGHT => this.wght = Some(axis),
112                Self::WDTH => this.wdth = Some(axis),
113                Self::OPSZ => this.opsz = Some(axis),
114                _ => {}
115            }
116        }
117        this
118    }
119
120    /// Whether the given tag is one of the standard ones.
121    pub fn knows(tag: Tag) -> bool {
122        Self::LIST.contains(&tag)
123    }
124
125    /// Returns a metric with which axes can be sorted for user-facing display.
126    pub fn order(tag: Tag) -> impl Ord {
127        Self::LIST.iter().position(|&t| t == tag).unwrap_or(Self::LIST.len())
128    }
129}
130
131/// Variable font axis values.
132///
133/// This stores axis tag to value mappings for variable fonts. Unlike font
134/// features which are integers, axis values are floating-point numbers.
135#[derive(Debug, Default, Clone, PartialEq, Hash)]
136pub struct FontVariations(pub SmallVec<[(Tag, AxisValue); 2]>);
137
138impl FontVariations {
139    /// Resolves which variations to set given the `axes` supported by a font
140    /// and a desired font `variant` and point `size`.
141    pub fn resolve(axes: &[FontAxis], variant: FontVariant, size: Abs) -> Self {
142        let mut variations = FontVariations::default();
143        let mut set = |axis: &FontAxis, value: AxisValue| {
144            variations.0.push((axis.tag, value.clamp(axis)));
145        };
146
147        let axes = StandardAxes::parse(axes);
148
149        match (variant.style, axes.ital, axes.slnt) {
150            // Don't need to do anything or can't do anything.
151            (FontStyle::Normal, ..) | (_, None, None) => {}
152
153            // Serve italic due to request or as fallback for oblique.
154            (FontStyle::Italic, Some(axis), _)
155            | (FontStyle::Oblique, Some(axis), None) => {
156                // Set to 1.0 for italic, but avoid exceeding the axis' range.
157                set(axis, AxisValue(axis.max.0.min(1.0)));
158            }
159
160            // Serve oblique due to request or as fallback for italic.
161            (FontStyle::Oblique, _, Some(axis))
162            | (FontStyle::Italic, None, Some(axis)) => {
163                // Slant values are clockwise and a typical italic is
164                // counter-clockwise, so negative values are desirable. If the
165                // axis doesn't support negative slant, however, we prefer,
166                // positive slant over no slant at all.
167                if axis.min.0 < 0.0 {
168                    set(axis, axis.min);
169                } else if axis.max.0 > 0.0 {
170                    set(axis, axis.max);
171                }
172            }
173        }
174
175        if let Some(axis) = axes.wdth {
176            set(axis, variant.stretch.to_wdth());
177        }
178
179        if let Some(axis) = axes.wght {
180            set(axis, variant.weight.to_wght());
181        }
182
183        if let Some(axis) = axes.opsz {
184            set(axis, AxisValue(size.to_pt() as f32));
185        }
186
187        variations
188    }
189
190    /// Adds additional font variations to the end.
191    pub fn chain(mut self, other: &FontVariations) -> Self {
192        self.0.extend_from_slice(&other.0);
193        self
194    }
195
196    /// Sorts and deduplicates variations so that we have one canonical
197    /// font instance for each combination.
198    pub fn normalized(mut self) -> Self {
199        // Sort should be stable so that later values consistently win. The
200        // stable std sort only allocates for larger arrays, so this is fine.
201        self.0.sort_by_key(|&(tag, _)| tag);
202
203        // We want later values to win, so we can't use the built-in
204        // `dedup_by_key` (which would let earlier ones win).
205        self.0.rdedup_by_key(|&mut (tag, _)| tag);
206
207        self
208    }
209}
210
211impl Fold for FontVariations {
212    fn fold(self, outer: Self) -> Self {
213        Self(self.0.fold(outer.0))
214    }
215}
216
217cast! {
218    FontVariations,
219    self => self.0
220        .into_iter()
221        .map(|(tag, num)|(tag.to_str_lossy().into(), num.into_value()))
222        .collect::<Dict>()
223        .into_value(),
224    values: Dict => Self(values
225        .into_iter()
226        .enumerate()
227        .map(|(i, (k, v))| Ok((
228            k.clone().into_value().cast::<Tag>().hint(tag_hint_helper(i, &k))?,
229            v.cast::<AxisValue>().hint(tag_hint_helper(i, &k))?
230        )))
231        .collect::<HintedStrResult<_>>()?),
232}
233
234fn tag_hint_helper(index: usize, key: &impl Repr) -> EcoString {
235    eco_format!("occurred in tag at index {index} (`{}`)", key.repr())
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn text_axis_value_fmt() {
244        // Assure we're printing with appropriate precision.
245        assert_eq!(format!("{}", AxisValue(100.)), "100");
246        assert_eq!(format!("{}", AxisValue(-2.5)), "-2.5");
247        assert_eq!(format!("{}", AxisValue(4.2)), "4.2");
248        assert_eq!(format!("{}", AxisValue(i16::MAX as f32 + 0.75)), "32767.75");
249
250        // These should be impossible to represent, but still work in fmt.
251        assert_eq!(format!("{}", AxisValue(f32::NAN)), "NaN");
252        assert_eq!(format!("{}", AxisValue(f32::INFINITY)), "inf");
253    }
254}