lightweight_pdf_fonts/
font.rs1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use skrifa::attribute::Style;
6use skrifa::raw::TableProvider;
7use skrifa::{FontRef, MetadataProvider, Tag};
8
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub enum FontError {
11 UnsupportedFont,
14 ParseError,
16 MalformedFont,
22}
23
24impl core::fmt::Display for FontError {
25 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26 match self {
27 FontError::UnsupportedFont => write!(f, "unsupported font (need static TrueType glyf)"),
28 FontError::ParseError => write!(f, "could not parse font data"),
29 FontError::MalformedFont => write!(f, "malformed or inconsistent font tables"),
30 }
31 }
32}
33
34#[derive(Clone, Debug)]
37pub struct FontData {
38 bytes: Arc<[u8]>,
39}
40
41impl FontData {
42 pub fn load(bytes: impl Into<Arc<[u8]>>) -> Result<Self, FontError> {
47 let bytes: Arc<[u8]> = bytes.into();
48 let font = FontRef::new(&bytes).map_err(|_| FontError::ParseError)?;
49 if font.data_for_tag(Tag::new(b"glyf")).is_none() {
50 return Err(FontError::UnsupportedFont);
51 }
52 if font.data_for_tag(Tag::new(b"fvar")).is_some() {
53 return Err(FontError::UnsupportedFont);
54 }
55 Ok(FontData { bytes })
56 }
57
58 pub fn bytes(&self) -> &[u8] {
59 &self.bytes
60 }
61
62 pub fn with_font<R>(&self, f: impl FnOnce(&FontRef) -> R) -> Result<R, FontError> {
63 let font = FontRef::new(&self.bytes).map_err(|_| FontError::ParseError)?;
64 Ok(f(&font))
65 }
66}
67
68#[derive(Debug)]
77pub struct EmbeddedFontMetrics {
78 font: FontData,
79 advance_cache: RefCell<HashMap<char, f32>>,
80 pub ascent: f32,
81 pub descent: f32,
82 pub cap_height: f32,
83 pub italic_angle: f32,
84 pub bbox: (f32, f32, f32, f32),
86 pub is_italic: bool,
87 pub is_bold: bool,
88}
89
90impl Clone for EmbeddedFontMetrics {
91 fn clone(&self) -> Self {
92 EmbeddedFontMetrics {
93 font: self.font.clone(),
94 advance_cache: RefCell::new(self.advance_cache.borrow().clone()),
95 ascent: self.ascent,
96 descent: self.descent,
97 cap_height: self.cap_height,
98 italic_angle: self.italic_angle,
99 bbox: self.bbox,
100 is_italic: self.is_italic,
101 is_bold: self.is_bold,
102 }
103 }
104}
105
106impl EmbeddedFontMetrics {
107 pub fn from_font_data(data: &FontData) -> Result<Self, FontError> {
108 data.with_font(|font| -> Result<Self, FontError> {
109 let head = font.head().map_err(|_| FontError::MalformedFont)?;
110 let hhea = font.hhea().map_err(|_| FontError::MalformedFont)?;
111 let upem = head.units_per_em() as f32;
112 let scale = 1000.0 / upem;
113 let ascent = hhea.ascender().to_i16() as f32 * scale;
114 let cap_height = font
115 .os2()
116 .ok()
117 .and_then(|os2| os2.s_cap_height())
118 .map(|c| c as f32 * scale)
119 .unwrap_or(ascent);
120 let italic_angle = font.post().ok().map(|post| post.italic_angle().to_f64() as f32).unwrap_or(0.0);
121 let attrs = font.attributes();
126 Ok(EmbeddedFontMetrics {
127 font: data.clone(),
128 advance_cache: RefCell::new(HashMap::new()),
129 ascent,
130 descent: hhea.descender().to_i16() as f32 * scale,
131 cap_height,
132 italic_angle,
133 bbox: (
134 head.x_min() as f32 * scale,
135 head.y_min() as f32 * scale,
136 head.x_max() as f32 * scale,
137 head.y_max() as f32 * scale,
138 ),
139 is_italic: attrs.style != Style::Normal,
140 is_bold: attrs.weight >= skrifa::attribute::Weight::BOLD,
141 })
142 })?
143 }
144
145 pub fn advance_1000(&self, ch: char) -> Option<f32> {
148 if let Some(w) = self.advance_cache.borrow().get(&ch) {
149 return Some(*w);
150 }
151 let advance = self
152 .font
153 .with_font(|font| {
154 let upem = font.head().ok()?.units_per_em() as f32;
155 let gid = font.charmap().map(ch)?;
156 font.hmtx().ok()?.advance(gid).map(|a| a as f32 * 1000.0 / upem)
157 })
158 .ok()
159 .flatten();
160 if let Some(w) = advance {
161 self.advance_cache.borrow_mut().insert(ch, w);
162 }
163 advance
164 }
165
166 pub fn glyph_id(&self, ch: char) -> Option<u16> {
170 self.font
171 .with_font(|font| font.charmap().map(ch).map(|g| g.to_u32() as u16))
172 .ok()
173 .flatten()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 fn regular_bytes() -> Vec<u8> {
182 std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/SourceSans3-Regular.ttf")).expect("test font asset present")
183 }
184
185 fn bold_bytes() -> Vec<u8> {
186 std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/SourceSans3-Bold.ttf")).expect("test font asset present")
187 }
188
189 #[test]
190 fn loads_static_glyf_font() {
191 let data = FontData::load(regular_bytes()).unwrap();
192 assert!(data.bytes().len() > 1000);
193 }
194
195 #[test]
196 fn rejects_garbage_bytes() {
197 assert_eq!(FontData::load(vec![0u8; 16]).unwrap_err(), FontError::ParseError);
198 }
199
200 #[test]
201 fn computes_plausible_metrics_for_regular_and_bold() {
202 for bytes in [regular_bytes(), bold_bytes()] {
203 let data = FontData::load(bytes).unwrap();
204 let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
205 let w = metrics.advance_1000('H').unwrap();
206 assert!(w > 400.0 && w < 900.0, "unexpected advance for 'H': {w}");
207 assert!(metrics.ascent > 0.0);
208 assert!(metrics.descent < 0.0);
209 for ch in ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß', '€', '–', '„', '"'] {
211 assert!(metrics.advance_1000(ch).is_some(), "missing glyph for {ch:?}");
212 assert!(metrics.glyph_id(ch).is_some(), "missing glyph id for {ch:?}");
213 }
214 }
215 }
216
217 #[test]
218 fn advance_lookup_is_cached_and_repeatable() {
219 let data = FontData::load(regular_bytes()).unwrap();
220 let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
221 let first = metrics.advance_1000('x').unwrap();
222 let second = metrics.advance_1000('x').unwrap();
223 assert_eq!(first, second);
224 }
225
226 #[test]
227 fn unrepresentable_glyph_is_none_not_a_panic() {
228 let data = FontData::load(regular_bytes()).unwrap();
229 let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
230 assert_eq!(metrics.advance_1000('\u{E000}'), None);
232 assert_eq!(metrics.glyph_id('\u{E000}'), None);
233 }
234}