use std::collections::{BTreeMap, BTreeSet};
use ttf_parser::{GlyphId, name_id};
mod glyph;
mod pbf;
const FONT_SIZE_PX: u32 = 24;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid font: {0}")]
InvalidFont(String),
#[error("unsupported font: {0}")]
UnsupportedFont(String),
#[error("range start {0} is not a multiple of 256")]
InvalidRangeStart(u32),
}
pub struct FontFace {
font: fontdue::Font,
family_name: String,
style_name: String,
fontstack_name: String,
ascender_round: i32,
charmap: BTreeMap<u32, u16>,
advances: Vec<u32>,
}
impl FontFace {
pub fn parse(bytes: &[u8]) -> Result<FontFace, Error> {
let face = ttf_parser::Face::parse(bytes, 0)
.map_err(|error| Error::InvalidFont(error.to_string()))?;
if face.tables().colr.is_some() {
return Err(Error::UnsupportedFont(
"COLR color fonts are not supported".to_owned(),
));
}
let family_name = preferred_name(
&face,
name_id::TYPOGRAPHIC_FAMILY,
name_id::FAMILY,
"family",
)?;
let style_name = preferred_name(
&face,
name_id::TYPOGRAPHIC_SUBFAMILY,
name_id::SUBFAMILY,
"style",
)?;
let fontstack_name = format!("{family_name} {style_name}");
let units_per_em = face.units_per_em();
let ascender_round = (f64::from(face.ascender()) * f64::from(FONT_SIZE_PX)
/ f64::from(units_per_em))
.round() as i32;
let charmap = extract_charmap(&face);
let advances = (0..face.number_of_glyphs())
.map(|glyph_index| {
face.glyph_hor_advance(GlyphId(glyph_index))
.map(|advance| {
glyph::freetype_advance(u32::from(advance), units_per_em, FONT_SIZE_PX)
})
.unwrap_or(0)
})
.collect();
let font = fontdue::Font::from_bytes(
bytes,
fontdue::FontSettings {
scale: FONT_SIZE_PX as f32,
collection_index: 0,
..fontdue::FontSettings::default()
},
)
.map_err(|message| Error::InvalidFont(message.to_owned()))?;
Ok(FontFace {
font,
family_name,
style_name,
fontstack_name,
ascender_round,
charmap,
advances,
})
}
pub fn family_name(&self) -> &str {
&self.family_name
}
pub fn style_name(&self) -> &str {
&self.style_name
}
pub fn fontstack_name(&self) -> &str {
&self.fontstack_name
}
pub fn covered_ranges(&self) -> Vec<u32> {
self.charmap
.keys()
.map(|codepoint| codepoint & !0xff)
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
pub fn glyph_count(&self) -> usize {
self.charmap.len()
}
}
pub fn generate_range(face: &FontFace, start: u32) -> Result<Vec<u8>, Error> {
if !start.is_multiple_of(256) {
return Err(Error::InvalidRangeStart(start));
}
let end = start + 255;
let glyphs = face
.charmap
.range(start..=end)
.map(|(&codepoint, &glyph_index)| glyph::generate(face, codepoint, glyph_index))
.collect::<Vec<_>>();
let range = format!("{start}-{end}");
Ok(pbf::encode_fontstack(
face.fontstack_name(),
&range,
&glyphs,
))
}
fn preferred_name(
face: &ttf_parser::Face<'_>,
preferred_id: u16,
fallback_id: u16,
kind: &str,
) -> Result<String, Error> {
for name_id in [preferred_id, fallback_id] {
if let Some(value) = face
.names()
.into_iter()
.filter(|name| name.name_id == name_id && name.is_unicode())
.filter_map(|name| name.to_string())
.find(|value| !value.is_empty())
{
return Ok(value);
}
}
Err(Error::UnsupportedFont(format!(
"missing Unicode {kind} name (name IDs {preferred_id} and {fallback_id})"
)))
}
fn extract_charmap(face: &ttf_parser::Face<'_>) -> BTreeMap<u32, u16> {
let mut charmap = BTreeMap::new();
let Some(cmap) = face.tables().cmap else {
return charmap;
};
for subtable in cmap
.subtables
.into_iter()
.filter(|table| table.is_unicode())
{
subtable.codepoints(|codepoint| {
if char::from_u32(codepoint).is_none() {
return;
}
if let Some(glyph_id) = subtable.glyph_index(codepoint)
&& glyph_id.0 != 0
{
charmap.insert(codepoint, glyph_id.0);
}
});
}
charmap
}
#[cfg(test)]
mod tests;