#![deny(missing_debug_implementations)]
#![warn(rust_2018_idioms)]
pub mod collection;
pub mod outline;
pub mod parser;
pub mod tables;
pub use collection::{is_collection, CollectionHeader, TTC_MAGIC};
use crate::parser::TableDirectory;
use crate::tables::{
cbdt::CbdtTable, cblc::CblcTable, cmap::CmapTable, gdef::GdefTable, glyf::GlyfTable,
gpos::GposTable, gsub::GsubTable, head::HeadTable, hhea::HheaTable, hmtx::HmtxTable,
kern::KernTable, loca::LocaTable, maxp::MaxpTable, name::NameTable, os2::Os2Table,
post::PostTable,
};
pub use outline::{BBox, Contour, Point, TtOutline};
pub use tables::cbdt::ColorBitmap;
pub use tables::cblc::{BigGlyphMetrics, SmallGlyphMetrics};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
UnexpectedEof,
BadMagic,
BadHeader,
MissingTable(&'static str),
BadOffset,
GlyphOutOfRange(u16),
UnsupportedCmapFormat(u16),
CompositeTooDeep,
BadLocaOffset,
BadStructure(&'static str),
SubfontOutOfRange(u32),
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnexpectedEof => f.write_str("unexpected end of font data"),
Self::BadMagic => f.write_str("not a TrueType / OpenType font (bad magic)"),
Self::BadHeader => f.write_str("malformed sfnt header"),
Self::MissingTable(t) => write!(f, "required table missing: {t}"),
Self::BadOffset => f.write_str("table offset out of range"),
Self::GlyphOutOfRange(g) => write!(f, "glyph index {g} out of range"),
Self::UnsupportedCmapFormat(fmt) => {
write!(f, "cmap format {fmt} not implemented in round 1")
}
Self::CompositeTooDeep => f.write_str("composite glyph recursion too deep"),
Self::BadLocaOffset => f.write_str("loca offset past end of glyf"),
Self::BadStructure(s) => write!(f, "malformed structure: {s}"),
Self::SubfontOutOfRange(i) => write!(f, "subfont index {i} not in collection"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug)]
pub struct Font<'a> {
bytes: &'a [u8],
head: HeadTable,
hhea: HheaTable,
maxp: MaxpTable,
cmap: CmapTable<'a>,
name: NameTable<'a>,
os2: Option<Os2Table>,
hmtx: HmtxTable<'a>,
loca: Option<LocaTable<'a>>,
glyf: Option<GlyfTable<'a>>,
post: Option<PostTable>,
kern: Option<KernTable<'a>>,
gsub: Option<GsubTable<'a>>,
gpos: Option<GposTable<'a>>,
gdef: Option<GdefTable<'a>>,
cblc: Option<CblcTable<'a>>,
cbdt: Option<CbdtTable<'a>>,
}
impl<'a> Font<'a> {
pub fn from_collection_bytes(bytes: &'a [u8], index: u32) -> Result<Self, Error> {
let header = CollectionHeader::parse(bytes)?;
let offset = header
.font_offset(index)
.ok_or(Error::SubfontOutOfRange(index))? as usize;
let sub = bytes.get(offset..).ok_or(Error::BadOffset)?;
Self::from_bytes(sub)
}
pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
let dir = TableDirectory::parse(bytes)?;
let head = HeadTable::parse(dir.required(b"head", bytes)?)?;
let hhea = HheaTable::parse(dir.required(b"hhea", bytes)?)?;
let maxp = MaxpTable::parse(dir.required(b"maxp", bytes)?)?;
let cmap = CmapTable::parse(dir.required(b"cmap", bytes)?)?;
let name = NameTable::parse(dir.required(b"name", bytes)?)?;
let hmtx = HmtxTable::parse(
dir.required(b"hmtx", bytes)?,
hhea.num_long_hor_metrics,
maxp.num_glyphs,
)?;
let loca = match (dir.find(b"loca", bytes), dir.find(b"glyf", bytes)) {
(Some(l), Some(_g)) => Some(LocaTable::parse(
l,
maxp.num_glyphs,
head.index_to_loc_format,
)?),
(None, None) => None,
_ => {
return Err(Error::BadStructure(
"loca/glyf must both be present or both absent",
))
}
};
let glyf = dir.find(b"glyf", bytes).map(GlyfTable::new);
let os2 = dir.find(b"OS/2", bytes).map(Os2Table::parse).transpose()?;
let post = dir.find(b"post", bytes).map(PostTable::parse).transpose()?;
let kern = dir.find(b"kern", bytes).map(KernTable::parse).transpose()?;
let gsub = dir.find(b"GSUB", bytes).map(GsubTable::parse).transpose()?;
let gpos = dir.find(b"GPOS", bytes).map(GposTable::parse).transpose()?;
let gdef = dir.find(b"GDEF", bytes).map(GdefTable::parse).transpose()?;
let cblc = dir.find(b"CBLC", bytes).map(CblcTable::parse).transpose()?;
let cbdt = dir.find(b"CBDT", bytes).map(CbdtTable::parse).transpose()?;
Ok(Self {
bytes,
head,
hhea,
maxp,
cmap,
name,
os2,
hmtx,
loca,
glyf,
post,
kern,
gsub,
gpos,
gdef,
cblc,
cbdt,
})
}
pub fn bytes(&self) -> &'a [u8] {
self.bytes
}
pub fn family_name(&self) -> Option<&str> {
self.name.find(1)
}
pub fn full_name(&self) -> Option<&str> {
self.name.find(4)
}
pub fn units_per_em(&self) -> u16 {
self.head.units_per_em
}
pub fn ascent(&self) -> i16 {
self.os2
.as_ref()
.and_then(|o| o.s_typo_ascender)
.unwrap_or(self.hhea.ascent)
}
pub fn descent(&self) -> i16 {
self.os2
.as_ref()
.and_then(|o| o.s_typo_descender)
.unwrap_or(self.hhea.descent)
}
pub fn line_gap(&self) -> i16 {
self.os2
.as_ref()
.and_then(|o| o.s_typo_line_gap)
.unwrap_or(self.hhea.line_gap)
}
pub fn glyph_count(&self) -> u16 {
self.maxp.num_glyphs
}
pub fn weight_class(&self) -> u16 {
self.os2.as_ref().map(|o| o.us_weight_class).unwrap_or(400)
}
pub fn italic_angle(&self) -> f32 {
self.post.as_ref().map(|p| p.italic_angle).unwrap_or(0.0)
}
pub fn glyph_index(&self, codepoint: char) -> Option<u16> {
self.cmap.lookup(codepoint as u32)
}
pub fn glyph_outline(&self, glyph_id: u16) -> Result<TtOutline, Error> {
if glyph_id >= self.maxp.num_glyphs {
return Err(Error::GlyphOutOfRange(glyph_id));
}
let (loca, glyf) = match (self.loca.as_ref(), self.glyf.as_ref()) {
(Some(l), Some(g)) => (l, g),
_ => return Ok(TtOutline::default()),
};
let range = loca.glyph_range(glyph_id)?;
if range.is_empty() {
return Ok(TtOutline::default());
}
glyf.glyph_outline(range, loca, 0)
}
pub fn glyph_advance(&self, glyph_id: u16) -> i16 {
self.hmtx.advance(glyph_id) as i16
}
pub fn glyph_lsb(&self, glyph_id: u16) -> i16 {
self.hmtx.lsb(glyph_id)
}
pub fn glyph_bounding_box(&self, glyph_id: u16) -> Option<BBox> {
if glyph_id >= self.maxp.num_glyphs {
return None;
}
let (loca, glyf) = (self.loca.as_ref()?, self.glyf.as_ref()?);
let range = loca.glyph_range(glyph_id).ok()?;
if range.is_empty() {
return None;
}
glyf.bbox(range)
}
pub fn lookup_ligature(&self, glyphs: &[u16]) -> Option<(u16, usize)> {
self.gsub.as_ref().and_then(|g| g.lookup_ligature(glyphs))
}
pub fn lookup_kerning(&self, left: u16, right: u16) -> i16 {
if let Some(gpos) = &self.gpos {
let v = gpos.lookup_kerning(left, right, self.gdef.as_ref());
if v != 0 {
return v;
}
}
if let Some(kern) = &self.kern {
return kern.lookup(left, right);
}
0
}
pub fn lookup_mark_to_base(&self, base: u16, mark: u16) -> Option<(i16, i16)> {
self.gpos.as_ref()?.lookup_mark_to_base(base, mark)
}
pub fn lookup_mark_to_mark(&self, mark1: u16, mark2: u16) -> Option<(i16, i16)> {
self.gpos.as_ref()?.lookup_mark_to_mark(mark1, mark2)
}
pub fn is_mark_glyph(&self, glyph_id: u16) -> bool {
self.gdef
.as_ref()
.map(|g| g.is_mark(glyph_id))
.unwrap_or(false)
}
pub fn has_color_bitmaps(&self) -> bool {
self.cblc.is_some() && self.cbdt.is_some()
}
pub fn color_strike_sizes(&self) -> Vec<(u8, u8)> {
self.cblc
.as_ref()
.map(|c| c.ppem_sizes().collect())
.unwrap_or_default()
}
pub fn glyph_color_bitmap(&self, glyph_id: u16, target_ppem: u8) -> Option<ColorBitmap<'a>> {
let cblc = self.cblc.as_ref()?;
let cbdt = self.cbdt.as_ref()?;
let entry = cblc.lookup_glyph(glyph_id, target_ppem)?;
cbdt.lookup(&entry).ok().flatten()
}
}