1use std::{
2 collections::BTreeMap,
3 hash::{Hash, Hasher},
4 str::FromStr,
5 sync::Arc,
6};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
10pub struct FontTag([u8; 4]);
11
12impl FontTag {
13 pub const WEIGHT: Self = Self(*b"wght");
15 pub const WIDTH: Self = Self(*b"wdth");
17 pub const SLANT: Self = Self(*b"slnt");
19 pub const OPTICAL_SIZE: Self = Self(*b"opsz");
22 pub const ITALIC: Self = Self(*b"ital");
25
26 pub fn as_bytes(&self) -> &[u8; 4] {
28 &self.0
29 }
30}
31
32impl FromStr for FontTag {
33 type Err = anyhow::Error;
34 fn from_str(value: &str) -> Result<Self, Self::Err> {
35 anyhow::ensure!(
36 value.len() == 4 && value.bytes().all(|b| (0x20..=0x7e).contains(&b)),
37 "OpenType tags must contain four printable ASCII bytes"
38 );
39 Ok(Self(value.as_bytes().try_into().unwrap()))
40 }
41}
42
43impl std::fmt::Display for FontTag {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.write_str(std::str::from_utf8(&self.0).unwrap())
46 }
47}
48
49#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, schemars::JsonSchema)]
56#[serde(into = "BTreeMap<String, f32>")]
57#[schemars(with = "BTreeMap<String, f32>")]
58pub struct FontVariations(Arc<Vec<(FontTag, f32)>>);
59
60impl Eq for FontVariations {}
61impl Hash for FontVariations {
62 fn hash<H: Hasher>(&self, state: &mut H) {
63 self.0.len().hash(state);
64 for (tag, value) in self.0.iter() {
65 tag.hash(state);
66 value.to_bits().hash(state);
67 }
68 }
69}
70
71impl FontVariations {
72 pub fn new(settings: impl IntoIterator<Item = (FontTag, f32)>) -> anyhow::Result<Self> {
75 let mut axes = BTreeMap::new();
76 for (tag, value) in settings {
77 anyhow::ensure!(value.is_finite(), "axis {tag} must be finite");
78 axes.insert(tag, if value == 0.0 { 0.0 } else { value });
79 }
80 Ok(Self(Arc::new(axes.into_iter().collect())))
81 }
82
83 pub fn settings(&self) -> &[(FontTag, f32)] {
85 &self.0
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::collections::hash_map::DefaultHasher;
93 fn hash(value: &FontVariations) -> u64 {
94 let mut h = DefaultHasher::new();
95 value.hash(&mut h);
96 h.finish()
97 }
98 #[test]
99 fn canonical_coordinates_preserve_signed_precision() {
100 let slnt = FontTag::SLANT;
101 let wght = FontTag::WEIGHT;
102 let a = FontVariations::new([(wght, 300.25), (slnt, -12.5), (wght, 400.125)]).unwrap();
103 let b = FontVariations::new([(slnt, -12.5), (wght, 400.125)]).unwrap();
104 assert_eq!(a, b);
105 assert_eq!(hash(&a), hash(&b));
106 assert_eq!(a.settings()[0], (slnt, -12.5));
107 assert_ne!(
108 a,
109 FontVariations::new([(slnt, -12.5), (wght, 400.126)]).unwrap()
110 );
111 let zero = FontVariations::new([(slnt, 0.0)]).unwrap();
112 let negative_zero = FontVariations::new([(slnt, -0.0)]).unwrap();
113 assert_eq!(zero, negative_zero);
114 assert_eq!(hash(&zero), hash(&negative_zero));
115 }
116 #[test]
117 fn serialization_preserves_signed_settings_and_rejects_invalid_duplicates() {
118 let values = FontVariations::new([("slnt".parse().unwrap(), -10.125)]).unwrap();
119 assert_eq!(
120 serde_json::to_string(&values).unwrap(),
121 r#"{"slnt":-10.125}"#
122 );
123 assert_eq!(
124 serde_json::from_str::<FontVariations>(r#"{"wght":400,"slnt":-10.125,"wght":500}"#)
125 .unwrap()
126 .settings(),
127 &[
128 ("slnt".parse().unwrap(), -10.125),
129 ("wght".parse().unwrap(), 500.0)
130 ]
131 );
132 assert!(serde_json::from_str::<FontVariations>(r#"{"wght":1e100,"wght":400}"#).is_err());
133 assert!(serde_json::from_str::<FontVariations>(r#"{"bad":400}"#).is_err());
134 }
135
136 #[test]
137 fn rejects_invalid_coordinates_and_tags() {
138 for (tag, name) in [
139 (FontTag::WEIGHT, "wght"),
140 (FontTag::WIDTH, "wdth"),
141 (FontTag::SLANT, "slnt"),
142 (FontTag::OPTICAL_SIZE, "opsz"),
143 (FontTag::ITALIC, "ital"),
144 ] {
145 assert_eq!(name.parse::<FontTag>().unwrap(), tag);
146 assert_eq!(tag.to_string(), name);
147 }
148 for tag in ["", "abc", "abcde", "éab", "a\nbc"] {
149 assert!(tag.parse::<FontTag>().is_err());
150 }
151 assert!("CFF ".parse::<FontTag>().is_ok());
152 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
153 assert!(FontVariations::new([("slnt".parse().unwrap(), value)]).is_err());
154 }
155 assert!(FontVariations::default().settings().is_empty());
156 }
157}
158
159impl TryFrom<BTreeMap<String, f32>> for FontVariations {
160 type Error = anyhow::Error;
161 fn try_from(settings: BTreeMap<String, f32>) -> Result<Self, Self::Error> {
162 Self::new(
163 settings
164 .into_iter()
165 .map(|(tag, value)| Ok((tag.parse()?, value)))
166 .collect::<anyhow::Result<Vec<_>>>()?,
167 )
168 }
169}
170
171impl From<FontVariations> for BTreeMap<String, f32> {
172 fn from(settings: FontVariations) -> Self {
173 settings
174 .settings()
175 .iter()
176 .map(|(tag, value)| (tag.to_string(), *value))
177 .collect()
178 }
179}
180
181impl<'de> serde::Deserialize<'de> for FontVariations {
182 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
183 struct Visitor;
184 impl<'de> serde::de::Visitor<'de> for Visitor {
185 type Value = FontVariations;
186 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
187 formatter.write_str("a map of four-byte axis tags to finite coordinates")
188 }
189 fn visit_map<M: serde::de::MapAccess<'de>>(
190 self,
191 mut map: M,
192 ) -> Result<Self::Value, M::Error> {
193 let mut settings = Vec::new();
194 while let Some((tag, value)) = map.next_entry::<String, f32>()? {
195 let tag = tag.parse().map_err(serde::de::Error::custom)?;
196 if !value.is_finite() {
197 return Err(serde::de::Error::custom("axis coordinate must be finite"));
198 }
199 settings.push((tag, value));
200 }
201 FontVariations::new(settings).map_err(serde::de::Error::custom)
202 }
203 }
204 deserializer.deserialize_map(Visitor)
205 }
206}