#![deny(missing_debug_implementations)]
#![warn(rust_2018_idioms)]
pub mod cff;
pub mod outline;
pub mod parser;
pub mod tables;
pub use cff::{PrivateHints, RegistryOrdering, TopMetadata};
pub use outline::{BBox, CubicContour, CubicOutline, CubicSegment, Point};
use crate::cff::Cff;
use crate::parser::TableDirectory;
use crate::tables::{
cmap::CmapTable, head::HeadTable, hhea::HheaTable, hmtx::HmtxTable, maxp::MaxpTable,
name::NameTable, post::PostTable,
};
pub use crate::tables::post::PostFormat;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
UnexpectedEof,
BadMagic,
BadHeader,
BadOffset,
MissingTable(&'static str),
MissingCff,
Cff2NotImplemented,
GlyphOutOfRange(u16),
UnsupportedCmapFormat(u16),
Cff(&'static str),
BadStructure(&'static str),
CharstringStackOverflow,
CharstringStackUnderflow,
CharstringBadSubrIndex(i32),
CharstringNoLocalSubrs,
CharstringTooDeep,
CharstringTooLong,
CharstringUnsupportedOp(u16),
#[doc(hidden)]
CharstringEnd,
CharstringSeacBadComponent(u8),
CharstringSeacNested,
CharstringTransientIndex(i32),
}
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::BadOffset => f.write_str("table offset out of range"),
Self::MissingTable(t) => write!(f, "required table missing: {t}"),
Self::MissingCff => f.write_str("font has no CFF/CFF2 table"),
Self::Cff2NotImplemented => f.write_str("CFF2 (variable) not implemented in round 1"),
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::Cff(s) => write!(f, "CFF: {s}"),
Self::BadStructure(s) => write!(f, "malformed structure: {s}"),
Self::CharstringStackOverflow => {
f.write_str("Type 2 charstring: operand stack overflow")
}
Self::CharstringStackUnderflow => {
f.write_str("Type 2 charstring: operand stack underflow")
}
Self::CharstringBadSubrIndex(i) => {
write!(f, "Type 2 charstring: subr index {i} out of range")
}
Self::CharstringNoLocalSubrs => {
f.write_str("Type 2 charstring: callsubr but no local subrs INDEX")
}
Self::CharstringTooDeep => {
f.write_str("Type 2 charstring: subroutine recursion too deep")
}
Self::CharstringTooLong => f.write_str("Type 2 charstring: too many bytes processed"),
Self::CharstringUnsupportedOp(op) => {
write!(f, "Type 2 charstring: unsupported operator {op:#06x}")
}
Self::CharstringEnd => f.write_str("Type 2 charstring: end (internal)"),
Self::CharstringSeacBadComponent(code) => write!(
f,
"Type 2 charstring: seac component (Standard Encoding code {code}) \
has no matching glyph in this font's charset"
),
Self::CharstringSeacNested => {
f.write_str("Type 2 charstring: nested seac is forbidden (TN5177 Appendix C)")
}
Self::CharstringTransientIndex(i) => write!(
f,
"Type 2 charstring: transient-array index {i} out of range (0..32)"
),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug)]
pub struct Font<'a> {
bytes: &'a [u8],
dir: TableDirectory,
head: HeadTable,
hhea: HheaTable,
maxp: MaxpTable,
cmap: CmapTable<'a>,
name: NameTable<'a>,
hmtx: HmtxTable<'a>,
post: Option<PostTable<'a>>,
cff: Cff<'a>,
}
impl<'a> Font<'a> {
pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
let dir = TableDirectory::parse(bytes)?;
let cff_tag = dir.cff_tag.ok_or(Error::MissingCff)?;
if cff_tag == *b"CFF2" {
return Err(Error::Cff2NotImplemented);
}
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 post = match dir.find(b"post", bytes) {
Some(slice) => Some(PostTable::parse(slice)?),
None => None,
};
let cff_bytes = dir.required(b"CFF ", bytes)?;
let cff = Cff::parse(cff_bytes)?;
Ok(Self {
bytes,
dir,
head,
hhea,
maxp,
cmap,
name,
hmtx,
post,
cff,
})
}
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 glyph_count(&self) -> u16 {
self.maxp.num_glyphs
}
pub fn ascent(&self) -> i16 {
self.hhea.ascent
}
pub fn descent(&self) -> i16 {
self.hhea.descent
}
pub fn line_gap(&self) -> i16 {
self.hhea.line_gap
}
pub fn ps_name(&self) -> Option<&str> {
std::str::from_utf8(self.cff.ps_name()).ok()
}
pub fn glyph_index(&self, codepoint: char) -> Option<u16> {
self.cmap.lookup(codepoint as u32)
}
pub fn glyph_outline(&self, glyph_id: u16) -> Result<CubicOutline, Error> {
if glyph_id >= self.maxp.num_glyphs {
return Err(Error::GlyphOutOfRange(glyph_id));
}
self.cff.glyph_outline(glyph_id)
}
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_name(&self, glyph_id: u16) -> Option<&str> {
let sid = self.cff.charset().sid_of(glyph_id)?;
self.cff.strings().get(sid)
}
pub fn cff(&self) -> &Cff<'a> {
&self.cff
}
pub fn is_cid(&self) -> bool {
self.cff.is_cid()
}
pub fn cid_registry(&self) -> Option<&str> {
let ros = self.cff.registry_ordering()?;
self.cff.resolve_sid(ros.registry_sid)
}
pub fn cid_ordering(&self) -> Option<&str> {
let ros = self.cff.registry_ordering()?;
self.cff.resolve_sid(ros.ordering_sid)
}
pub fn cid_supplement(&self) -> Option<i32> {
Some(self.cff.registry_ordering()?.supplement)
}
pub fn cff_fd_count(&self) -> usize {
self.cff.fd_count()
}
pub fn font_bbox(&self) -> [f32; 4] {
self.cff.top_metadata().font_bbox
}
pub fn italic_angle(&self) -> f64 {
self.cff.top_metadata().italic_angle
}
pub fn underline_position(&self) -> f64 {
self.cff.top_metadata().underline_position
}
pub fn underline_thickness(&self) -> f64 {
self.cff.top_metadata().underline_thickness
}
pub fn is_fixed_pitch(&self) -> bool {
self.cff.top_metadata().is_fixed_pitch
}
pub fn font_matrix(&self) -> [f64; 6] {
self.cff.top_metadata().font_matrix
}
pub fn paint_type(&self) -> i32 {
self.cff.top_metadata().paint_type
}
pub fn charstring_type(&self) -> i32 {
self.cff.top_metadata().charstring_type
}
pub fn stroke_width(&self) -> f64 {
self.cff.top_metadata().stroke_width
}
pub fn weight_name(&self) -> Option<&str> {
self.cff
.top_metadata()
.weight_sid
.and_then(|sid| self.cff.resolve_sid(sid))
}
pub fn notice(&self) -> Option<&str> {
self.cff
.top_metadata()
.notice_sid
.and_then(|sid| self.cff.resolve_sid(sid))
}
pub fn copyright(&self) -> Option<&str> {
self.cff
.top_metadata()
.copyright_sid
.and_then(|sid| self.cff.resolve_sid(sid))
}
pub fn version_string(&self) -> Option<&str> {
self.cff
.top_metadata()
.version_sid
.and_then(|sid| self.cff.resolve_sid(sid))
}
pub fn postscript(&self) -> Option<&str> {
self.cff
.top_metadata()
.postscript_sid
.and_then(|sid| self.cff.resolve_sid(sid))
}
pub fn base_font_name(&self) -> Option<&str> {
self.cff
.top_metadata()
.base_font_name_sid
.and_then(|sid| self.cff.resolve_sid(sid))
}
pub fn unique_id(&self) -> Option<i32> {
self.cff.top_metadata().unique_id
}
pub fn xuid(&self) -> &[i32] {
&self.cff.top_metadata().xuid
}
pub fn synthetic_base(&self) -> Option<i32> {
self.cff.top_metadata().synthetic_base
}
pub fn base_font_blend(&self) -> &[f64] {
&self.cff.top_metadata().base_font_blend
}
pub fn private_hints(&self) -> &PrivateHints {
self.cff.private_hints()
}
pub fn glyph_private_hints(&self, glyph_id: u16) -> Option<&PrivateHints> {
if glyph_id >= self.maxp.num_glyphs {
return None;
}
self.cff.private_hints_for_glyph(glyph_id)
}
pub fn glyph_bbox(&self, glyph_id: u16) -> Result<Option<BBox>, Error> {
let outline = self.glyph_outline(glyph_id)?;
if outline.is_empty() {
Ok(None)
} else {
Ok(Some(outline.bounds))
}
}
pub fn table_tags(&self) -> impl Iterator<Item = ([u8; 4], u32)> + '_ {
self.dir.tag_list()
}
pub fn table_data(&self, tag: &[u8; 4]) -> Option<&'a [u8]> {
self.dir.find(tag, self.bytes)
}
pub fn has_table(&self, tag: &[u8; 4]) -> bool {
self.dir.find(tag, self.bytes).is_some()
}
pub fn post(&self) -> Option<&PostTable<'a>> {
self.post.as_ref()
}
pub fn post_format(&self) -> Option<PostFormat> {
self.post.as_ref().map(PostTable::format)
}
pub fn post_italic_angle(&self) -> Option<f64> {
self.post.as_ref().map(PostTable::italic_angle)
}
pub fn post_underline_position(&self) -> Option<i16> {
self.post.as_ref().map(PostTable::underline_position)
}
pub fn post_underline_thickness(&self) -> Option<i16> {
self.post.as_ref().map(PostTable::underline_thickness)
}
pub fn post_is_fixed_pitch(&self) -> Option<bool> {
self.post.as_ref().map(PostTable::is_fixed_pitch)
}
pub fn post_glyph_name(&self, glyph_id: u16) -> Option<&'a [u8]> {
let post = self.post.as_ref()?;
let idx = post.name_index(glyph_id)?;
if idx < 258 {
return None;
}
post.name_string(idx - 258)
}
}