1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use skrifa::attribute::Style;
6use skrifa::raw::tables::head::Head;
7use skrifa::raw::tables::hhea::Hhea;
8use skrifa::raw::TableProvider;
9use skrifa::{FontRef, MetadataProvider, Tag};
10
11#[derive(Clone, Copy, PartialEq, Eq, Debug)]
12pub enum FontError {
13 UnsupportedFont,
16 ParseError,
18 MalformedFont,
24}
25
26impl core::fmt::Display for FontError {
27 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28 match self {
29 FontError::UnsupportedFont => write!(f, "unsupported font (need static TrueType glyf)"),
30 FontError::ParseError => write!(f, "could not parse font data"),
31 FontError::MalformedFont => write!(f, "malformed or inconsistent font tables"),
32 }
33 }
34}
35
36pub(crate) fn require_table<T>(table: Result<T, skrifa::raw::ReadError>) -> Result<T, FontError> {
44 table.map_err(|_| FontError::MalformedFont)
45}
46
47pub(crate) fn require_head_hhea<'a>(font: &FontRef<'a>) -> Result<(Head<'a>, Hhea<'a>), FontError> {
52 let head = require_table(font.head())?;
53 let hhea = require_table(font.hhea())?;
54 Ok((head, hhea))
55}
56
57#[derive(Clone, Debug)]
60pub struct FontData {
61 bytes: Arc<[u8]>,
62}
63
64impl FontData {
65 pub fn load(bytes: impl Into<Arc<[u8]>>) -> Result<Self, FontError> {
70 let bytes: Arc<[u8]> = bytes.into();
71 let font = FontRef::new(&bytes).map_err(|_| FontError::ParseError)?;
72 if font.data_for_tag(Tag::new(b"glyf")).is_none() {
73 return Err(FontError::UnsupportedFont);
74 }
75 if font.data_for_tag(Tag::new(b"fvar")).is_some() {
76 return Err(FontError::UnsupportedFont);
77 }
78 Ok(FontData { bytes })
79 }
80
81 pub fn bytes(&self) -> &[u8] {
82 &self.bytes
83 }
84
85 pub fn with_font<R>(&self, f: impl FnOnce(&FontRef) -> R) -> Result<R, FontError> {
86 let font = FontRef::new(&self.bytes).map_err(|_| FontError::ParseError)?;
87 Ok(f(&font))
88 }
89}
90
91#[derive(Debug)]
100pub struct EmbeddedFontMetrics {
101 font: FontData,
102 advance_cache: RefCell<HashMap<char, f32>>,
103 pub ascent: f32,
104 pub descent: f32,
105 pub cap_height: f32,
106 pub italic_angle: f32,
107 pub bbox: (f32, f32, f32, f32),
109 pub is_italic: bool,
110 pub is_bold: bool,
111}
112
113impl Clone for EmbeddedFontMetrics {
114 fn clone(&self) -> Self {
115 EmbeddedFontMetrics {
116 font: self.font.clone(),
117 advance_cache: RefCell::new(self.advance_cache.borrow().clone()),
118 ascent: self.ascent,
119 descent: self.descent,
120 cap_height: self.cap_height,
121 italic_angle: self.italic_angle,
122 bbox: self.bbox,
123 is_italic: self.is_italic,
124 is_bold: self.is_bold,
125 }
126 }
127}
128
129impl EmbeddedFontMetrics {
130 pub fn from_font_data(data: &FontData) -> Result<Self, FontError> {
131 data.with_font(|font| -> Result<Self, FontError> {
132 let (head, hhea) = require_head_hhea(font)?;
133 let upem = head.units_per_em() as f32;
134 let scale = 1000.0 / upem;
135 let ascent = hhea.ascender().to_i16() as f32 * scale;
136 let cap_height = font
137 .os2()
138 .ok()
139 .and_then(|os2| os2.s_cap_height())
140 .map(|c| c as f32 * scale)
141 .unwrap_or(ascent);
142 let italic_angle = font.post().ok().map(|post| post.italic_angle().to_f64() as f32).unwrap_or(0.0);
143 let attrs = font.attributes();
148 Ok(EmbeddedFontMetrics {
149 font: data.clone(),
150 advance_cache: RefCell::new(HashMap::new()),
151 ascent,
152 descent: hhea.descender().to_i16() as f32 * scale,
153 cap_height,
154 italic_angle,
155 bbox: (
156 head.x_min() as f32 * scale,
157 head.y_min() as f32 * scale,
158 head.x_max() as f32 * scale,
159 head.y_max() as f32 * scale,
160 ),
161 is_italic: attrs.style != Style::Normal,
162 is_bold: attrs.weight >= skrifa::attribute::Weight::BOLD,
163 })
164 })?
165 }
166
167 pub fn advance_1000(&self, ch: char) -> Option<f32> {
170 if let Some(w) = self.advance_cache.borrow().get(&ch) {
171 return Some(*w);
172 }
173 let advance = self
174 .font
175 .with_font(|font| {
176 let upem = font.head().ok()?.units_per_em() as f32;
177 let gid = font.charmap().map(ch)?;
178 font.hmtx().ok()?.advance(gid).map(|a| a as f32 * 1000.0 / upem)
179 })
180 .ok()
181 .flatten();
182 if let Some(w) = advance {
183 self.advance_cache.borrow_mut().insert(ch, w);
184 }
185 advance
186 }
187
188 pub fn glyph_id(&self, ch: char) -> Option<u16> {
192 self.font
193 .with_font(|font| {
194 font.charmap().map(ch).and_then(|g| u16::try_from(g.to_u32()).ok())
200 })
201 .ok()
202 .flatten()
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn regular_bytes() -> Vec<u8> {
211 std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/SourceSans3-Regular.ttf")).expect("test font asset present")
212 }
213
214 fn bold_bytes() -> Vec<u8> {
215 std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/SourceSans3-Bold.ttf")).expect("test font asset present")
216 }
217
218 #[test]
219 fn loads_static_glyf_font() {
220 let data = FontData::load(regular_bytes()).unwrap();
221 assert!(data.bytes().len() > 1000);
222 }
223
224 #[test]
225 fn rejects_garbage_bytes() {
226 assert_eq!(FontData::load(vec![0u8; 16]).unwrap_err(), FontError::ParseError);
227 }
228
229 #[test]
230 fn computes_plausible_metrics_for_regular_and_bold() {
231 for bytes in [regular_bytes(), bold_bytes()] {
232 let data = FontData::load(bytes).unwrap();
233 let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
234 let w = metrics.advance_1000('H').unwrap();
235 assert!(w > 400.0 && w < 900.0, "unexpected advance for 'H': {w}");
236 assert!(metrics.ascent > 0.0);
237 assert!(metrics.descent < 0.0);
238 for ch in ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß', '€', '–', '„', '"'] {
240 assert!(metrics.advance_1000(ch).is_some(), "missing glyph for {ch:?}");
241 assert!(metrics.glyph_id(ch).is_some(), "missing glyph id for {ch:?}");
242 }
243 }
244 }
245
246 #[test]
247 fn advance_lookup_is_cached_and_repeatable() {
248 let data = FontData::load(regular_bytes()).unwrap();
249 let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
250 let first = metrics.advance_1000('x').unwrap();
251 let second = metrics.advance_1000('x').unwrap();
252 assert_eq!(first, second);
253 }
254
255 #[test]
256 fn unrepresentable_glyph_is_none_not_a_panic() {
257 let data = FontData::load(regular_bytes()).unwrap();
258 let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
259 assert_eq!(metrics.advance_1000('\u{E000}'), None);
261 assert_eq!(metrics.glyph_id('\u{E000}'), None);
262 }
263}