#![doc(html_root_url = "https://docs.rs/ttf-parser/0.3.0")]
#![no_std]
#![warn(missing_docs)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
#[cfg(feature = "std")]
extern crate std;
use core::fmt;
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 raw;
mod vhea;
mod vmtx;
use parser::{Stream, FromData, SafeStream, TrySlice, LazyArray};
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(data: &[u8]) -> Self {
let mut s = SafeStream::new(data);
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 core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::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)
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
pub(crate) type Result<T> = core::result::Result<T, Error>;
#[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,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct VerticalMetrics {
pub advance: u16,
pub top_side_bearing: i16,
}
#[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,
}
}
}
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)]
pub struct Font<'a> {
head: raw::head::Table<'a>,
hhea: raw::hhea::Table<'a>,
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]>,
os_2_v0: Option<raw::os_2::TableV0<'a>>,
post: Option<raw::post::Table<'a>>,
vhea: Option<raw::vhea::Table<'a>>,
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 OFFSET_32_SIZE: usize = 4;
let offset = raw::TTCHeader::SIZE + 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 sfnt_version != SFNT_VERSION_TRUE_TYPE && sfnt_version != SFNT_VERSION_OPEN_TYPE {
return Err(Error::NotATrueType);
}
let num_tables: u16 = s.read()?;
s.skip_len(6u32); let tables: LazyArray<raw::TableRecord> = s.read_array(num_tables)?;
let mut font = Font {
head: raw::head::Table::new(&[0; raw::head::Table::SIZE]),
hhea: raw::hhea::Table::new(&[0; raw::hhea::Table::SIZE]),
cff_: None,
cmap: None,
glyf: None,
hmtx: None,
kern: None,
loca: None,
name: None,
os_2: None,
os_2_v0: None,
post: None,
vhea: None,
vmtx: None,
number_of_glyphs: GlyphId(0),
cff_metadata: cff::Metadata::default(),
};
let mut has_head = false;
let mut has_hhea = false;
for table in tables {
let offset = table.offset() as usize;
let length = table.length() as usize;
let range = offset..(offset + length);
match &table.table_tag() {
b"head" => {
if length != raw::head::Table::SIZE {
return Err(Error::InvalidTableSize(TableName::Header));
}
font.head = raw::head::Table::new(data.try_slice(range)?);
has_head = true;
}
b"hhea" => {
if length != raw::hhea::Table::SIZE {
return Err(Error::InvalidTableSize(TableName::HorizontalHeader));
}
font.hhea = raw::hhea::Table::new(data.try_slice(range)?);
has_hhea = true;
}
b"maxp" => {
if length < raw::maxp::Table::SIZE {
return Err(Error::InvalidTableSize(TableName::MaximumProfile));
}
let data = &data[offset..(offset + raw::maxp::Table::SIZE)];
let table = raw::maxp::Table::new(data);
font.number_of_glyphs = GlyphId(table.num_glyphs());
}
b"OS/2" => {
if length < raw::os_2::TableV0::SIZE {
return Err(Error::InvalidTableSize(TableName::WindowsMetrics));
}
if let Some(data) = data.get(range) {
font.os_2 = Some(data);
let data = &data[0..raw::os_2::TableV0::SIZE];
font.os_2_v0 = Some(raw::os_2::TableV0::new(data));
}
}
b"post" => {
if length < raw::post::Table::SIZE {
return Err(Error::InvalidTableSize(TableName::PostScript));
}
let data = data.try_slice(offset..(offset + raw::post::Table::SIZE))?;
font.post = Some(raw::post::Table::new(data));
}
b"vhea" => {
if length != raw::vhea::Table::SIZE {
return Err(Error::InvalidTableSize(TableName::VerticalHeader));
}
font.vhea = data.get(range).map(raw::vhea::Table::new);
}
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 !has_head {
return Err(Error::TableMissing(TableName::Header));
}
if !has_hhea {
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)
}
}
}
impl fmt::Debug for Font<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Font()")
}
}
#[inline]
pub fn fonts_in_collection(data: &[u8]) -> Option<u32> {
let table = raw::TTCHeader::new(data.get(0..raw::TTCHeader::SIZE)?);
if &table.ttc_tag() != b"ttcf" {
return None;
}
Some(table.num_fonts())
}