use std::ops::Range;
use std::fmt::{ Debug, Formatter, Result as FmtResult };
use thiserror::Error;
use bytemuck::{ Zeroable, Pod };
pub mod core;
pub mod common;
pub mod maxp;
pub mod head;
pub mod hhea;
pub mod hmtx;
pub mod name;
pub mod cmap;
pub mod os_2;
pub mod post;
pub use maxp::MaximumProfile;
pub use head::HeaderTable;
pub use hhea::HorizontalHeaderTable;
pub use hmtx::HorizontalMetricsTable;
pub use name::NamingTable;
pub use cmap::CharacterToGlyphMap;
pub use os_2::Os2MetricsTable;
pub use post::PostScriptTable;
pub mod loca;
pub mod glyf;
pub use loca::IndexToLocationTable;
pub use glyf::GlyphDataTable;
pub mod gsub;
pub use gsub::GlyphSubstitutionTable;
pub mod vhea;
pub mod vmtx;
pub use vhea::VertialHeaderTable;
pub use vmtx::VerticalMetricsTable;
use core::*;
#[derive(Error, Debug)]
pub enum ReadError {
#[error("unexpected end of file")]
UnexpectedEof,
#[error("unsupported font file version: {1} (0x{0:08X})")]
UnsupportedFileVersion(u32, &'static str),
#[error("table not found")]
TableNotFound,
#[error("checksum mismatch: 0x{calculated_checksum:08X} != 0x{stored_checksum:08X}")]
TableChecksumMismatch {
calculated_checksum: u32,
stored_checksum: u32,
},
#[error("unsupported version: {0}")]
UnsupportedTableVersionSingle(u16),
#[error("unsupported version: {0}.{1}")]
UnsupportedTableVersionPair(u16, u16),
#[error("unsupported version: {0:?}")]
UnsupportedTableVersion16Dot16(Version16Dot16),
#[error("unknown coverage table format: {0}")]
UnknownCoverageTableFormat(u16),
#[error("unsupported magic number: 0x{0:08X} (should be 0x5F0F3CF5)")]
UnsupportedMagicNumber(u32),
#[error("unsupported font direction hint: {0}")]
UnsupportedFontDirectionHint(i16),
#[error("unknown glyph data format: {0}")]
UnknownGlyphDataFormat(i16),
#[error("unsupported metrics data format: {0} (should be 0)")]
UnsupportedMetricsDataFormat(i16),
#[error("unknown cmap format: {0}")]
UnknownCmapFormat(u16),
#[error("unsupported cmap format: {0:?}")]
UnsupportedCmapFormat(cmap::Format),
#[error("unsupported index to location format: {0:?}")]
UnsupportedIndexToLocationFormat(head::IndexToLocationFormat),
#[error("unsupported gsub lookup type: {0:?}")]
UnsupportedGsubLookupType(gsub::LookupType),
#[error("unknown single substitution format: {0:?}")]
UnknownSingleSubstitutionFormat(u16),
}
pub fn checksum(data: &[u8], is_head: bool) -> u32 {
let split = data.len() & !3;
let mut checksum: u32 = 0;
for i in (0..split).step_by(4) {
let segment = &data[i..i + 4];
let segment = [segment[0], segment[1], segment[2], segment[3]];
checksum = checksum.wrapping_add(u32::from_be_bytes(segment));
}
let remainder = &data[split..];
let remainder = match remainder.len() {
0 => [0, 0, 0, 0],
1 => [remainder[0], 0, 0, 0],
2 => [remainder[0], remainder[1], 0, 0],
3 => [remainder[0], remainder[1], remainder[2], 0],
_ => unreachable!(),
};
checksum = checksum.wrapping_add(u32::from_be_bytes(remainder));
if is_head && data.len() >= 12 {
let segment = &data[8..12];
let segment = [segment[0], segment[1], segment[2], segment[3]];
checksum = checksum.wrapping_sub(u32::from_be_bytes(segment));
}
checksum
}
#[derive(Clone, Copy, Zeroable, Pod)]
#[repr(transparent)]
pub struct TableRecord([u8; 16]);
impl<'a> RandomAccess<'a> for &'a TableRecord {
fn bytes(&self) -> &'a [u8] { &self.0 }
}
impl TableRecord {
pub fn tag(&self) -> &Tag { self.item(0) }
pub fn checksum(&self) -> u32 { self.uint32(4) }
pub fn data(&self) -> Range<u32> {
let start = self.uint32(8);
let stop = start + self.uint32(12);
start..stop
}
}
impl Debug for TableRecord {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("TableRecord")
.field("tag", &self.tag())
.field("checksum", &self.checksum())
.field("data", &self.data())
.finish()
}
}
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct FontFile<'a>(&'a [u8]);
impl<'a> RandomAccess<'a> for FontFile<'a> {
fn bytes(&self) -> &'a [u8] { self.0 }
}
impl<'a> FontFile<'a> {
pub fn version(&self) -> u32 { self.uint32(0) }
pub fn table_count(&self) -> u16 { self.uint16(4) }
pub fn search_range(&self) -> u16 { self.uint16(6) }
pub fn entry_selector(&self) -> u16 { self.uint16(8) }
pub fn range_shift(&self) -> u16 { self.uint16(10) }
pub fn table_records(&self) -> &'a [TableRecord] { self.array(12, self.table_count() as usize) }
pub fn table_data(&self, tag: &[u8; 4], check: bool) -> Result<&'a [u8], ReadError> {
let tag = Tag::new(tag);
for record in self.table_records() {
if *record.tag() == tag {
let range = record.data();
let range = (range.start as usize)..(range.end as usize);
let data = self.bytes();
if range.end <= data.len() {
let data = &data[range];
if check {
let calculated_checksum = checksum(data, tag == Tag::new(b"head"));
let stored_checksum = record.checksum();
if calculated_checksum != stored_checksum {
return Err(ReadError::TableChecksumMismatch { calculated_checksum, stored_checksum });
}
}
return Ok(data);
}
}
}
Err(ReadError::TableNotFound)
}
pub fn maximum_profile(&self, check: bool) -> Result<MaximumProfile<'a>, ReadError> {
let data = self.table_data(b"maxp", check)?;
data.try_into()
}
pub fn header_table(&self, check: bool) -> Result<&'a HeaderTable, ReadError> {
let data = self.table_data(b"head", check)?;
data.try_into()
}
pub fn horizontal_header_table(&self, check: bool) -> Result<&'a HorizontalHeaderTable, ReadError> {
let data = self.table_data(b"hhea", check)?;
data.try_into()
}
pub fn horizontal_metrics_table(&self, check: bool) -> Result<HorizontalMetricsTable<'a>, ReadError> {
let data = self.table_data(b"hmtx", check)?;
let maxp = self.maximum_profile(check)?;
let hhea = self.horizontal_header_table(check)?;
HorizontalMetricsTable::try_from(data, hhea.number_of_hmetrics(), maxp.num_glyphs())
}
pub fn naming_table(&self, check: bool) -> Result<NamingTable<'a>, ReadError> {
let data = self.table_data(b"name", check)?;
data.try_into()
}
pub fn character_to_glyph_map(&self, check: bool) -> Result<CharacterToGlyphMap<'a>, ReadError> {
let data = self.table_data(b"cmap", check)?;
data.try_into()
}
pub fn os_2_metrics_table(&self, check: bool) -> Result<Os2MetricsTable<'a>, ReadError> {
let data = self.table_data(b"OS/2", check)?;
data.try_into()
}
pub fn post_script_table(&self, check: bool) -> Result<PostScriptTable<'a>, ReadError> {
let data = self.table_data(b"post", check)?;
data.try_into()
}
pub fn index_to_location_table(&self, check: bool) -> Result<IndexToLocationTable<'a>, ReadError> {
let data = self.table_data(b"loca", check)?;
let maxp = self.maximum_profile(check)?;
let head = self.header_table(check)?;
IndexToLocationTable::try_from(data, head.index_to_location_format(), maxp.num_glyphs())
}
pub fn glyph_data_table(&self, check: bool) -> Result<GlyphDataTable<'a>, ReadError> {
let data = self.table_data(b"glyf", check)?;
let loca = self.index_to_location_table(check)?;
Ok(GlyphDataTable::from(data, loca))
}
pub fn glyph_substitution_table(&self, check: bool) -> Result<GlyphSubstitutionTable<'a>, ReadError> {
let data = self.table_data(b"GSUB", check)?;
data.try_into()
}
pub fn vertical_header_table(&self, check: bool) -> Result<&'a VertialHeaderTable, ReadError> {
let data = self.table_data(b"vhea", check)?;
data.try_into()
}
pub fn vertical_metrics_table(&self, check: bool) -> Result<VerticalMetricsTable<'a>, ReadError> {
let data = self.table_data(b"vmtx", check)?;
let maxp = self.maximum_profile(check)?;
let vhea = self.vertical_header_table(check)?;
VerticalMetricsTable::try_from(data, vhea.number_of_vmetrics(), maxp.num_glyphs())
}
}
impl<'a> Debug for FontFile<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("FontFile")
.field("version", &self.version())
.field("table_count", &self.table_count())
.field("search_range", &self.search_range())
.field("entry_selector", &self.entry_selector())
.field("range_shift", &self.range_shift())
.field("table_records", &self.table_records())
.field("maximum_profile", &self.maximum_profile(true))
.field("header_table", &self.header_table(true))
.field("horizontal_header_table", &self.horizontal_header_table(true))
.field("horizontal_metrics_table", &self.horizontal_metrics_table(true))
.field("naming_table", &self.naming_table(true))
.field("character_to_glyph_map", &self.character_to_glyph_map(true))
.field("os_2_metrics_table", &self.os_2_metrics_table(true))
.field("post_script_table", &self.post_script_table(true))
.field("index_to_location_table", &self.index_to_location_table(true))
.field("glyph_data_table", &self.glyph_data_table(true))
.field("glyph_substitution_table", &self.glyph_substitution_table(true))
.field("vertical_header_table", &self.vertical_header_table(true))
.field("vertical_metrics_table", &self.vertical_metrics_table(true))
.finish()
}
}
impl<'a> TryFrom<&'a [u8]> for FontFile<'a> {
type Error = ReadError;
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
if value.len() < 12 {
return Err(ReadError::UnexpectedEof);
}
let version = value.uint32(0);
match version {
0x4F54544F => return Err(ReadError::UnsupportedFileVersion(version, "Postscript outlines are not supported")),
0x74746366 => return Err(ReadError::UnsupportedFileVersion(version, "TrueType Fonts Collections not supported")),
0x00010000 => (),
0x74727565 => (),
x => return Err(ReadError::UnsupportedFileVersion(x, "Not a valid TrueType version")),
};
let table_count = value.uint16(4);
if value.len() < 12 + table_count as usize * 16 {
return Err(ReadError::UnexpectedEof);
}
Ok(Self(value))
}
}