#![doc(html_root_url = "https://docs.rs/ttf-parser/0.1.0")]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
macro_rules! impl_bit_ops {
($name:ty) => {
impl std::ops::BitOr for $name {
type Output = Self;
#[inline]
fn bitor(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
impl std::ops::BitAnd for $name {
type Output = Self;
#[inline]
fn bitand(self, other: Self) -> Self {
Self(self.0 & other.0)
}
}
}
}
pub mod cmap;
pub mod gdef;
pub mod glyf;
pub mod hmtx;
pub mod loca;
pub mod name;
pub mod os2;
pub mod post;
mod head;
mod hhea;
mod stream;
type Range32 = std::ops::Range<u32>;
use stream::Stream;
#[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,
}
#[derive(Clone, Copy, Debug)]
pub enum Error {
NotATrueType,
IndexOutOfBounds,
TableMissing(Tag),
InvalidTableChecksum(Tag),
}
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::IndexOutOfBounds => {
write!(f, "font index is out of bounds")
}
Error::TableMissing(tag) => {
write!(f, "font doesn't have a {} table", tag)
}
Error::InvalidTableChecksum(tag) => {
write!(f, "table {} has an invalid checksum", tag)
}
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Copy, PartialEq)]
pub struct Tag {
tag: [u8; 4],
}
impl Tag {
pub fn new(c1: u8, c2: u8, c3: u8, c4: u8) -> Self {
Tag { tag: [c1, c2, c3, c4] }
}
pub fn from_slice(data: &[u8]) -> Self {
assert_eq!(data.len(), 4);
Tag { tag: [data[0], data[1], data[2], data[3]] }
}
fn to_ascii(&self) -> [char; 4] {
let mut tag2 = [' '; 4];
for i in 0..4 {
if self.tag[i].is_ascii() {
tag2[i] = self.tag[i] as char;
}
}
tag2
}
fn zero() -> Self {
Tag { tag: [0; 4] }
}
}
impl std::ops::Deref for Tag {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.tag
}
}
impl std::fmt::Debug for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let tag = self.to_ascii();
write!(f, "Tag({}{}{}{})", tag[0], tag[1], tag[2], tag[3])
}
}
impl std::fmt::Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let tag = self.to_ascii();
write!(f, "{}{}{}{}", tag[0], tag[1], tag[2], tag[3])
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct LineMetrics {
pub position: i16,
pub thickness: i16,
}
#[derive(Clone, Copy, Debug)]
struct TableInfo {
tag: Tag,
checksum: u32,
offset: u32,
length: u32,
}
impl TableInfo {
fn range(&self) -> std::ops::Range<usize> {
self.offset as usize .. (self.offset as usize + self.length as usize)
}
}
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct Font<'a> {
data: &'a [u8],
cmap: TableInfo,
gdef: Option<TableInfo>,
glyf: TableInfo,
head: TableInfo,
hhea: TableInfo,
hmtx: TableInfo,
loca: TableInfo,
name: Option<TableInfo>,
os_2: Option<TableInfo>,
post: TableInfo,
number_of_glyphs: u16,
}
impl<'a> Font<'a> {
pub fn from_data(data: &'a [u8], index: u32) -> Result<Self, Error> {
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[font_offset as usize ..]
} else {
return Err(Error::IndexOutOfBounds);
}
} else {
data
};
const SFNT_VERSION_TRUE_TYPE: u32 = 0x00010000;
let mut s = stream::Stream::new(table_data);
let sfnt_version = s.read_u32();
if sfnt_version != SFNT_VERSION_TRUE_TYPE {
return Err(Error::NotATrueType);
}
let num_tables = s.read_u16();
s.skip_u16();
s.skip_u16();
s.skip_u16();
let maxp = Self::find_table(s.tail(), num_tables, b"maxp")?;
let number_of_glyphs = Self::parse_number_of_glyphs(&data[maxp.range()]);
Ok(Font {
data,
cmap: Self::find_table(s.tail(), num_tables, b"cmap")?,
gdef: Self::find_table(s.tail(), num_tables, b"GDEF").ok(),
glyf: Self::find_table(s.tail(), num_tables, b"glyf")?,
head: Self::find_table(s.tail(), num_tables, b"head")?,
hhea: Self::find_table(s.tail(), num_tables, b"hhea")?,
hmtx: Self::find_table(s.tail(), num_tables, b"hmtx")?,
loca: Self::find_table(s.tail(), num_tables, b"loca")?,
name: Self::find_table(s.tail(), num_tables, b"name").ok(),
os_2: Self::find_table(s.tail(), num_tables, b"OS/2").ok(),
post: Self::find_table(s.tail(), num_tables, b"post")?,
number_of_glyphs,
})
}
fn find_table(data: &[u8], num_tables: u16, name: &[u8]) -> Result<TableInfo, Error> {
let mut s = Stream::new(data);
for _ in 0..num_tables {
let tag = s.read_tag();
let table = TableInfo {
tag,
checksum: s.read_u32(),
offset: s.read_u32(),
length: s.read_u32(),
};
if &*tag == name {
return Ok(table);
}
}
Err(Error::TableMissing(Tag::from_slice(name)))
}
#[inline]
pub fn number_of_glyphs(&self) -> u16 {
self.number_of_glyphs
}
fn parse_number_of_glyphs(data: &[u8]) -> u16 {
const NUM_GLYPHS_OFFSET: usize = 4;
Stream::read_at(data, NUM_GLYPHS_OFFSET)
}
pub fn is_valid(&self) -> Result<(), Error> {
self.calc_table_checksum(&self.cmap)?;
self.calc_table_checksum(&self.glyf)?;
self.calc_table_checksum(&self.hhea)?;
self.calc_table_checksum(&self.hmtx)?;
self.calc_table_checksum(&self.loca)?;
self.calc_table_checksum(&self.post)?;
if let Some(ref gdef) = self.gdef { self.calc_table_checksum(gdef)?; }
if let Some(ref name) = self.name { self.calc_table_checksum(name)?; }
if let Some(ref os_2) = self.os_2 { self.calc_table_checksum(os_2)?; }
Ok(())
}
fn calc_table_checksum(&self, table: &TableInfo) -> Result<(), Error> {
let sum = calc_checksum(&self.data[table.offset as usize..], table.length);
if sum == table.checksum {
Ok(())
} else {
Err(Error::InvalidTableChecksum(table.tag))
}
}
pub fn os2_table(&self) -> Option<os2::Table> {
Some(os2::Table { data: &self.data[self.os_2?.range()] })
}
pub fn name_table(&self) -> Option<name::Table> {
Some(name::Table { data: &self.data[self.name?.range()] })
}
pub fn gdef_table(&self) -> Option<gdef::Table> {
Some(gdef::Table {
data: &self.data[self.name?.range()],
number_of_glyphs: self.number_of_glyphs,
})
}
}
fn calc_checksum(data: &[u8], length: u32) -> u32 {
let length = (length + 3) & !3;
let mut sum: u32 = 0;
for n in Stream::new(data).read_array::<u32>(length as usize / 4) {
sum = sum.wrapping_add(n);
}
sum
}
fn is_collection(data: &[u8]) -> bool {
data.len() >= 4 && &data[0..4] == b"ttcf"
}
pub fn fonts_in_collection(data: &[u8]) -> Option<u32> {
const NUM_FONTS_OFFSET: usize = 8;
if !is_collection(data) {
return None;
}
Some(Stream::read_at(data, NUM_FONTS_OFFSET))
}