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#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)]
16pub struct FontAxis {
17 pub tag: Tag,
20 pub min: AxisValue,
22 pub max: AxisValue,
24 pub default: AxisValue,
26}
27
28impl FontAxis {
29 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#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
55#[serde(transparent)]
56pub struct AxisValue(pub f32);
57
58impl AxisValue {
59 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#[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 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 pub fn knows(tag: Tag) -> bool {
122 Self::LIST.contains(&tag)
123 }
124
125 pub fn order(tag: Tag) -> impl Ord {
127 Self::LIST.iter().position(|&t| t == tag).unwrap_or(Self::LIST.len())
128 }
129}
130
131#[derive(Debug, Default, Clone, PartialEq, Hash)]
136pub struct FontVariations(pub SmallVec<[(Tag, AxisValue); 2]>);
137
138impl FontVariations {
139 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 (FontStyle::Normal, ..) | (_, None, None) => {}
152
153 (FontStyle::Italic, Some(axis), _)
155 | (FontStyle::Oblique, Some(axis), None) => {
156 set(axis, AxisValue(axis.max.0.min(1.0)));
158 }
159
160 (FontStyle::Oblique, _, Some(axis))
162 | (FontStyle::Italic, None, Some(axis)) => {
163 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 pub fn chain(mut self, other: &FontVariations) -> Self {
192 self.0.extend_from_slice(&other.0);
193 self
194 }
195
196 pub fn normalized(mut self) -> Self {
199 self.0.sort_by_key(|&(tag, _)| tag);
202
203 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 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 assert_eq!(format!("{}", AxisValue(f32::NAN)), "NaN");
252 assert_eq!(format!("{}", AxisValue(f32::INFINITY)), "inf");
253 }
254}