1use std::{
2 hash::{BuildHasher, Hash, Hasher},
3 sync::{Arc, OnceLock},
4};
5
6use foldhash::fast::FixedState;
7use read_fonts::{
8 FontRef, TableProvider,
9 model::{Blob, Font, Source, Variation, attribute::Style, metrics::GlyphExtents, name::NameId},
10 types::{Fixed, GlyphId as FontGlyphId, Tag},
11};
12use smallvec::SmallVec;
13
14use crate::{
15 coverage::Coverage,
16 font::{FontFamily, FontStyle, FontSynthesis, FontWeight},
17 glyph_id::GlyphId,
18 glyph_ink::GlyphInks,
19 simple_glyf::{self, GlyfTables},
20};
21
22pub enum FontBytes {
24 Static(&'static [u8]),
27 Owned(Vec<u8>),
29}
30
31impl FontBytes {
32 pub fn as_slice(&self) -> &[u8] {
34 match self {
35 Self::Static(bytes) => bytes,
36 Self::Owned(bytes) => bytes,
37 }
38 }
39}
40
41impl From<&'static [u8]> for FontBytes {
42 fn from(bytes: &'static [u8]) -> Self {
43 Self::Static(bytes)
44 }
45}
46
47impl<const N: usize> From<&'static [u8; N]> for FontBytes {
48 fn from(bytes: &'static [u8; N]) -> Self {
49 Self::Static(bytes)
50 }
51}
52
53impl From<Vec<u8>> for FontBytes {
54 fn from(bytes: Vec<u8>) -> Self {
55 Self::Owned(bytes)
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
67pub struct FontFamilyKey(u64);
68
69impl FontFamilyKey {
70 pub fn of(family: &FontFamily) -> Self {
72 Self(HASH_STATE.hash_one(family))
73 }
74}
75
76#[derive(Debug, PartialEq, thiserror::Error)]
78pub enum TypefaceError {
79 #[error("the bytes are not a readable font")]
81 InvalidFont,
82 #[error("variation {tag:?} is not an axis of the font, or lies outside its range")]
84 InvalidVariation {
85 tag: [u8; 4],
87 },
88}
89
90#[derive(Clone)]
95pub struct Typeface(Arc<Loaded>);
96
97impl PartialEq for Typeface {
98 fn eq(&self, other: &Self) -> bool {
99 self.same(other) || self.0.content_hash == other.0.content_hash
100 }
101}
102
103struct Loaded {
104 font: Font,
105 families: Box<[String]>,
106 registered_family: Option<FontFamilyKey>,
107 weight: FontWeight,
108 style: FontStyle,
109 weights: (f32, f32),
112 italic_axis: bool,
114 style_axes: SmallVec<[StyleAxis; 3]>,
118 is_color: bool,
119 variations: Box<[([u8; 4], f32)]>,
121 content_hash: u64,
122 found: Found,
123 tracking: Box<[(f32, f32)]>,
126 space: Option<GlyphId>,
128}
129
130#[derive(Default)]
132struct Found {
133 coverage: OnceLock<Coverage>,
134 glyf: OnceLock<Option<GlyfTables>>,
135 inks: OnceLock<GlyphInks>,
138}
139
140impl Typeface {
141 pub fn from_bytes(bytes: impl Into<FontBytes>) -> Result<Self, TypefaceError> {
144 let font = Font::new(blob(bytes.into()), 0).ok_or(TypefaceError::InvalidFont)?;
145 let content_hash = content_hash(&font, &[]);
146 let weight = (&font.tables())
147 .os2()
148 .ok()
149 .and_then(|os2| FontWeight::try_new(os2.us_weight_class()))
150 .unwrap_or_default();
151 let style = match font.default_attributes().style {
152 Style::Normal => FontStyle::Normal,
153 Style::Italic | Style::Oblique(_) => FontStyle::Italic,
154 };
155 let style_axes = style_axes(&font);
156 let axis = |tag: &[u8; 4]| style_axes.iter().find(|axis| axis.tag == *tag);
157 let default_weight = f32::from(weight.value());
158 let weights = axis(b"wght").map_or((default_weight, default_weight), |axis| {
159 (axis.min.max(1.0), axis.max.min(1000.0))
160 });
161 let italic_axis = axis(b"ital").is_some_and(|axis| axis.max >= 1.0)
162 || axis(b"slnt").is_some_and(|axis| axis.min < 0.0);
163 Ok(Self(Arc::new(Loaded {
164 families: family_names(&font),
165 registered_family: None,
166 weight,
167 style,
168 weights,
169 italic_axis,
170 style_axes,
171 is_color: is_color(&font),
172 variations: Box::default(),
173 content_hash,
174 found: Found::default(),
175 tracking: normal_track(&font),
176 space: space_glyph(&font),
177 font,
178 })))
179 }
180
181 pub fn from_registered(
189 family: &FontFamily,
190 weight: FontWeight,
191 style: FontStyle,
192 bytes: impl Into<FontBytes>,
193 variations: &[([u8; 4], f32)],
194 ) -> Result<Self, TypefaceError> {
195 let font = Font::new(blob(bytes.into()), 0).ok_or(TypefaceError::InvalidFont)?;
196 let mut applied = declared_variations(&style_axes(&font), weight, style);
197 for &(tag, value) in variations {
198 let valid = value.is_finite()
199 && font.axes().iter().any(|axis| {
200 axis.tag() == Tag::new(&tag)
201 && (axis.min_value()..=axis.max_value()).contains(&value)
202 });
203 if !valid {
204 return Err(TypefaceError::InvalidVariation { tag });
205 }
206 applied.retain(|(existing, _)| *existing != tag);
207 applied.push((tag, value));
208 }
209 applied.sort_unstable_by_key(|(tag, _)| *tag);
210 let content_hash = content_hash(&font, &applied);
211 let font = if applied.is_empty() {
212 font
213 } else {
214 font.instance_builder()
215 .variations(
216 applied
217 .iter()
218 .map(|(tag, value)| Variation::new(Tag::new(tag), *value)),
219 )
220 .build()
221 };
222 let declared_weight = f32::from(weight.value());
223 Ok(Self(Arc::new(Loaded {
224 families: family_names(&font),
225 registered_family: Some(FontFamilyKey::of(family)),
226 weight,
227 style,
228 weights: (declared_weight, declared_weight),
229 italic_axis: false,
230 style_axes: SmallVec::new(),
231 is_color: is_color(&font),
232 variations: applied.into_boxed_slice(),
233 content_hash,
234 found: Found::default(),
235 tracking: normal_track(&font),
236 space: space_glyph(&font),
237 font,
238 })))
239 }
240
241 pub fn family_names(&self) -> &[String] {
244 &self.0.families
245 }
246
247 pub fn registered_family(&self) -> Option<FontFamilyKey> {
250 self.0.registered_family
251 }
252
253 pub fn weight(&self) -> FontWeight {
255 self.0.weight
256 }
257
258 pub fn style(&self) -> FontStyle {
260 self.0.style
261 }
262
263 pub fn is_color(&self) -> bool {
266 self.0.is_color
267 }
268
269 pub fn synthesis(
274 &self,
275 weight: FontWeight,
276 style: FontStyle,
277 synthesis: FontSynthesis,
278 ) -> Synthesis {
279 const BOLD: u16 = 600;
280 Synthesis {
281 bold: matches!(synthesis, FontSynthesis::All | FontSynthesis::Weight)
282 && weight.value() >= BOLD
283 && self.0.weights.1 < f32::from(BOLD),
284 italic: matches!(synthesis, FontSynthesis::All | FontSynthesis::Style)
285 && style == FontStyle::Italic
286 && !self.draws(FontStyle::Italic),
287 }
288 }
289
290 pub(crate) fn weights(&self) -> (f32, f32) {
292 self.0.weights
293 }
294
295 pub(crate) fn draws(&self, style: FontStyle) -> bool {
297 self.0.style == style || self.0.italic_axis
298 }
299
300 pub(crate) fn coverage(&self) -> &Coverage {
302 self.0.found.coverage.get_or_init(|| {
303 Coverage::from_codepoints(
304 self.0
305 .font
306 .charmap()
307 .unicodes()
308 .filter(|(_, glyph)| *glyph != FontGlyphId::NOTDEF)
309 .map(|(codepoint, _)| codepoint),
310 )
311 })
312 }
313
314 pub fn variations(&self) -> &[([u8; 4], f32)] {
316 &self.0.variations
317 }
318
319 pub fn content_hash(&self) -> u64 {
322 self.0.content_hash
323 }
324
325 pub fn units_per_em(&self) -> u16 {
327 self.0.font.units_per_em()
328 }
329
330 #[inline(never)]
334 pub fn line_metrics(&self) -> LineMetrics {
335 let metrics = self.0.font.metrics();
336 metrics
337 .h_line()
338 .map_or_else(LineMetrics::default, |line| LineMetrics {
339 ascent: line.ascender.to_f32(),
340 descent: line.descender.to_f32(),
341 line_gap: line.line_gap.to_f32(),
342 })
343 }
344
345 pub fn decoration_metrics(&self) -> DecorationMetrics {
349 let metrics = self.0.font.style_metrics();
350 let em = f32::from(self.units_per_em());
351 let top = |decoration: Option<read_fonts::model::metrics::Decoration>,
352 default: (f32, f32)| {
353 decoration.map_or(default, |decoration| {
354 (-decoration.position.to_f32(), decoration.thickness.to_f32())
355 })
356 };
357 let (underline_top, underline_thickness) = top(metrics.underline, (em / 9.0, em / 18.0));
358 let (strike_through_top, strike_through_thickness) =
359 top(metrics.strike_through, (-em * 6.0 / 21.0, em / 18.0));
360 DecorationMetrics {
361 underline_top,
362 underline_thickness,
363 strike_through_top,
364 strike_through_thickness,
365 }
366 }
367
368 pub(crate) fn tracking(&self, size: f32) -> f32 {
372 let track = &self.0.tracking;
373 if track.is_empty() || !size.is_finite() || size <= 0.0 {
374 return 0.0;
375 }
376 self.tracked(size)
377 }
378
379 #[inline(never)]
382 fn tracked(&self, size: f32) -> f32 {
383 let track = &self.0.tracking;
384 let above = track.partition_point(|&(point_size, _)| point_size < size);
385 let em = match (
386 above.checked_sub(1).map(|below| track[below]),
387 track.get(above),
388 ) {
389 (Some((low_size, low)), Some(&(high_size, high))) => {
390 low + (high - low) * (size - low_size) / (high_size - low_size)
391 }
392 (Some((_, em)), None) | (None, Some(&(_, em))) => em,
393 (None, None) => 0.0,
394 };
395 em * size
396 }
397
398 pub fn glyph(&self, ch: char) -> Option<GlyphId> {
400 self.0
401 .font
402 .charmap()
403 .map_unicode(ch)
404 .map(|glyph| GlyphId(glyph.to_u32()))
405 }
406
407 pub(crate) fn space(&self) -> Option<GlyphId> {
409 self.0.space
410 }
411
412 pub fn covers(&self, ch: char) -> bool {
414 self.coverage().contains(ch)
415 }
416
417 pub fn advance(&self, glyph: GlyphId) -> f32 {
419 self.0
420 .font
421 .glyph_metrics()
422 .h_advance(FontGlyphId::new(glyph.0))
423 }
424
425 pub(crate) fn same(&self, other: &Self) -> bool {
427 Arc::ptr_eq(&self.0, &other.0)
428 }
429
430 pub(crate) fn font(&self) -> &Font {
432 &self.0.font
433 }
434
435 pub(crate) fn glyph_extents(&self, glyph: GlyphId) -> Option<GlyphExtents<f32>> {
438 let id = FontGlyphId::new(glyph.0);
439 match self.glyf_tables() {
440 Some(tables) => simple_glyf::extents(&self.0.font, tables, id),
441 None => self.0.font.glyph_metrics().extents(id),
442 }
443 }
444
445 pub(crate) fn glyph_inks(&self) -> &GlyphInks {
447 self.0.found.inks.get_or_init(|| GlyphInks::of(self))
448 }
449
450 pub(crate) fn glyf_tables(&self) -> Option<&GlyfTables> {
453 self.0
454 .found
455 .glyf
456 .get_or_init(|| GlyfTables::of(&self.0.font))
457 .as_ref()
458 }
459
460 pub(crate) fn axis_positions(
465 &self,
466 weight: FontWeight,
467 style: FontStyle,
468 ) -> SmallVec<[([u8; 4], f32); 3]> {
469 declared_variations(&self.0.style_axes, weight, style)
470 }
471}
472
473impl std::fmt::Debug for Typeface {
474 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475 formatter
476 .debug_struct("Typeface")
477 .field("family_names", &self.0.families)
478 .field("weight", &self.0.weight)
479 .field("style", &self.0.style)
480 .field("variations", &self.0.variations)
481 .finish_non_exhaustive()
482 }
483}
484
485#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
488pub struct Synthesis {
489 pub bold: bool,
491 pub italic: bool,
493}
494
495#[derive(Clone, Copy, Debug, Default, PartialEq)]
498pub struct DecorationMetrics {
499 pub underline_top: f32,
501 pub underline_thickness: f32,
503 pub strike_through_top: f32,
506 pub strike_through_thickness: f32,
508}
509
510#[derive(Clone, Copy, Debug, Default, PartialEq)]
512pub struct LineMetrics {
513 pub ascent: f32,
515 pub descent: f32,
517 pub line_gap: f32,
519}
520
521fn blob(bytes: FontBytes) -> Blob {
522 match bytes {
523 FontBytes::Static(bytes) => Blob::from(bytes),
524 FontBytes::Owned(bytes) => Blob::from(bytes),
525 }
526}
527
528fn normal_track(font: &Font) -> Box<[(f32, f32)]> {
532 let read = || {
533 let trak = font.tables().trak().ok()?;
534 let data = trak.horiz()?.ok()?;
535 let offset_data = trak.offset_data();
536 let normal = data
537 .track_table()
538 .iter()
539 .find(|track| track.track() == Fixed::ZERO)?;
540 let values = normal.per_size_values(offset_data, data.n_sizes())?;
541 let sizes = data.size_table(offset_data)?;
542 let em = f32::from(font.units_per_em());
543 let mut points = Vec::with_capacity(sizes.len());
544 for (size, value) in sizes.iter().zip(values) {
545 let size = size.get().to_f32();
546 if size <= 0.0 || points.last().is_some_and(|&(previous, _)| size <= previous) {
547 return None;
548 }
549 points.push((size, f32::from(value.get()) / em));
550 }
551 Some(points.into_boxed_slice())
552 };
553 read().unwrap_or_default()
554}
555
556fn is_color(font: &Font) -> bool {
559 let Source::Blob(bytes) = font.source() else {
560 return false;
561 };
562 FontRef::from_index(bytes.as_ref(), 0).is_ok_and(|font| {
563 [*b"COLR", *b"CBDT", *b"sbix", *b"SVG "]
564 .iter()
565 .any(|tag| font.table_data(Tag::new(tag)).is_some())
566 })
567}
568
569const HASH_STATE: FixedState = FixedState::with_seed(0);
572
573fn content_hash(font: &Font, variations: &[([u8; 4], f32)]) -> u64 {
575 let mut state = HASH_STATE.build_hasher();
576 if let Source::Blob(bytes) = font.source() {
577 bytes.as_ref().hash(&mut state);
578 }
579 for (tag, value) in variations {
580 tag.hash(&mut state);
581 value.to_bits().hash(&mut state);
582 }
583 state.finish()
584}
585
586fn family_names(font: &Font) -> Box<[String]> {
587 let names = font.names();
588 let mut families: Vec<String> = Vec::new();
589 for id in [NameId::TYPOGRAPHIC_FAMILY_NAME, NameId::FAMILY_NAME] {
590 let Some(name) = names.get(id) else {
591 continue;
592 };
593 for localized in name.iter() {
594 let value: String = localized.name().chars().collect();
595 if !value.is_empty()
596 && !families
597 .iter()
598 .any(|existing| existing.eq_ignore_ascii_case(&value))
599 {
600 families.push(value);
601 }
602 }
603 }
604 families.into_boxed_slice()
605}
606
607fn declared_variations(
610 axes: &[StyleAxis],
611 weight: FontWeight,
612 style: FontStyle,
613) -> SmallVec<[([u8; 4], f32); 3]> {
614 const OBLIQUE_DEGREES: f32 = -12.0;
615 axes.iter()
616 .filter_map(|axis| {
617 let requested = match &axis.tag {
618 b"wght" => f32::from(weight.value()),
619 b"ital" if style == FontStyle::Italic => 1.0,
620 b"slnt" if style == FontStyle::Italic => OBLIQUE_DEGREES,
621 _ => return None,
622 };
623 Some((axis.tag, requested.clamp(axis.min, axis.max)))
624 })
625 .collect()
626}
627
628#[derive(Clone, Copy, Debug)]
630struct StyleAxis {
631 tag: [u8; 4],
632 min: f32,
633 max: f32,
634}
635
636fn style_axes(font: &Font) -> SmallVec<[StyleAxis; 3]> {
638 font.axes()
639 .iter()
640 .map(|axis| StyleAxis {
641 tag: axis.tag().to_be_bytes(),
642 min: axis.min_value(),
643 max: axis.max_value(),
644 })
645 .filter(|axis| matches!(&axis.tag, b"wght" | b"ital" | b"slnt"))
646 .collect()
647}
648
649fn space_glyph(font: &Font) -> Option<GlyphId> {
650 font.charmap()
651 .map_unicode(' ')
652 .map(|glyph| GlyphId(glyph.to_u32()))
653}