1use std::fmt;
2use std::hash::{Hash, Hasher};
3use std::rc::Rc;
4use std::str::FromStr;
5
6use crate::media::fonts::OpenTypeTag;
7use crate::utilities::FormatError;
8
9#[derive(Clone, Copy, Debug)]
12pub struct FontVariation {
13 pub tag: OpenTypeTag,
15 pub value: f64,
17}
18
19impl FontVariation {
20 pub const fn new(tag: OpenTypeTag, value: f64) -> Self {
22 Self { tag, value }
23 }
24}
25
26impl PartialEq for FontVariation {
28 fn eq(&self, other: &Self) -> bool {
29 self.tag == other.tag && (self.value == other.value || (self.value.is_nan() && other.value.is_nan()))
30 }
31}
32
33impl Eq for FontVariation {}
34
35impl Hash for FontVariation {
36 fn hash<H: Hasher>(&self, state: &mut H) {
37 self.tag.hash(state);
38 state.write_u64(normalized_bits(self.value));
39 }
40}
41
42fn normalized_bits(value: f64) -> u64 {
44 if value == 0.0 {
45 0
46 } else if value.is_nan() {
47 f64::NAN.to_bits()
48 } else {
49 value.to_bits()
50 }
51}
52
53impl fmt::Display for FontVariation {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{}=", self.tag)?;
58 write_invariant(f, self.value)
59 }
60}
61
62fn write_invariant(f: &mut fmt::Formatter<'_>, value: f64) -> fmt::Result {
65 if value.is_nan() {
66 return f.write_str("NaN");
67 }
68
69 if value.is_infinite() {
70 return f.write_str(if value > 0.0 { "Infinity" } else { "-Infinity" });
71 }
72
73 if value == 0.0 {
74 return f.write_str(if value.is_sign_negative() { "-0" } else { "0" });
75 }
76
77 let scientific = format!("{value:e}");
78 let (mantissa, exponent) = scientific.split_once('e').unwrap_or((&scientific, "0"));
79 let exponent: i32 = exponent.parse().unwrap_or(0);
80
81 if (-4..15).contains(&exponent) {
82 return write!(f, "{value}");
83 }
84
85 write!(f, "{mantissa}E{}{:02}", if exponent < 0 { '-' } else { '+' }, exponent.abs())
86}
87
88#[derive(Clone, Debug)]
96pub struct FontVariationSettings {
97 variations: Rc<[FontVariation]>,
98 hash_code: u64,
99}
100
101impl FontVariationSettings {
102 fn from_sorted(sorted_variations: Vec<FontVariation>) -> Self {
103 let hash_code = Self::compute_hash_code(&sorted_variations);
104
105 Self { variations: Rc::from(sorted_variations), hash_code }
106 }
107
108 pub fn empty() -> FontVariationSettings {
110 crate::ferro_thread_local! {
111 static EMPTY: FontVariationSettings = FontVariationSettings::from_sorted(Vec::new());
112 }
113
114 EMPTY.with(Clone::clone)
115 }
116
117 pub fn new(variations: impl IntoIterator<Item = FontVariation>) -> Self {
123 let mut builder: Vec<FontVariation> = Vec::new();
124
125 for variation in variations {
126 if variation.value.is_nan() {
129 panic!("Value for axis '{}' must not be NaN.", variation.tag);
130 }
131
132 match builder.iter_mut().find(|existing| existing.tag == variation.tag) {
134 Some(existing) => *existing = variation,
135 None => builder.push(variation),
136 }
137 }
138
139 builder.sort_by_key(|variation| variation.tag.value());
140
141 Self::from_sorted(builder)
142 }
143
144 pub fn variations(&self) -> &[FontVariation] {
146 &self.variations
147 }
148
149 pub fn is_empty(&self) -> bool {
151 self.variations.is_empty()
152 }
153
154 pub fn try_get_value(&self, tag: OpenTypeTag) -> Option<f64> {
156 self.variations.iter().find(|variation| variation.tag == tag).map(|variation| variation.value)
157 }
158
159 pub fn parse(s: &str) -> Result<FontVariationSettings, FormatError> {
162 if s.trim().is_empty() {
163 return Ok(Self::empty());
164 }
165
166 let mut variations = Vec::new();
167
168 for part in s.split(',') {
169 let pair = part.trim();
170
171 if pair.is_empty() {
172 continue;
173 }
174
175 let (tag_text, value_text) = match pair.split_once('=') {
176 Some((tag_text, value_text)) if !tag_text.is_empty() && !value_text.is_empty() => {
177 (tag_text.trim(), value_text.trim())
178 }
179 _ => {
180 return Err(FormatError::from_string(format!(
181 "Invalid font variation '{pair}': expected tag=value."
182 )));
183 }
184 };
185
186 if tag_text.is_empty() || tag_text.encode_utf16().count() > 4 {
187 return Err(FormatError::from_string(format!("Invalid font variation axis tag '{tag_text}'.")));
188 }
189
190 let value = match parse_invariant(value_text) {
192 Some(value) if !value.is_nan() => value,
193 _ => {
194 return Err(FormatError::from_string(format!(
195 "Invalid font variation value '{value_text}' for axis '{tag_text}'."
196 )));
197 }
198 };
199
200 variations.push(FontVariation::new(OpenTypeTag::parse(tag_text), value));
201 }
202
203 Ok(if variations.is_empty() { Self::empty() } else { Self::new(variations) })
204 }
205
206 fn compute_hash_code(variations: &[FontVariation]) -> u64 {
207 let mut hasher = std::collections::hash_map::DefaultHasher::new();
208
209 for variation in variations {
210 variation.hash(&mut hasher);
211 }
212
213 hasher.finish()
214 }
215}
216
217fn parse_invariant(text: &str) -> Option<f64> {
221 let bytes = text.as_bytes();
222 let mut index = 0;
223
224 if matches!(bytes.first(), Some(b'+' | b'-')) {
225 index += 1;
226 }
227
228 let integer_start = index;
229
230 while bytes.get(index).is_some_and(u8::is_ascii_digit) {
231 index += 1;
232 }
233
234 let mut digits = index - integer_start;
235
236 if bytes.get(index) == Some(&b'.') {
237 index += 1;
238
239 let fraction_start = index;
240
241 while bytes.get(index).is_some_and(u8::is_ascii_digit) {
242 index += 1;
243 }
244
245 digits += index - fraction_start;
246 }
247
248 let mut is_number = digits > 0;
249
250 if is_number && matches!(bytes.get(index), Some(b'e' | b'E')) {
251 index += 1;
252
253 if matches!(bytes.get(index), Some(b'+' | b'-')) {
254 index += 1;
255 }
256
257 let exponent_start = index;
258
259 while bytes.get(index).is_some_and(u8::is_ascii_digit) {
260 index += 1;
261 }
262
263 is_number = index > exponent_start;
264 }
265
266 if is_number && index == bytes.len() {
267 return text.parse().ok();
268 }
269
270 let (negative, symbol) = match bytes.first() {
271 Some(b'-') => (true, &text[1..]),
272 Some(b'+') => (false, &text[1..]),
273 _ => (false, text),
274 };
275
276 if symbol.eq_ignore_ascii_case("Infinity") {
277 Some(if negative { f64::NEG_INFINITY } else { f64::INFINITY })
278 } else if symbol.eq_ignore_ascii_case("NaN") {
279 Some(f64::NAN)
280 } else {
281 None
282 }
283}
284
285impl Default for FontVariationSettings {
286 fn default() -> Self {
287 Self::empty()
288 }
289}
290
291impl FromStr for FontVariationSettings {
292 type Err = FormatError;
293
294 fn from_str(s: &str) -> Result<Self, Self::Err> {
295 Self::parse(s)
296 }
297}
298
299impl fmt::Display for FontVariationSettings {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 for (index, variation) in self.variations.iter().enumerate() {
304 if index > 0 {
305 f.write_str(",")?;
306 }
307
308 fmt::Display::fmt(variation, f)?;
309 }
310
311 Ok(())
312 }
313}
314
315impl PartialEq for FontVariationSettings {
316 fn eq(&self, other: &Self) -> bool {
317 if Rc::ptr_eq(&self.variations, &other.variations) {
318 return true;
319 }
320
321 if self.hash_code != other.hash_code {
322 return false;
323 }
324
325 self.variations == other.variations
326 }
327}
328
329impl Eq for FontVariationSettings {}
330
331impl Hash for FontVariationSettings {
332 fn hash<H: Hasher>(&self, state: &mut H) {
333 state.write_u64(self.hash_code);
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use std::collections::hash_map::DefaultHasher;
340
341 use super::*;
342
343 fn wght() -> OpenTypeTag {
344 OpenTypeTag::parse("wght")
345 }
346
347 fn wdth() -> OpenTypeTag {
348 OpenTypeTag::parse("wdth")
349 }
350
351 fn opsz() -> OpenTypeTag {
352 OpenTypeTag::parse("opsz")
353 }
354
355 fn hash_of(settings: &FontVariationSettings) -> u64 {
356 let mut hasher = DefaultHasher::new();
357 settings.hash(&mut hasher);
358 hasher.finish()
359 }
360
361 #[test]
362 fn empty_is_empty_and_round_trips() {
363 assert!(FontVariationSettings::empty().is_empty());
364 assert!(FontVariationSettings::empty().variations().is_empty());
365 assert_eq!(FontVariationSettings::empty().to_string(), "");
366 assert_eq!(FontVariationSettings::parse("").unwrap(), FontVariationSettings::empty());
367 assert_eq!(FontVariationSettings::parse(" ").unwrap(), FontVariationSettings::empty());
368 }
369
370 #[test]
371 fn parse_reads_comma_separated_tag_value_pairs() {
372 let settings = FontVariationSettings::parse(" wght = 700 , wdth=85.5 ").unwrap();
373
374 assert_eq!(settings.variations().len(), 2);
375 assert_eq!(settings.try_get_value(wght()), Some(700.0));
376 assert_eq!(settings.try_get_value(wdth()), Some(85.5));
377 }
378
379 #[test]
380 fn parse_rejects_malformed_input() {
381 for input in ["wght", "wght=", "=700", "weight=700", "wght=seven", "wght=NaN", "wght=7 0", "wght=inf", "wght=1e"]
382 {
383 assert!(FontVariationSettings::parse(input).is_err(), "{input}");
384 }
385 }
386
387 #[test]
388 fn to_string_round_trips_through_parse() {
389 let settings = FontVariationSettings::parse("opsz=14.25,wght=650").unwrap();
390
391 let round_tripped = FontVariationSettings::parse(&settings.to_string()).unwrap();
392
393 assert_eq!(settings, round_tripped);
394 assert_eq!(settings.to_string(), "opsz=14.25,wght=650");
395 }
396
397 #[test]
398 fn equality_is_order_independent_with_matching_hashes() {
399 let a = FontVariationSettings::new([FontVariation::new(wght(), 700.0), FontVariation::new(opsz(), 36.0)]);
400 let b = FontVariationSettings::new([FontVariation::new(opsz(), 36.0), FontVariation::new(wght(), 700.0)]);
401
402 assert_eq!(a, b);
403 assert_eq!(hash_of(&a), hash_of(&b));
404 assert_ne!(a, FontVariationSettings::parse("wght=700").unwrap());
405 }
406
407 #[test]
408 fn duplicate_tags_collapse_to_the_last_value() {
409 let settings = FontVariationSettings::parse("wght=400,wght=700").unwrap();
411
412 assert_eq!(settings.variations().len(), 1);
413 assert_eq!(settings.try_get_value(wght()), Some(700.0));
414 }
415
416 #[test]
417 fn variations_are_sorted_by_tag() {
418 let settings = FontVariationSettings::parse("wght=700,opsz=14,wdth=85").unwrap();
419
420 assert_eq!(settings.variations()[0].tag, opsz());
421 assert_eq!(settings.variations()[1].tag, wdth());
422 assert_eq!(settings.variations()[2].tag, wght());
423 }
424
425 #[test]
426 #[should_panic(expected = "must not be NaN")]
427 fn constructor_rejects_nan_and_null() {
430 FontVariationSettings::new([FontVariation::new(wght(), f64::NAN)]);
431 }
432
433 #[test]
434 fn infinite_values_are_accepted_and_round_trip() {
435 let settings = FontVariationSettings::new([
437 FontVariation::new(wght(), f64::INFINITY),
438 FontVariation::new(opsz(), f64::NEG_INFINITY),
439 ]);
440
441 assert_eq!(settings.try_get_value(wght()), Some(f64::INFINITY));
442
443 let round_tripped = FontVariationSettings::parse(&settings.to_string()).unwrap();
444
445 assert_eq!(settings, round_tripped);
446 assert_eq!(round_tripped.try_get_value(opsz()), Some(f64::NEG_INFINITY));
447 }
448
449 #[test]
450 fn try_get_value_misses_report_false_and_zero() {
451 let settings = FontVariationSettings::parse("wght=700").unwrap();
452
453 assert_eq!(settings.try_get_value(opsz()), None);
454 }
455
456 #[test]
457 fn variation_to_string_is_the_pair_form() {
458 assert_eq!(FontVariation::new(wght(), 700.0).to_string(), "wght=700");
459 assert_eq!(FontVariation::new(opsz(), 14.25).to_string(), "opsz=14.25");
460 }
461
462 #[test]
463 fn values_use_the_invariant_number_format() {
464 for (value, text) in [
465 (1e15, "wght=1E+15"),
466 (1.5e21, "wght=1.5E+21"),
467 (123456789012345.0, "wght=123456789012345"),
468 (0.0001, "wght=0.0001"),
469 (0.00001, "wght=1E-05"),
470 (-0.5, "wght=-0.5"),
471 (0.0, "wght=0"),
472 ] {
473 let variation = FontVariation::new(wght(), value);
474
475 assert_eq!(variation.to_string(), text);
476 assert_eq!(FontVariationSettings::parse(text).unwrap().try_get_value(wght()), Some(value));
477 }
478
479 assert_eq!(FontVariationSettings::parse("wght=+.5e1").unwrap().try_get_value(wght()), Some(5.0));
480 assert_eq!(FontVariationSettings::parse("wght=5.").unwrap().try_get_value(wght()), Some(5.0));
481 }
482}