1#![warn(
6 missing_docs,
7 clippy::all,
8 clippy::correctness,
9 clippy::suspicious,
10 clippy::style,
11 clippy::complexity,
12 clippy::perf,
13 clippy::pedantic,
14 clippy::cargo,
15 clippy::nursery
16)]
17
18mod default;
19mod error;
20mod face;
21
22use std::fmt::Debug;
23use std::sync::OnceLock;
24
25use dashmap::DashMap;
26use geom::{BezPath, Rect, Shape};
27use log::warn;
28use ttf_parser::GlyphId;
29
30pub use self::error::{Error, Result};
31use face::Face;
32
33#[derive(Debug, Clone)]
35#[non_exhaustive]
36pub struct Glyph {
37 pub path: BezPath,
39 pub bounds: Rect,
42 pub advance: f64,
44}
45
46impl Glyph {
47 fn parse_from(face: &Face, gid: GlyphId) -> Option<Self> {
48 struct BezPathBuilder(BezPath);
49
50 impl ttf_parser::OutlineBuilder for BezPathBuilder {
52 fn move_to(&mut self, x: f32, y: f32) {
53 self.0.move_to((x.into(), (-y).into()));
55 }
56
57 fn line_to(&mut self, x: f32, y: f32) {
58 self.0.line_to((x.into(), (-y).into()));
60 }
61
62 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
63 self.0
65 .quad_to((x1.into(), (-y1).into()), (x.into(), (-y).into()));
66 }
67
68 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
69 self.0.curve_to(
71 (x1.into(), (-y1).into()),
72 (x2.into(), (-y2).into()),
73 (x.into(), (-y).into()),
74 );
75 }
76
77 fn close(&mut self) {
78 self.0.close_path();
79 }
80 }
81 let mut builder = BezPathBuilder(BezPath::new());
84
85 let bounds = face.outline_glyph(gid, &mut builder)?;
86 let path = builder.0;
87
88 let bounds = Rect::new(
89 f64::from(bounds.x_min),
90 f64::from(bounds.y_min),
91 f64::from(bounds.x_max),
92 f64::from(bounds.y_max),
93 );
94
95 let advance = f64::from(face.glyph_hor_advance(gid)?);
96
97 Some(Self {
98 path,
99 bounds,
100 advance,
101 })
102 }
103}
104
105#[derive(Clone)]
107pub struct Font {
108 face: Face,
109 family: OnceLock<String>,
110 name: OnceLock<String>,
111 cap_height: OnceLock<f64>,
112 x_height: OnceLock<f64>,
113 notdef: OnceLock<Glyph>,
114 glyphs: DashMap<char, Option<Glyph>>,
115 kerning: DashMap<(char, char), f64>,
116}
117
118impl Debug for Font {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_tuple("Font").field(self.name()).finish()
121 }
122}
123
124impl Default for Font {
125 fn default() -> Self {
126 Self::default_ref().clone()
127 }
128}
129
130impl Font {
131 #[must_use]
136 pub fn default_ref() -> &'static Self {
137 default::font()
138 }
139
140 pub fn from_ttf(data: Vec<u8>) -> Result<Self> {
146 Ok(Self {
147 face: Face::from_ttf(data)?,
148 family: OnceLock::new(),
149 name: OnceLock::new(),
150 cap_height: OnceLock::new(),
151 x_height: OnceLock::new(),
152 notdef: OnceLock::new(),
153 glyphs: DashMap::new(),
154 kerning: DashMap::new(),
155 })
156 }
157
158 pub fn family(&self) -> &String {
162 self.family.get_or_init(|| {
163 self.face
164 .names()
165 .into_iter()
166 .filter(|n| n.name_id == ttf_parser::name_id::FAMILY)
167 .find_map(|n| n.to_string())
168 .unwrap_or_else(|| {
169 warn!("cannot read font family name");
170 "unknown".to_owned()
171 })
172 })
173 }
174
175 pub fn name(&self) -> &String {
179 self.name.get_or_init(|| {
180 self.face
181 .names()
182 .into_iter()
183 .filter(|n| n.name_id == ttf_parser::name_id::FULL_NAME)
184 .find_map(|n| n.to_string())
185 .unwrap_or_else(|| {
186 warn!("cannot read font full name");
187 "unknown".to_owned()
188 })
189 })
190 }
191
192 pub fn em_size(&self) -> f64 {
194 f64::from(self.face.units_per_em())
195 }
196
197 pub fn cap_height(&self) -> f64 {
202 *self.cap_height.get_or_init(|| {
203 self.face
204 .capital_height()
205 .map(f64::from)
206 .or_else(|| self.glyph('M').map(|g| g.path.bounding_box().height()))
207 .unwrap_or_else(|| {
208 default::cap_height() / default::line_height() * self.line_height()
209 })
210 })
211 }
212
213 pub fn x_height(&self) -> f64 {
218 *self.x_height.get_or_init(|| {
219 self.face
220 .x_height()
221 .map(f64::from)
222 .or_else(|| self.glyph('x').map(|g| g.path.bounding_box().height()))
223 .unwrap_or_else(|| {
224 default::x_height() / default::line_height() * self.line_height()
225 })
226 })
227 }
228
229 pub fn ascender(&self) -> f64 {
231 f64::from(self.face.ascender())
232 }
233
234 pub fn descender(&self) -> f64 {
236 -f64::from(self.face.descender())
237 }
238
239 pub fn line_gap(&self) -> f64 {
241 f64::from(self.face.line_gap())
242 }
243
244 pub fn line_height(&self) -> f64 {
248 self.ascender() + self.descender() + self.line_gap()
249 }
250
251 pub fn slope(&self) -> Option<f64> {
253 self.face
254 .italic_angle()
255 .map(f64::from)
256 .map(std::ops::Neg::neg) }
258
259 pub fn num_glyphs(&self) -> usize {
261 usize::from(self.face.number_of_glyphs())
262 }
263
264 #[allow(clippy::missing_panics_doc)] pub fn glyph(&self, char: char) -> Option<Glyph> {
267 self.glyphs
268 .entry(char)
269 .or_insert_with(|| {
270 self.face
271 .glyph_index(char)
272 .and_then(|gid| Glyph::parse_from(&self.face, gid))
273 })
274 .clone()
275 }
276
277 pub fn glyph_or_default(&self, char: char) -> Glyph {
279 self.glyph(char).unwrap_or_else(|| self.notdef())
280 }
281
282 pub fn notdef(&self) -> Glyph {
284 self.notdef
285 .get_or_init(|| {
286 Glyph::parse_from(&self.face, GlyphId(0)).unwrap_or_else(|| {
287 warn!("no valid outline for glyph .notdef in font");
288 default::notdef()
289 })
290 })
291 .clone()
292 }
293
294 #[allow(clippy::missing_panics_doc)] pub fn kerning(&self, left: char, right: char) -> f64 {
298 *self.kerning.entry((left, right)).or_insert_with(|| {
299 if let (Some(lhs), Some(rhs)) =
300 (self.face.glyph_index(left), self.face.glyph_index(right))
301 {
302 self.face.glyphs_kerning(lhs, rhs).map_or(0.0, f64::from)
303 } else {
304 0.0
305 }
306 })
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use assert_approx_eq::assert_approx_eq;
313 use assert_matches::assert_matches;
314
315 use super::*;
316
317 #[test]
318 fn glyph_parse_from() {
319 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
320 let face = Face::from_ttf(data).unwrap();
321
322 let a = Glyph::parse_from(&face, GlyphId(1)).unwrap();
323 assert_approx_eq!(a.advance, 540.0);
324 assert_approx_eq!(a.bounds.width(), 535.0);
325 assert_approx_eq!(a.bounds.height(), 656.0);
326
327 let v = Glyph::parse_from(&face, GlyphId(2)).unwrap();
328 assert_approx_eq!(v.advance, 540.0);
329 assert_approx_eq!(v.bounds.width(), 535.0);
330 assert_approx_eq!(v.bounds.height(), 656.0);
331 }
332
333 #[test]
334 fn font_debug() {
335 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
336 let font = Font::from_ttf(data).unwrap();
337
338 assert_eq!(format!("{:?}", font), r#"Font("demo regular")"#);
339 }
340
341 #[test]
342 fn font_clone() {
343 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
344 let font = Font::from_ttf(data).unwrap();
345
346 let _ = font.clone(); }
348
349 #[test]
350 fn font_default() {
351 let _ = Font::default(); }
353
354 #[test]
355 fn font_from_ttf() {
356 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
357 let font = Font::from_ttf(data).unwrap();
358
359 assert_matches!(font.face, Face { .. });
360 assert!(font.family.get().is_none());
361 assert!(font.name.get().is_none());
362 assert!(font.cap_height.get().is_none());
363 assert!(font.x_height.get().is_none());
364 assert!(font.notdef.get().is_none());
365 assert_eq!(font.glyphs.len(), 0);
366 assert_eq!(font.kerning.len(), 0);
367 }
368
369 #[test]
370 fn font_properties() {
371 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
372 let font = Font::from_ttf(data).unwrap();
373
374 assert_eq!(font.family(), "demo");
375 assert_eq!(font.name(), "demo regular");
376 assert_approx_eq!(font.em_size(), 1000.0);
377 assert_approx_eq!(font.cap_height(), 650.0);
378 assert_approx_eq!(font.x_height(), 450.0);
379 assert_approx_eq!(font.ascender(), 1024.0);
380 assert_approx_eq!(font.descender(), 400.0);
381 assert_approx_eq!(font.line_gap(), 0.0);
382 assert_approx_eq!(font.line_height(), 1424.0);
383 assert_eq!(font.slope(), None);
384 assert_eq!(font.num_glyphs(), 3);
385
386 let data = std::fs::read(env!("NULL_TTF")).unwrap();
387 let font = Font::from_ttf(data).unwrap();
388
389 let line_scaling = font.line_height() / default::line_height();
390 assert_eq!(font.family(), "unknown");
391 assert_eq!(font.name(), "unknown");
392 assert_approx_eq!(font.em_size(), 1000.0);
393 assert_approx_eq!(font.cap_height(), default::cap_height() * line_scaling);
394 assert_approx_eq!(font.x_height(), default::x_height() * line_scaling);
395 assert_approx_eq!(font.ascender(), 600.0);
396 assert_approx_eq!(font.descender(), 400.0);
397 assert_approx_eq!(font.line_gap(), 200.0);
398 assert_approx_eq!(font.line_height(), 1200.0);
399 assert_eq!(font.slope(), None);
400 assert_eq!(font.num_glyphs(), 1); }
402
403 #[test]
404 fn font_glyph() {
405 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
406 let font = Font::from_ttf(data).unwrap();
407
408 assert!(font.glyph('A').is_some());
409 assert!(font.glyph('B').is_none());
410 }
411
412 #[test]
413 fn font_glyph_or_default() {
414 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
415 let font = Font::from_ttf(data).unwrap();
416
417 assert_ne!(font.glyph_or_default('A').path, font.notdef().path);
418 assert_eq!(font.glyph_or_default('B').path, font.notdef().path);
419 }
420
421 #[test]
422 fn font_notdef() {
423 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
424 let font = Font::from_ttf(data).unwrap();
425
426 assert_eq!(font.notdef().path.elements().len(), 12);
427
428 let data = std::fs::read(env!("NULL_TTF")).unwrap();
429 let font = Font::from_ttf(data).unwrap();
430
431 assert_eq!(font.notdef().path.elements().len(), 26);
432 }
433
434 #[test]
435 fn font_kerning() {
436 let data = std::fs::read(env!("DEMO_TTF")).unwrap();
437 let font = Font::from_ttf(data).unwrap();
438
439 assert_approx_eq!(font.kerning('A', 'V'), -70.0);
440 assert_approx_eq!(font.kerning('A', 'B'), 0.0);
441 }
442}