mttf 0.1.4

A library for working with TrueType fonts. Most parts are zero-allocation.
Documentation

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<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()
	}
}

// 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<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()
	}
	
	// TrueType Outline Tables

	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))
	}
	
	// Advanced Typographic Tables
	
	pub fn glyph_substitution_table(&self, check: bool) -> Result<GlyphSubstitutionTable<'a>, 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<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())
			
			// 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<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))
	}
}