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; // Required Tables 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; // TrueType Outline Tables pub mod loca; pub mod glyf; pub use loca::IndexToLocationTable; pub use glyf::GlyphDataTable; // Advanced Typographic Tables pub mod gsub; pub use gsub::GlyphSubstitutionTable; // Vertical Metrics Tables pub mod vhea; pub mod vmtx; pub use vhea::VertialHeaderTable; pub use vmtx::VerticalMetricsTable; // Font 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 } // Table Record #[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 { 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() } } // Font #[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> { // TODO binary search 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) } // Required Tables pub fn maximum_profile(&self, check: bool) -> Result, 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, 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, ReadError> { let data = self.table_data(b"name", check)?; data.try_into() } pub fn character_to_glyph_map(&self, check: bool) -> Result, ReadError> { let data = self.table_data(b"cmap", check)?; data.try_into() } pub fn os_2_metrics_table(&self, check: bool) -> Result, ReadError> { let data = self.table_data(b"OS/2", check)?; data.try_into() } pub fn post_script_table(&self, check: bool) -> Result, ReadError> { let data = self.table_data(b"post", check)?; data.try_into() } // TrueType Outline Tables pub fn index_to_location_table(&self, check: bool) -> Result, 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, ReadError> { let data = self.table_data(b"glyf", check)?; let loca = self.index_to_location_table(check)?; Ok(GlyphDataTable::from(data, loca)) } // Advanced Typographic Tables pub fn glyph_substitution_table(&self, check: bool) -> Result, ReadError> { let data = self.table_data(b"GSUB", check)?; data.try_into() } // Vertical Metrics Tables 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, 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()) // Required Tables .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)) // TrueType Outline Tables .field("index_to_location_table", &self.index_to_location_table(true)) .field("glyph_data_table", &self.glyph_data_table(true)) // Advanced Typographic Tables .field("glyph_substitution_table", &self.glyph_substitution_table(true)) // Vertical Metrics Tables .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 { 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)) } }