#![doc(html_root_url = "https://docs.rs/ttf-parser/0.2.0")]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
mod cff;
mod cmap;
mod glyf;
mod head;
mod hhea;
mod hmtx;
mod kern;
mod loca;
mod name;
mod os2;
mod parser;
mod post;
mod vhea;
mod vmtx;
use parser::{Stream, FromData, SafeStream, TrySlice};
pub use cff::CFFError;
pub use name::*;
pub use os2::*;
#[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Debug)]
pub struct GlyphId(pub u16);
impl FromData for GlyphId {
#[inline]
fn parse(s: &mut SafeStream) -> Self {
GlyphId(s.read())
}
}
#[derive(Clone, Copy, Debug)]
pub enum Error {
NotATrueType,
FontIndexOutOfBounds,
TableMissing(TableName),
InvalidTableSize(TableName),
NoGlyph,
NoOutline,
InvalidGlyphClass(u16),
NoHorizontalMetrics,
NoVerticalMetrics,
NoKerning,
UnsupportedTableVersion(TableName, u16),
CFFError(CFFError),
#[allow(missing_docs)]
SliceOutOfBounds {
start: u32,
end: u32,
data_len: u32,
},
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Error::NotATrueType => {
write!(f, "not a TrueType font")
}
Error::FontIndexOutOfBounds => {
write!(f, "font index is out of bounds")
}
Error::TableMissing(name) => {
write!(f, "font doesn't have a {:?} table", name)
}
Error::InvalidTableSize(name) => {
write!(f, "table {:?} has an invalid size", name)
}
Error::SliceOutOfBounds { start, end, data_len } => {
write!(f, "an attempt to slice {}..{} on 0..{}", start, end, data_len)
}
Error::NoGlyph => {
write!(f, "font doesn't have such glyph ID")
}
Error::NoOutline => {
write!(f, "glyph has no outline")
}
Error::InvalidGlyphClass(n) => {
write!(f, "{} is not a valid glyph class", n)
}
Error::NoHorizontalMetrics => {
write!(f, "glyph has no horizontal metrics")
}
Error::NoVerticalMetrics => {
write!(f, "glyph has no vertical metrics")
}
Error::NoKerning => {
write!(f, "glyph has no kerning")
}
Error::UnsupportedTableVersion(name, version) => {
write!(f, "table {:?} with version {} is not supported", name, version)
}
Error::CFFError(e) => {
write!(f, "{:?} table parsing failed cause {}", TableName::CompactFontFormat, e)
}
}
}
}
impl From<CFFError> for Error {
#[inline]
fn from(e: CFFError) -> Self {
Error::CFFError(e)
}
}
impl std::error::Error for Error {}
pub(crate) type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy)]
struct Tag {
tag: [u8; Tag::LENGTH],
}
impl Tag {
const LENGTH: usize = 4;
}
impl std::fmt::Debug for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let d = self.tag;
write!(f, "Tag({}{}{}{})", d[0] as char, d[1] as char, d[2] as char, d[3] as char)
}
}
impl FromData for Tag {
#[inline]
fn parse(s: &mut SafeStream) -> Self {
Tag { tag: [s.read(), s.read(), s.read(), s.read()] }
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct LineMetrics {
pub position: i16,
pub thickness: i16,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct HorizontalMetrics {
pub advance: u16,
pub left_side_bearing: i16,
}
impl FromData for HorizontalMetrics {
#[inline]
fn parse(s: &mut SafeStream) -> Self {
HorizontalMetrics {
advance: s.read(),
left_side_bearing: s.read(),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct VerticalMetrics {
pub advance: u16,
pub top_side_bearing: i16,
}
impl FromData for VerticalMetrics {
#[inline]
fn parse(s: &mut SafeStream) -> Self {
VerticalMetrics {
advance: s.read(),
top_side_bearing: s.read(),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct Rect {
pub x_min: i16,
pub y_min: i16,
pub x_max: i16,
pub y_max: i16,
}
impl Rect {
#[inline]
pub(crate) fn zero() -> Self {
Rect {
x_min: 0,
y_min: 0,
x_max: 0,
y_max: 0,
}
}
}
impl FromData for Rect {
#[inline]
fn parse(s: &mut SafeStream) -> Self {
Rect {
x_min: s.read(),
y_min: s.read(),
x_max: s.read(),
y_max: s.read(),
}
}
}
pub trait OutlineBuilder {
fn move_to(&mut self, x: f32, y: f32);
fn line_to(&mut self, x: f32, y: f32);
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32);
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32);
fn close(&mut self);
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[allow(missing_docs)]
pub enum TableName {
CharacterToGlyphIndexMapping,
CompactFontFormat,
GlyphData,
Header,
HorizontalHeader,
HorizontalMetrics,
IndexToLocation,
Kerning,
MaximumProfile,
Naming,
PostScript,
VerticalHeader,
VerticalMetrics,
WindowsMetrics,
}
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct Font<'a> {
head: &'a [u8],
hhea: &'a [u8],
cff_: Option<&'a [u8]>,
cmap: Option<&'a [u8]>,
glyf: Option<&'a [u8]>,
hmtx: Option<&'a [u8]>,
kern: Option<&'a [u8]>,
loca: Option<&'a [u8]>,
name: Option<&'a [u8]>,
os_2: Option<&'a [u8]>,
post: Option<&'a [u8]>,
vhea: Option<&'a [u8]>,
vmtx: Option<&'a [u8]>,
number_of_glyphs: GlyphId,
cff_metadata: cff::Metadata,
}
impl<'a> Font<'a> {
pub fn from_data(data: &'a [u8], index: u32) -> Result<Self> {
let table_data = if let Some(n) = fonts_in_collection(data) {
if index < n {
const OFFSETS_TABLE_OFFSET: usize = 12;
const OFFSET_32_SIZE: usize = 4;
let offset = OFFSETS_TABLE_OFFSET + OFFSET_32_SIZE * index as usize;
let font_offset: u32 = Stream::read_at(data, offset)?;
data.try_slice(font_offset as usize .. data.len())?
} else {
return Err(Error::FontIndexOutOfBounds);
}
} else {
data
};
const OFFSET_TABLE_SIZE: usize = 12;
if data.len() < OFFSET_TABLE_SIZE {
return Err(Error::NotATrueType);
}
const SFNT_VERSION_TRUE_TYPE: u32 = 0x00010000;
const SFNT_VERSION_OPEN_TYPE: u32 = 0x4F54544F;
let mut s = Stream::new(table_data);
let sfnt_version: u32 = s.read()?;
if let SFNT_VERSION_TRUE_TYPE | SFNT_VERSION_OPEN_TYPE = sfnt_version {} else {
return Err(Error::NotATrueType);
}
let num_tables: u16 = s.read()?;
s.skip_len(6u32);
const OFFSET_RECORD_SIZE: u32 = 16;
let mut s = SafeStream::new(s.read_bytes(num_tables as u32 * OFFSET_RECORD_SIZE)?);
let mut font = Font {
head: &[],
hhea: &[],
cff_: None,
cmap: None,
glyf: None,
hmtx: None,
kern: None,
loca: None,
name: None,
os_2: None,
post: None,
vhea: None,
vmtx: None,
number_of_glyphs: GlyphId(0),
cff_metadata: cff::Metadata::default(),
};
for _ in 0..num_tables {
let tag: Tag = s.read();
s.skip::<u32>(); let offset = s.read::<u32>() as usize;
let length = s.read::<u32>() as usize;
let range = offset..(offset + length);
match &tag.tag {
b"head" => {
const HEAD_TABLE_SIZE: usize = 54;
if length < HEAD_TABLE_SIZE {
return Err(Error::InvalidTableSize(TableName::Header));
}
font.head = data.try_slice(range)?;
}
b"hhea" => {
const HHEA_TABLE_SIZE: usize = 36;
if length < HHEA_TABLE_SIZE {
return Err(Error::InvalidTableSize(TableName::HorizontalHeader));
}
font.hhea = data.try_slice(range)?;
}
b"maxp" => {
const MAXP_TABLE_MIN_SIZE: usize = 6;
const NUM_GLYPHS_OFFSET: usize = 4;
if length < MAXP_TABLE_MIN_SIZE {
return Err(Error::InvalidTableSize(TableName::MaximumProfile));
}
let data = data.try_slice(range)?;
font.number_of_glyphs = SafeStream::read_at(data, NUM_GLYPHS_OFFSET);
}
b"OS/2" => {
const OS_2_TABLE_MIN_SIZE: usize = 78;
if length < OS_2_TABLE_MIN_SIZE {
return Err(Error::InvalidTableSize(TableName::WindowsMetrics));
}
font.os_2 = data.get(range);
}
b"post" => {
const POST_TABLE_MIN_SIZE: usize = 16;
if length < POST_TABLE_MIN_SIZE {
return Err(Error::InvalidTableSize(TableName::PostScript));
}
font.post = data.get(range);
}
b"vhea" => {
const VHEA_TABLE_MIN_SIZE: usize = 36;
if length < VHEA_TABLE_MIN_SIZE {
return Err(Error::InvalidTableSize(TableName::VerticalHeader));
}
font.vhea = data.get(range);
}
b"CFF " => {
if let Some(data) = data.get(range) {
if let Ok(metadata) = cff::parse_metadata(data) {
font.cff_ = Some(data);
font.cff_metadata = metadata;
}
}
}
b"cmap" => font.cmap = data.get(range),
b"glyf" => font.glyf = data.get(range),
b"hmtx" => font.hmtx = data.get(range),
b"kern" => font.kern = data.get(range),
b"loca" => font.loca = data.get(range),
b"name" => font.name = data.get(range),
b"vmtx" => font.vmtx = data.get(range),
_ => {}
}
}
if font.head.is_empty() {
return Err(Error::TableMissing(TableName::Header));
}
if font.hhea.is_empty() {
return Err(Error::TableMissing(TableName::HorizontalHeader));
}
Ok(font)
}
#[inline]
pub fn has_table(&self, name: TableName) -> bool {
match name {
TableName::Header => true,
TableName::HorizontalHeader => true,
TableName::MaximumProfile => true,
TableName::CharacterToGlyphIndexMapping => self.cmap.is_some(),
TableName::CompactFontFormat => self.cff_.is_some(),
TableName::GlyphData => self.glyf.is_some(),
TableName::HorizontalMetrics => self.hmtx.is_some(),
TableName::IndexToLocation => self.loca.is_some(),
TableName::Kerning => self.kern.is_some(),
TableName::Naming => self.name.is_some(),
TableName::PostScript => self.post.is_some(),
TableName::VerticalHeader => self.vhea.is_some(),
TableName::VerticalMetrics => self.vmtx.is_some(),
TableName::WindowsMetrics => self.os_2.is_some(),
}
}
#[inline]
pub fn number_of_glyphs(&self) -> u16 {
self.number_of_glyphs.0
}
#[inline]
pub(crate) fn check_glyph_id(&self, glyph_id: GlyphId) -> Result<()> {
if glyph_id < self.number_of_glyphs {
Ok(())
} else {
Err(Error::NoGlyph)
}
}
#[inline]
pub fn outline_glyph(
&self,
glyph_id: GlyphId,
builder: &mut impl OutlineBuilder,
) -> Result<Rect> {
if self.glyf.is_some() {
self.glyf_glyph_outline(glyph_id, builder)
} else if self.cff_.is_some() {
self.cff_glyph_outline(glyph_id, builder)
} else {
Err(Error::NoGlyph)
}
}
}
#[inline]
fn is_collection(data: &[u8]) -> bool {
data.get(0..Tag::LENGTH) == Some(b"ttcf")
}
#[inline]
pub fn fonts_in_collection(data: &[u8]) -> Option<u32> {
if !is_collection(data) {
return None;
}
const NUM_FONTS_OFFSET: usize = 8;
Stream::read_at(data, NUM_FONTS_OFFSET).ok()
}