use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use skrifa::attribute::Style;
use skrifa::raw::tables::head::Head;
use skrifa::raw::tables::hhea::Hhea;
use skrifa::raw::TableProvider;
use skrifa::{FontRef, MetadataProvider, Tag};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FontError {
UnsupportedFont,
ParseError,
MalformedFont,
}
impl core::fmt::Display for FontError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FontError::UnsupportedFont => write!(f, "unsupported font (need static TrueType glyf)"),
FontError::ParseError => write!(f, "could not parse font data"),
FontError::MalformedFont => write!(f, "malformed or inconsistent font tables"),
}
}
}
pub(crate) fn require_table<T>(table: Result<T, skrifa::raw::ReadError>) -> Result<T, FontError> {
table.map_err(|_| FontError::MalformedFont)
}
pub(crate) fn require_head_hhea<'a>(font: &FontRef<'a>) -> Result<(Head<'a>, Hhea<'a>), FontError> {
let head = require_table(font.head())?;
let hhea = require_table(font.hhea())?;
Ok((head, hhea))
}
#[derive(Clone, Debug)]
pub struct FontData {
bytes: Arc<[u8]>,
}
impl FontData {
pub fn load(bytes: impl Into<Arc<[u8]>>) -> Result<Self, FontError> {
let bytes: Arc<[u8]> = bytes.into();
let font = FontRef::new(&bytes).map_err(|_| FontError::ParseError)?;
if font.data_for_tag(Tag::new(b"glyf")).is_none() {
return Err(FontError::UnsupportedFont);
}
if font.data_for_tag(Tag::new(b"fvar")).is_some() {
return Err(FontError::UnsupportedFont);
}
Ok(FontData { bytes })
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn with_font<R>(&self, f: impl FnOnce(&FontRef) -> R) -> Result<R, FontError> {
let font = FontRef::new(&self.bytes).map_err(|_| FontError::ParseError)?;
Ok(f(&font))
}
}
#[derive(Debug)]
pub struct EmbeddedFontMetrics {
font: FontData,
advance_cache: RefCell<HashMap<char, f32>>,
pub ascent: f32,
pub descent: f32,
pub cap_height: f32,
pub italic_angle: f32,
pub bbox: (f32, f32, f32, f32),
pub is_italic: bool,
pub is_bold: bool,
}
impl Clone for EmbeddedFontMetrics {
fn clone(&self) -> Self {
EmbeddedFontMetrics {
font: self.font.clone(),
advance_cache: RefCell::new(self.advance_cache.borrow().clone()),
ascent: self.ascent,
descent: self.descent,
cap_height: self.cap_height,
italic_angle: self.italic_angle,
bbox: self.bbox,
is_italic: self.is_italic,
is_bold: self.is_bold,
}
}
}
impl EmbeddedFontMetrics {
pub fn from_font_data(data: &FontData) -> Result<Self, FontError> {
data.with_font(|font| -> Result<Self, FontError> {
let (head, hhea) = require_head_hhea(font)?;
let upem = head.units_per_em() as f32;
let scale = 1000.0 / upem;
let ascent = hhea.ascender().to_i16() as f32 * scale;
let cap_height = font
.os2()
.ok()
.and_then(|os2| os2.s_cap_height())
.map(|c| c as f32 * scale)
.unwrap_or(ascent);
let italic_angle = font.post().ok().map(|post| post.italic_angle().to_f64() as f32).unwrap_or(0.0);
let attrs = font.attributes();
Ok(EmbeddedFontMetrics {
font: data.clone(),
advance_cache: RefCell::new(HashMap::new()),
ascent,
descent: hhea.descender().to_i16() as f32 * scale,
cap_height,
italic_angle,
bbox: (
head.x_min() as f32 * scale,
head.y_min() as f32 * scale,
head.x_max() as f32 * scale,
head.y_max() as f32 * scale,
),
is_italic: attrs.style != Style::Normal,
is_bold: attrs.weight >= skrifa::attribute::Weight::BOLD,
})
})?
}
pub fn advance_1000(&self, ch: char) -> Option<f32> {
if let Some(w) = self.advance_cache.borrow().get(&ch) {
return Some(*w);
}
let advance = self
.font
.with_font(|font| {
let upem = font.head().ok()?.units_per_em() as f32;
let gid = font.charmap().map(ch)?;
font.hmtx().ok()?.advance(gid).map(|a| a as f32 * 1000.0 / upem)
})
.ok()
.flatten();
if let Some(w) = advance {
self.advance_cache.borrow_mut().insert(ch, w);
}
advance
}
pub fn glyph_id(&self, ch: char) -> Option<u16> {
self.font
.with_font(|font| {
font.charmap().map(ch).and_then(|g| u16::try_from(g.to_u32()).ok())
})
.ok()
.flatten()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn regular_bytes() -> Vec<u8> {
std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/SourceSans3-Regular.ttf")).expect("test font asset present")
}
fn bold_bytes() -> Vec<u8> {
std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/SourceSans3-Bold.ttf")).expect("test font asset present")
}
#[test]
fn loads_static_glyf_font() {
let data = FontData::load(regular_bytes()).unwrap();
assert!(data.bytes().len() > 1000);
}
#[test]
fn rejects_garbage_bytes() {
assert_eq!(FontData::load(vec![0u8; 16]).unwrap_err(), FontError::ParseError);
}
#[test]
fn computes_plausible_metrics_for_regular_and_bold() {
for bytes in [regular_bytes(), bold_bytes()] {
let data = FontData::load(bytes).unwrap();
let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
let w = metrics.advance_1000('H').unwrap();
assert!(w > 400.0 && w < 900.0, "unexpected advance for 'H': {w}");
assert!(metrics.ascent > 0.0);
assert!(metrics.descent < 0.0);
for ch in ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß', '€', '–', '„', '"'] {
assert!(metrics.advance_1000(ch).is_some(), "missing glyph for {ch:?}");
assert!(metrics.glyph_id(ch).is_some(), "missing glyph id for {ch:?}");
}
}
}
#[test]
fn advance_lookup_is_cached_and_repeatable() {
let data = FontData::load(regular_bytes()).unwrap();
let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
let first = metrics.advance_1000('x').unwrap();
let second = metrics.advance_1000('x').unwrap();
assert_eq!(first, second);
}
#[test]
fn unrepresentable_glyph_is_none_not_a_panic() {
let data = FontData::load(regular_bytes()).unwrap();
let metrics = EmbeddedFontMetrics::from_font_data(&data).unwrap();
assert_eq!(metrics.advance_1000('\u{E000}'), None);
assert_eq!(metrics.glyph_id('\u{E000}'), None);
}
}