use crate::parser::{ParseError, ParseResult};
use std::collections::{HashMap, HashSet};
const HEAD_TABLE: [u8; 4] = *b"head";
const CMAP_TABLE: [u8; 4] = *b"cmap";
const GLYF_TABLE: [u8; 4] = *b"glyf";
const LOCA_TABLE: [u8; 4] = *b"loca";
const MAXP_TABLE: [u8; 4] = *b"maxp";
const HHEA_TABLE: [u8; 4] = *b"hhea";
const HMTX_TABLE: [u8; 4] = *b"hmtx";
const NAME_TABLE: [u8; 4] = *b"name";
const CFF_TABLE: [u8; 4] = *b"CFF ";
const POST_TABLE: [u8; 4] = *b"post";
const _FPGM_TABLE: [u8; 4] = *b"fpgm";
const _CVT_TABLE: &[u8] = b"cvt ";
const _PREP_TABLE: &[u8] = b"prep";
#[rustfmt::skip]
const MAC_GLYPH_NAMES: [&str; 258] = [
".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar",
"percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma",
"hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven",
"eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B",
"C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U",
"V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum",
"underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright",
"asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis",
"aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute",
"egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", "ntilde",
"oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex",
"udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph",
"germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE",
"Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff",
"summation", "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", "ae",
"oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal",
"Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde",
"Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft",
"quoteright", "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency",
"guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase",
"quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave",
"Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve",
"Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve",
"dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash",
"Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn",
"thorn", "minus", "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf",
"onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla",
"scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat",
];
const TRUETYPE_REQUIRED_TABLES: &[&[u8]] = &[
&HEAD_TABLE,
&CMAP_TABLE,
&GLYF_TABLE,
&LOCA_TABLE,
&MAXP_TABLE,
&HHEA_TABLE,
&HMTX_TABLE,
];
const CFF_REQUIRED_TABLES: &[&[u8]] = &[
&HEAD_TABLE,
&CMAP_TABLE,
&CFF_TABLE,
&MAXP_TABLE,
&HHEA_TABLE,
&HMTX_TABLE,
];
#[derive(Debug, Clone)]
pub struct TrueTypeFont {
data: Vec<u8>,
tables: HashMap<[u8; 4], TableEntry>,
pub num_glyphs: u16,
pub units_per_em: u16,
pub loca_format: u16,
pub is_cff: bool,
}
#[derive(Debug, Clone)]
pub struct TableEntry {
pub tag: [u8; 4],
pub _checksum: u32,
pub offset: u32,
pub length: u32,
}
#[derive(Debug, Clone)]
pub struct GlyphInfo {
pub index: u16,
pub unicode: Vec<u32>,
pub advance_width: u16,
pub lsb: i16,
}
#[derive(Debug)]
pub struct CmapSubtable {
pub platform_id: u16,
pub encoding_id: u16,
pub format: u16,
pub mappings: HashMap<u32, u16>,
}
impl CmapSubtable {
pub fn select_best(subtables: &[CmapSubtable]) -> Option<&CmapSubtable> {
subtables
.iter()
.filter(|t| matches!((t.platform_id, t.encoding_id), (3, 10) | (3, 1) | (0, _)))
.max_by_key(|t| match (t.platform_id, t.encoding_id) {
(3, 10) => 3u8,
(3, 1) => 2u8,
(0, _) => 1u8,
_ => 0u8,
})
}
pub fn select_best_or_first(subtables: &[CmapSubtable]) -> Option<&CmapSubtable> {
Self::select_best(subtables).or_else(|| subtables.first())
}
}
#[cfg(test)]
impl TrueTypeFont {
pub fn empty_for_test() -> Self {
Self {
data: Vec::new(),
tables: HashMap::new(),
num_glyphs: 0,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
}
}
}
impl TrueTypeFont {
pub fn get_table(&self, tag: &[u8]) -> ParseResult<&TableEntry> {
let mut key = [0u8; 4];
for (i, &b) in tag.iter().take(4).enumerate() {
key[i] = b;
}
self.tables
.get(&key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: format!("Table {} not found", String::from_utf8_lossy(tag)),
})
}
pub fn parse(data: Vec<u8>) -> ParseResult<Self> {
if data.len() < 12 {
return Err(ParseError::SyntaxError {
position: 0,
message: "Font file too small".to_string(),
});
}
let signature = read_u32(&data, 0)?;
let is_otf = signature == 0x4F54544F; let is_ttf = signature == 0x00010000 || signature == 0x74727565; let is_ttc = signature == 0x74746366;
if !is_otf && !is_ttf && !is_ttc {
return Err(ParseError::SyntaxError {
position: 0,
message: format!("Invalid font signature: 0x{signature:08X}"),
});
}
if is_ttc {
return Err(ParseError::SyntaxError {
position: 0,
message: "TrueType Collection (TTC) files are not supported".to_string(),
});
}
let num_tables = read_u16(&data, 4)?;
let directory_end = 12 + usize::from(num_tables) * 16;
if directory_end > data.len() {
return Err(ParseError::SyntaxError {
position: 12,
message: "TrueType table directory is truncated".to_string(),
});
}
let mut tables = HashMap::new();
let mut offset = 12;
for _ in 0..num_tables {
let tag = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
];
let checksum = read_u32(&data, offset + 4)?;
let table_offset = read_u32(&data, offset + 8)?;
let length = read_u32(&data, offset + 12)?;
tables.insert(
tag,
TableEntry {
tag,
_checksum: checksum,
offset: table_offset,
length,
},
);
offset += 16;
}
let is_cff = tables.contains_key(&CFF_TABLE);
let required_tables = if is_cff {
CFF_REQUIRED_TABLES
} else {
TRUETYPE_REQUIRED_TABLES
};
for &required in required_tables {
let tag = [required[0], required[1], required[2], required[3]];
if !tables.contains_key(&tag) {
return Err(ParseError::SyntaxError {
position: 0,
message: format!(
"Missing required table for {} font: {}",
if is_cff { "CFF/OpenType" } else { "TrueType" },
std::str::from_utf8(required).unwrap_or("???")
),
});
}
}
let head_key: [u8; 4] = HEAD_TABLE;
let head_table = &tables[&head_key];
let head_offset = head_table.offset as usize;
if data.get(head_offset..).is_none_or(|table| table.len() < 54) {
return Err(ParseError::SyntaxError {
position: head_offset,
message: "Head table extends beyond file".to_string(),
});
}
let units_per_em = read_u16(&data, head_offset + 18)?;
let loca_format = read_i16(&data, head_offset + 50)? as u16;
let maxp_key: [u8; 4] = MAXP_TABLE;
let maxp_table = &tables[&maxp_key];
let maxp_offset = maxp_table.offset as usize;
if data.get(maxp_offset..).is_none_or(|table| table.len() < 6) {
return Err(ParseError::SyntaxError {
position: maxp_offset,
message: "Maxp table too small".to_string(),
});
}
let num_glyphs = read_u16(&data, maxp_offset + 4)?;
Ok(TrueTypeFont {
data,
tables,
num_glyphs,
units_per_em,
loca_format,
is_cff,
})
}
pub fn from_data(data: &[u8]) -> ParseResult<Self> {
Self::parse(data.to_vec())
}
pub fn is_cff_font(&self) -> bool {
self.is_cff
}
pub fn is_truetype_font(&self) -> bool {
!self.is_cff
}
pub fn get_font_name(&self) -> ParseResult<String> {
let name_key: [u8; 4] = NAME_TABLE;
if let Some(name_table) = self.tables.get(&name_key) {
let offset = name_table.offset as usize;
if offset + 6 > self.data.len() {
return Ok("Unknown".to_string());
}
let _format = read_u16(&self.data, offset)?;
let count = read_u16(&self.data, offset + 2)?;
let string_offset = read_u16(&self.data, offset + 4)? as usize;
let mut name_offset = offset + 6;
for _ in 0..count {
if name_offset + 12 > self.data.len() {
break;
}
let platform_id = read_u16(&self.data, name_offset)?;
let encoding_id = read_u16(&self.data, name_offset + 2)?;
let _language_id = read_u16(&self.data, name_offset + 4)?;
let name_id = read_u16(&self.data, name_offset + 6)?;
let length = read_u16(&self.data, name_offset + 8)? as usize;
let str_offset = read_u16(&self.data, name_offset + 10)? as usize;
if name_id == 6 || name_id == 4 {
let str_start = offset + string_offset + str_offset;
if str_start + length <= self.data.len() {
let name_bytes = &self.data[str_start..str_start + length];
if platform_id == 1 && encoding_id == 0 {
return Ok(String::from_utf8_lossy(name_bytes).into_owned());
} else if platform_id == 3 && (encoding_id == 1 || encoding_id == 10) {
let mut chars = Vec::new();
for i in (0..length).step_by(2) {
if i + 1 < length {
let ch =
((name_bytes[i] as u16) << 8) | (name_bytes[i + 1] as u16);
chars.push(ch);
}
}
return Ok(String::from_utf16_lossy(&chars));
}
}
}
name_offset += 12;
}
}
Ok("Unknown".to_string())
}
pub fn get_glyph_data(&self, glyph_id: u16) -> ParseResult<Vec<u8>> {
if self.is_cff {
return Err(ParseError::SyntaxError {
position: 0,
message: "Glyph data extraction not supported for CFF/OpenType fonts".to_string(),
});
}
let glyf_key: [u8; 4] = GLYF_TABLE;
let glyf_table = self
.tables
.get(&glyf_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing glyf table".to_string(),
})?;
let loca_key: [u8; 4] = LOCA_TABLE;
let loca_table = self
.tables
.get(&loca_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing loca table".to_string(),
})?;
let loca_offset = loca_table.offset as usize;
let (start_offset, end_offset) = if self.loca_format == 0 {
let idx = glyph_id as usize * 2;
if idx + 4 > loca_table.length as usize {
return Ok(Vec::new());
}
let start = read_u16(&self.data, loca_offset + idx)? as u32 * 2;
let end = read_u16(&self.data, loca_offset + idx + 2)? as u32 * 2;
(start, end)
} else {
let idx = glyph_id as usize * 4;
if idx + 8 > loca_table.length as usize {
return Ok(Vec::new());
}
let start = read_u32(&self.data, loca_offset + idx)?;
let end = read_u32(&self.data, loca_offset + idx + 4)?;
(start, end)
};
if start_offset >= end_offset {
return Ok(Vec::new());
}
let glyf_offset = glyf_table.offset as usize;
let glyph_start = glyf_offset + start_offset as usize;
let glyph_end = glyf_offset + end_offset as usize;
if glyph_end > self.data.len() {
return Err(ParseError::SyntaxError {
position: glyph_start,
message: "Glyph data out of bounds".to_string(),
});
}
Ok(self.data[glyph_start..glyph_end].to_vec())
}
pub fn get_glyph_offsets(&self) -> ParseResult<Vec<u32>> {
let loca_key: [u8; 4] = LOCA_TABLE;
let loca_table = self
.tables
.get(&loca_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing loca table".to_string(),
})?;
let loca_offset = loca_table.offset as usize;
let mut offsets = Vec::new();
if self.loca_format == 0 {
let num_offsets = loca_table.length as usize / 2;
for i in 0..num_offsets {
let offset = read_u16(&self.data, loca_offset + i * 2)? as u32 * 2;
offsets.push(offset);
}
} else {
let num_offsets = loca_table.length as usize / 4;
for i in 0..num_offsets {
let offset = read_u32(&self.data, loca_offset + i * 4)?;
offsets.push(offset);
}
}
Ok(offsets)
}
pub fn get_glyph_widths(
&self,
unicode_to_glyph: &HashMap<u32, u16>,
) -> ParseResult<HashMap<u32, u16>> {
let hmtx_key: [u8; 4] = HMTX_TABLE;
let hmtx_table = self
.tables
.get(&hmtx_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hmtx table".to_string(),
})?;
let hhea_key: [u8; 4] = HHEA_TABLE;
let hhea_table = self
.tables
.get(&hhea_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hhea table".to_string(),
})?;
let hhea_offset = hhea_table.offset as usize;
if hhea_offset + 36 > self.data.len() {
return Err(ParseError::SyntaxError {
position: hhea_offset,
message: "Hhea table too small".to_string(),
});
}
let num_h_metrics = read_u16(&self.data, hhea_offset + 34)?;
let hmtx_offset = hmtx_table.offset as usize;
let mut glyph_widths = Vec::new();
for i in 0..self.num_glyphs {
let width = if i < num_h_metrics {
let offset = hmtx_offset + (i as usize) * 4;
if offset + 2 <= self.data.len() {
read_u16(&self.data, offset)?
} else {
1000 }
} else {
let offset = hmtx_offset + ((num_h_metrics - 1) as usize) * 4;
if offset + 2 <= self.data.len() {
read_u16(&self.data, offset)?
} else {
1000
}
};
glyph_widths.push(width);
}
let mut unicode_widths = HashMap::new();
for (&unicode, &glyph_id) in unicode_to_glyph {
if (glyph_id as usize) < glyph_widths.len() {
let scaled_width = if self.units_per_em > 0 {
((glyph_widths[glyph_id as usize] as u32 * 1000) / self.units_per_em as u32)
as u16
} else {
glyph_widths[glyph_id as usize]
};
unicode_widths.insert(unicode, scaled_width);
}
}
Ok(unicode_widths)
}
pub fn parse_cmap(&self) -> ParseResult<Vec<CmapSubtable>> {
self.parse_cmap_with_codes(None)
}
pub(crate) fn parse_symbolic_cmap(&self) -> ParseResult<Vec<CmapSubtable>> {
let codes: Vec<u32> = [0, 0xF000, 0xF100, 0xF200]
.into_iter()
.flat_map(|base| (0..=255).map(move |code| base + code))
.collect();
self.parse_cmap_with_codes(Some(&codes))
}
fn parse_cmap_with_codes(&self, codes: Option<&[u32]>) -> ParseResult<Vec<CmapSubtable>> {
let entry = self.get_table(&CMAP_TABLE)?;
let start = entry.offset as usize;
let data = self
.data
.get(start..)
.and_then(|data| data.get(..entry.length as usize))
.ok_or_else(|| ParseError::SyntaxError {
position: start,
message: "Cmap table extends beyond font data".to_string(),
})?;
let count = read_u16(data, 2)?;
let mut subtables: Vec<CmapSubtable> = Vec::new();
for index in 0..usize::from(count) {
let record = 4 + 8 * index;
let Some(record) = data.get(record..record + 8) else {
break;
};
let platform_id = read_u16(record, 0)?;
let encoding_id = read_u16(record, 2)?;
if codes.is_some()
&& (!matches!((platform_id, encoding_id), (3, 0) | (1, 0))
|| subtables.iter().any(|table| {
(table.platform_id, table.encoding_id) == (platform_id, encoding_id)
}))
{
continue;
}
let offset = read_u32(record, 4)? as usize;
if let Some(data) = data.get(offset..) {
if let Ok(subtable) =
self.parse_cmap_subtable(data, platform_id, encoding_id, codes)
{
subtables.push(subtable);
}
}
}
Ok(subtables)
}
fn parse_cmap_subtable(
&self,
data: &[u8],
platform_id: u16,
encoding_id: u16,
codes: Option<&[u32]>,
) -> ParseResult<CmapSubtable> {
let format = read_u16(data, 0)?;
let length = if format == 12 {
read_u32(data, 4)? as usize
} else {
usize::from(read_u16(data, 2)?)
};
let data = data.get(..length).ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Cmap subtable extends beyond cmap table".to_string(),
})?;
let mut mappings = HashMap::new();
match format {
0 => {
for code in cmap_codes(0, 255, codes) {
let gid =
*data
.get(6 + code as usize)
.ok_or_else(|| ParseError::SyntaxError {
position: 6 + code as usize,
message: "Format 0 cmap is truncated".to_string(),
})? as u16;
if gid != 0 && gid < self.num_glyphs {
mappings.insert(code, gid);
}
}
}
4 => {
let seg_count_x2 = usize::from(read_u16(data, 6)?);
if seg_count_x2 % 2 != 0 {
return Err(ParseError::SyntaxError {
position: 6,
message: "Odd cmap segment count".to_string(),
});
}
let count = seg_count_x2 / 2;
let ends = 14;
let starts = ends + seg_count_x2 + 2;
let deltas = starts + seg_count_x2;
let offsets = deltas + seg_count_x2;
for index in 0..count {
let end = read_u16(data, ends + 2 * index)?;
if end == u16::MAX {
break;
}
let start = read_u16(data, starts + 2 * index)?;
let delta = read_i16(data, deltas + 2 * index)?;
let range_offset = usize::from(read_u16(data, offsets + 2 * index)?);
for code in cmap_codes(u32::from(start), u32::from(end), codes) {
let gid = if range_offset == 0 {
(code as u16).wrapping_add(delta as u16)
} else {
let position = offsets
+ 2 * index
+ range_offset
+ 2 * (code as usize - usize::from(start));
let gid = read_u16(data, position).unwrap_or(0);
if gid == 0 {
0
} else {
gid.wrapping_add(delta as u16)
}
};
if gid != 0 && gid < self.num_glyphs {
mappings.insert(code, gid);
}
}
}
}
6 => {
let first = u32::from(read_u16(data, 6)?);
let count = u32::from(read_u16(data, 8)?);
if count != 0 {
let last = (first + count - 1).min(u32::from(u16::MAX));
for code in cmap_codes(first, last, codes) {
let position = 10 + 2 * (code - first) as usize;
let Ok(gid) = read_u16(data, position) else {
break;
};
if gid != 0 && gid < self.num_glyphs {
mappings.insert(code, gid);
}
}
}
}
12 => {
let count = read_u32(data, 12)? as usize;
for group in data
.get(16..)
.unwrap_or_default()
.chunks_exact(12)
.take(count)
{
let start = read_u32(group, 0)?;
let end = read_u32(group, 4)?;
let first_gid = read_u32(group, 8)?;
for code in cmap_codes(start, end, codes) {
let Some(gid) = first_gid
.checked_add(code - start)
.and_then(|gid| u16::try_from(gid).ok())
else {
continue;
};
if gid != 0 && gid < self.num_glyphs {
mappings.insert(code, gid);
}
}
}
}
_ => {
return Err(ParseError::SyntaxError {
position: 0,
message: format!("Unsupported cmap format: {format}"),
})
}
}
Ok(CmapSubtable {
platform_id,
encoding_id,
format,
mappings,
})
}
pub fn get_glyph_metrics(&self, glyph_id: u16) -> ParseResult<(u16, i16)> {
let hhea_key: [u8; 4] = HHEA_TABLE;
let hhea_table = self
.tables
.get(&hhea_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hhea table".to_string(),
})?;
let hmtx_key: [u8; 4] = HMTX_TABLE;
let hmtx_table = self
.tables
.get(&hmtx_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hmtx table".to_string(),
})?;
let hhea_offset = hhea_table.offset as usize;
if hhea_offset + 36 > self.data.len() {
return Err(ParseError::SyntaxError {
position: hhea_offset,
message: "Hhea table too small".to_string(),
});
}
let num_h_metrics = read_u16(&self.data, hhea_offset + 34)?;
let hmtx_offset = hmtx_table.offset as usize;
if glyph_id < num_h_metrics {
let offset = hmtx_offset + (glyph_id as usize * 4);
if offset + 4 > self.data.len() {
return Err(ParseError::SyntaxError {
position: offset,
message: "Hmtx entry extends beyond file".to_string(),
});
}
let advance_width = read_u16(&self.data, offset)?;
let lsb = read_i16(&self.data, offset + 2)?;
Ok((advance_width, lsb))
} else {
let last_aw_offset = hmtx_offset + ((num_h_metrics - 1) as usize * 4);
if last_aw_offset + 2 > self.data.len() {
return Err(ParseError::SyntaxError {
position: last_aw_offset,
message: "Hmtx table too small".to_string(),
});
}
let advance_width = read_u16(&self.data, last_aw_offset)?;
let lsb_offset = hmtx_offset
+ (num_h_metrics as usize * 4)
+ ((glyph_id - num_h_metrics) as usize * 2);
if lsb_offset + 2 > self.data.len() {
return Ok((advance_width, 0));
}
let lsb = read_i16(&self.data, lsb_offset)?;
Ok((advance_width, lsb))
}
}
pub fn get_ascent(&self) -> ParseResult<i16> {
let hhea_key: [u8; 4] = HHEA_TABLE;
let hhea_table = self
.tables
.get(&hhea_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hhea table".to_string(),
})?;
let ascent_offset = hhea_table.offset as usize + 4; if ascent_offset + 2 > self.data.len() {
return Err(ParseError::SyntaxError {
position: ascent_offset,
message: "Incomplete hhea table - ascent".to_string(),
});
}
read_i16(&self.data, ascent_offset)
}
pub fn get_descent(&self) -> ParseResult<i16> {
let hhea_key: [u8; 4] = HHEA_TABLE;
let hhea_table = self
.tables
.get(&hhea_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hhea table".to_string(),
})?;
let descent_offset = hhea_table.offset as usize + 6; if descent_offset + 2 > self.data.len() {
return Err(ParseError::SyntaxError {
position: descent_offset,
message: "Incomplete hhea table - descent".to_string(),
});
}
read_i16(&self.data, descent_offset)
}
pub fn get_font_bbox(&self) -> ParseResult<[f32; 4]> {
let head_key: [u8; 4] = HEAD_TABLE;
let head_table = self
.tables
.get(&head_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing head table".to_string(),
})?;
let bbox_offset = head_table.offset as usize + 36; if bbox_offset + 8 > self.data.len() {
return Err(ParseError::SyntaxError {
position: bbox_offset,
message: "Incomplete head table - bbox".to_string(),
});
}
let xmin = read_i16(&self.data, bbox_offset)? as f32;
let ymin = read_i16(&self.data, bbox_offset + 2)? as f32;
let xmax = read_i16(&self.data, bbox_offset + 4)? as f32;
let ymax = read_i16(&self.data, bbox_offset + 6)? as f32;
Ok([xmin, ymin, xmax, ymax])
}
pub fn get_italic_angle(&self) -> ParseResult<f32> {
let head_key: [u8; 4] = HEAD_TABLE;
let head_table = self
.tables
.get(&head_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing head table".to_string(),
})?;
let mac_style_offset = head_table.offset as usize + 44;
if mac_style_offset + 2 > self.data.len() {
return Err(ParseError::SyntaxError {
position: mac_style_offset,
message: "Incomplete head table - macStyle".to_string(),
});
}
let mac_style = read_u16(&self.data, mac_style_offset)?;
if mac_style & 0x02 != 0 {
Ok(-12.0) } else {
Ok(0.0)
}
}
pub fn is_fixed_pitch(&self) -> ParseResult<bool> {
let hhea_key: [u8; 4] = HHEA_TABLE;
let hhea_table = self
.tables
.get(&hhea_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hhea table".to_string(),
})?;
let hmtx_key: [u8; 4] = HMTX_TABLE;
let hmtx_table = self
.tables
.get(&hmtx_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing hmtx table".to_string(),
})?;
let hhea_offset = hhea_table.offset as usize;
let num_hmetrics = read_u16(&self.data, hhea_offset + 34)?;
if num_hmetrics < 2 {
return Ok(false); }
let hmtx_offset = hmtx_table.offset as usize;
let first_width = read_u16(&self.data, hmtx_offset)?;
let check_count = std::cmp::min(5, num_hmetrics);
for i in 1..check_count {
let width_offset = hmtx_offset + (i as usize * 4);
if width_offset + 2 > self.data.len() {
break;
}
let width = read_u16(&self.data, width_offset)?;
if width != first_width {
return Ok(false); }
}
Ok(true) }
pub fn get_cap_height(&self) -> ParseResult<f32> {
let ascent = self.get_ascent()? as f32;
Ok(ascent * 0.7)
}
pub fn get_stem_width(&self) -> ParseResult<f32> {
let bbox = self.get_font_bbox()?;
let bbox_width = bbox[2] - bbox[0];
Ok(bbox_width * 0.12)
}
pub fn create_subset(&self, glyph_indices: &HashSet<u16>) -> ParseResult<Vec<u8>> {
if glyph_indices.iter().any(|&gid| gid >= self.num_glyphs) || self.num_glyphs == 0 {
return Err(ParseError::SyntaxError {
position: 0,
message: "Subset glyph ID outside maxp numGlyphs".into(),
});
}
if self.is_cff {
return Ok(self.data.clone());
}
let mut subset_glyphs = glyph_indices.clone();
subset_glyphs.insert(0);
let mut glyph_map: HashMap<u16, u16> = HashMap::new();
let mut sorted_glyphs: Vec<u16> = subset_glyphs.iter().copied().collect();
sorted_glyphs.sort();
for (new_index, &old_index) in sorted_glyphs.iter().enumerate() {
glyph_map.insert(old_index, new_index as u16);
}
let mut output = Vec::new();
if self.data.len() < 12 {
return Err(ParseError::SyntaxError {
position: 0,
message: "Font data too small".to_string(),
});
}
output.extend_from_slice(&self.data[0..12]);
let mut new_tables = Vec::new();
let mut table_data = Vec::new();
for table_entry in self.tables.values() {
let tag_str = std::str::from_utf8(&table_entry.tag).unwrap_or("");
match tag_str {
"glyf" => {
let (new_glyf, new_loca) = self.subset_glyf_table(&glyph_map)?;
new_tables.push((
table_entry.tag,
table_data.len() as u32,
new_glyf.len() as u32,
));
table_data.extend(new_glyf);
let loca_tag = [b'l', b'o', b'c', b'a'];
new_tables.push((loca_tag, table_data.len() as u32, new_loca.len() as u32));
table_data.extend(new_loca);
}
"loca" => {
}
"cmap" => {
let new_cmap = self.create_subset_cmap(&glyph_map)?;
new_tables.push((
table_entry.tag,
table_data.len() as u32,
new_cmap.len() as u32,
));
table_data.extend(new_cmap);
}
"hmtx" => {
let new_hmtx = self.subset_hmtx_table(&glyph_map)?;
new_tables.push((
table_entry.tag,
table_data.len() as u32,
new_hmtx.len() as u32,
));
table_data.extend(new_hmtx);
}
"maxp" | "head" | "hhea" => {
let updated =
self.update_table_for_subset(&table_entry.tag, glyph_map.len() as u16)?;
new_tables.push((
table_entry.tag,
table_data.len() as u32,
updated.len() as u32,
));
table_data.extend(updated);
}
_ => {
let start = table_entry.offset as usize;
let end = start + table_entry.length as usize;
if end <= self.data.len() {
let table_bytes = &self.data[start..end];
new_tables.push((
table_entry.tag,
table_data.len() as u32,
table_bytes.len() as u32,
));
table_data.extend_from_slice(table_bytes);
}
}
}
}
let num_tables = new_tables.len() as u16;
output[4] = (num_tables >> 8) as u8;
output[5] = (num_tables & 0xFF) as u8;
let table_dir_offset = output.len();
for &(tag, offset, length) in &new_tables {
output.extend(&tag);
output.extend(&[0, 0, 0, 0]); output.extend(
&((table_dir_offset + new_tables.len() * 16 + offset as usize) as u32)
.to_be_bytes(),
);
output.extend(&length.to_be_bytes());
}
output.extend(table_data);
while output.len() % 4 != 0 {
output.push(0);
}
Ok(output)
}
fn subset_glyf_table(&self, glyph_map: &HashMap<u16, u16>) -> ParseResult<(Vec<u8>, Vec<u8>)> {
let glyf_key: [u8; 4] = GLYF_TABLE;
let glyf_table = self
.tables
.get(&glyf_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing glyf table".to_string(),
})?;
let loca_key: [u8; 4] = LOCA_TABLE;
let loca_table = self
.tables
.get(&loca_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing loca table".to_string(),
})?;
let glyf_offset = glyf_table.offset as usize;
let loca_offset = loca_table.offset as usize;
let mut new_glyf = Vec::new();
let mut new_loca = Vec::new();
if self.loca_format == 0 {
new_loca.extend(&[0u8, 0]);
} else {
new_loca.extend(&[0u8, 0, 0, 0]);
}
let mut sorted_entries: Vec<(&u16, &u16)> = glyph_map.iter().collect();
sorted_entries.sort_by_key(|(_, &new_idx)| new_idx);
for &(old_index, _new_index) in &sorted_entries {
let (glyph_start, glyph_end) = if self.loca_format == 0 {
let idx = *old_index as usize;
if loca_offset + (idx + 1) * 2 + 2 > self.data.len() {
(0, 0)
} else {
let start = read_u16(&self.data, loca_offset + idx * 2)? as u32 * 2;
let end = read_u16(&self.data, loca_offset + (idx + 1) * 2)? as u32 * 2;
(start, end)
}
} else {
let idx = *old_index as usize;
if loca_offset + (idx + 1) * 4 + 4 > self.data.len() {
(0, 0)
} else {
let start = read_u32(&self.data, loca_offset + idx * 4)?;
let end = read_u32(&self.data, loca_offset + (idx + 1) * 4)?;
(start, end)
}
};
let glyph_length = (glyph_end - glyph_start) as usize;
if glyph_length > 0 {
let abs_start = glyf_offset + glyph_start as usize;
let abs_end = abs_start + glyph_length;
if abs_end <= self.data.len() {
let glyph_data = &self.data[abs_start..abs_end];
new_glyf.extend_from_slice(glyph_data);
}
}
let new_offset = new_glyf.len() as u32;
if self.loca_format == 0 {
let offset_short = (new_offset / 2) as u16;
new_loca.extend(&offset_short.to_be_bytes());
} else {
new_loca.extend(&new_offset.to_be_bytes());
}
}
Ok((new_glyf, new_loca))
}
fn create_subset_cmap(&self, glyph_map: &HashMap<u16, u16>) -> ParseResult<Vec<u8>> {
let subtables = self.parse_cmap()?;
let base_subtable = CmapSubtable::select_best_or_first(&subtables).ok_or_else(|| {
ParseError::SyntaxError {
position: 0,
message: "No suitable cmap subtable found".to_string(),
}
})?;
let mut new_mappings = HashMap::new();
for (char_code, &old_glyph) in &base_subtable.mappings {
if let Some(&new_glyph) = glyph_map.get(&old_glyph) {
new_mappings.insert(*char_code, new_glyph);
}
}
let mut cmap = Vec::new();
cmap.extend(&[0u8, 0]); cmap.extend(&[0u8, 1]);
cmap.extend(&[0u8, 3]); cmap.extend(&[0u8, 1]); cmap.extend(&[0u8, 0, 0, 12]);
let subtable_start = cmap.len();
cmap.extend(&[0u8, 4]); cmap.extend(&[0u8, 0]); cmap.extend(&[0u8, 0]);
let mut segments = Vec::new();
let mut sorted_chars: Vec<u32> = new_mappings.keys().copied().collect();
sorted_chars.sort();
if !sorted_chars.is_empty() {
let mut start = sorted_chars[0];
let mut end = start;
let mut start_glyph = new_mappings[&start];
for &ch in &sorted_chars[1..] {
let glyph = new_mappings[&ch];
if ch == end + 1 && glyph == start_glyph + (ch - start) as u16 {
end = ch;
} else {
segments.push((
start as u16,
end as u16,
start_glyph,
(start_glyph as i16 - start as i16),
));
start = ch;
end = ch;
start_glyph = glyph;
}
}
segments.push((
start as u16,
end as u16,
start_glyph,
(start_glyph as i16 - start as i16),
));
}
segments.push((0xFFFF, 0xFFFF, 0, 0));
let seg_count = segments.len();
let seg_count_x2 = (seg_count * 2) as u16;
cmap.extend(&seg_count_x2.to_be_bytes());
let mut search_range = 2;
let mut entry_selector: u16 = 0;
while search_range < seg_count {
search_range *= 2;
entry_selector += 1;
}
search_range *= 2;
let range_shift = seg_count_x2.saturating_sub(search_range as u16);
cmap.extend(&(search_range as u16).to_be_bytes());
cmap.extend(&entry_selector.to_be_bytes());
cmap.extend(&range_shift.to_be_bytes());
for &(_, end, _, _) in &segments {
cmap.extend(&end.to_be_bytes());
}
cmap.extend(&[0u8, 0]);
for &(start, _, _, _) in &segments {
cmap.extend(&start.to_be_bytes());
}
for &(_, _, _, delta) in &segments {
cmap.extend(&delta.to_be_bytes());
}
for _ in &segments {
cmap.extend(&[0u8, 0]);
}
let subtable_length = (cmap.len() - subtable_start) as u16;
cmap[subtable_start + 2] = (subtable_length >> 8) as u8;
cmap[subtable_start + 3] = (subtable_length & 0xFF) as u8;
Ok(cmap)
}
fn subset_hmtx_table(&self, glyph_map: &HashMap<u16, u16>) -> ParseResult<Vec<u8>> {
let mut new_hmtx = Vec::new();
let mut sorted_entries: Vec<(&u16, &u16)> = glyph_map.iter().collect();
sorted_entries.sort_by_key(|(_, &new_idx)| new_idx);
for &(old_index, _new_index) in &sorted_entries {
let (advance_width, lsb) = self.get_glyph_metrics(*old_index)?;
new_hmtx.extend(&advance_width.to_be_bytes());
new_hmtx.extend(&lsb.to_be_bytes());
}
Ok(new_hmtx)
}
fn update_table_for_subset(&self, tag: &[u8; 4], new_glyph_count: u16) -> ParseResult<Vec<u8>> {
let table_entry = self
.tables
.get(tag)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: format!(
"Missing table: {}",
std::str::from_utf8(tag).unwrap_or("???")
),
})?;
let start = table_entry.offset as usize;
let length = table_entry.length as usize;
if start + length > self.data.len() {
return Err(ParseError::SyntaxError {
position: start,
message: "Table extends beyond file".to_string(),
});
}
let mut table_data = self.data[start..start + length].to_vec();
match std::str::from_utf8(tag).unwrap_or("") {
"maxp" => {
if table_data.len() >= 6 {
table_data[4] = (new_glyph_count >> 8) as u8;
table_data[5] = (new_glyph_count & 0xFF) as u8;
}
}
"hhea" => {
if table_data.len() >= 36 {
table_data[34] = (new_glyph_count >> 8) as u8;
table_data[35] = (new_glyph_count & 0xFF) as u8;
}
}
_ => {}
}
Ok(table_data)
}
pub(crate) fn post_glyph_names(&self) -> Option<HashMap<u16, String>> {
let entry = self.tables.get(&POST_TABLE)?;
let start = entry.offset as usize;
let end = start
.checked_add(entry.length as usize)?
.min(self.data.len());
let table = self.data.get(start..end)?;
let version = u32::from_be_bytes(table.get(0..4)?.try_into().ok()?);
match version {
0x0001_0000 => Some(
(0..self.num_glyphs.min(258))
.map(|gid| (gid, MAC_GLYPH_NAMES[usize::from(gid)].to_owned()))
.collect(),
),
0x0002_0000 => {
let count = usize::from(u16::from_be_bytes(table.get(32..34)?.try_into().ok()?));
let indices_end = 34 + 2 * count;
let indices = table.get(34..indices_end)?;
let mut custom = Vec::new();
let mut position = indices_end;
while let Some(&length) = table.get(position) {
let name_end = position + 1 + usize::from(length);
let Some(bytes) = table.get(position + 1..name_end) else {
break;
};
custom.push(String::from_utf8_lossy(bytes).into_owned());
position = name_end;
}
let mut names = HashMap::with_capacity(count);
for (gid, index) in (0u16..).zip(indices.chunks_exact(2)) {
let index = usize::from(u16::from_be_bytes([index[0], index[1]]));
let name = match index.checked_sub(MAC_GLYPH_NAMES.len()) {
None => Some(MAC_GLYPH_NAMES[index].to_owned()),
Some(custom_index) => custom.get(custom_index).cloned(),
};
if let Some(name) = name {
names.insert(gid, name);
}
}
Some(names)
}
_ => None,
}
}
pub fn get_all_glyph_indices(&self) -> HashSet<u16> {
let mut indices = HashSet::new();
if let Ok(subtables) = self.parse_cmap() {
for subtable in subtables {
for &glyph_id in subtable.mappings.values() {
indices.insert(glyph_id);
}
}
}
indices
}
}
fn cmap_codes(start: u32, end: u32, codes: Option<&[u32]>) -> Box<dyn Iterator<Item = u32> + '_> {
if start > end {
return Box::new(std::iter::empty());
}
match codes {
Some(codes) => {
let first = codes.partition_point(|code| *code < start);
let last = codes.partition_point(|code| *code <= end);
Box::new(codes[first..last].iter().copied())
}
None => Box::new(start..=end),
}
}
fn read_u16(data: &[u8], offset: usize) -> ParseResult<u16> {
let bytes = data
.get(offset..)
.and_then(|tail| tail.get(..2))
.ok_or_else(|| ParseError::SyntaxError {
position: offset,
message: "Insufficient data for u16".to_string(),
})?;
Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
}
fn read_i16(data: &[u8], offset: usize) -> ParseResult<i16> {
read_u16(data, offset).map(|v| v as i16)
}
fn read_u32(data: &[u8], offset: usize) -> ParseResult<u32> {
let bytes = data
.get(offset..)
.and_then(|tail| tail.get(..4))
.ok_or_else(|| ParseError::SyntaxError {
position: offset,
message: "Insufficient data for u32".to_string(),
})?;
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_byte_cmap_selection_is_bounded_and_sorted() {
let codes = [0, 65, 255, 0xF041, 0xF141, 0xF241];
assert_eq!(
cmap_codes(0, u32::MAX, Some(&codes)).collect::<Vec<_>>(),
codes
);
assert_eq!(
cmap_codes(65, 255, Some(&codes)).collect::<Vec<_>>(),
[65, 255]
);
assert_eq!(cmap_codes(66, 254, Some(&codes)).count(), 0);
assert_eq!(cmap_codes(66, 65, Some(&codes)).count(), 0);
}
#[test]
fn truncated_directory_and_end_offsets_are_controlled_errors() {
let header = vec![0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0];
assert!(matches!(
TrueTypeFont::parse(header),
Err(ParseError::SyntaxError { .. })
));
assert!(read_u16(&[0, 1], usize::MAX).is_err());
assert!(read_u32(&[0, 0, 0, 1], usize::MAX).is_err());
assert_eq!(read_u16(&[0, 1], 0).unwrap(), 1);
assert_eq!(read_u32(&[0, 0, 0, 1], 0).unwrap(), 1);
}
fn font_with_post(post: Vec<u8>, num_glyphs: u16) -> TrueTypeFont {
let length = post.len() as u32;
let mut font = TrueTypeFont::empty_for_test();
font.data = post;
font.num_glyphs = num_glyphs;
font.tables.insert(
POST_TABLE,
TableEntry {
tag: POST_TABLE,
_checksum: 0,
offset: 0,
length,
},
);
font
}
fn post_v2(indices: &[u16], names: &[&[u8]]) -> Vec<u8> {
let mut post = 0x0002_0000u32.to_be_bytes().to_vec();
post.resize(32, 0);
post.extend_from_slice(&(indices.len() as u16).to_be_bytes());
for index in indices {
post.extend_from_slice(&index.to_be_bytes());
}
for name in names {
post.push(name.len() as u8);
post.extend_from_slice(name);
}
post
}
#[test]
fn post_format_2_resolves_standard_and_custom_names() {
let post = post_v2(&[0, 36, 258, 259], &[b"uniE000", b"Aogon"]);
let names = font_with_post(post, 4).post_glyph_names().unwrap();
assert_eq!(names[&0], ".notdef");
assert_eq!(names[&1], "A");
assert_eq!(names[&2], "uniE000");
assert_eq!(names[&3], "Aogon");
}
#[test]
fn post_format_2_tolerates_truncated_name_data() {
let mut post = post_v2(&[37, 258], &[]);
post.extend_from_slice(b"\x09abc");
let names = font_with_post(post, 2).post_glyph_names().unwrap();
assert_eq!(names.get(&0).map(String::as_str), Some("B"));
assert_eq!(names.get(&1), None);
}
#[test]
fn post_format_1_uses_the_standard_macintosh_order() {
let mut post = 0x0001_0000u32.to_be_bytes().to_vec();
post.resize(32, 0);
let names = font_with_post(post, 300).post_glyph_names().unwrap();
assert_eq!(names.len(), MAC_GLYPH_NAMES.len());
assert_eq!(names[&3], "space");
assert_eq!(names[&257], "dcroat");
}
#[test]
fn post_format_3_and_missing_table_have_no_names() {
let mut post = 0x0003_0000u32.to_be_bytes().to_vec();
post.resize(32, 0);
assert!(font_with_post(post, 4).post_glyph_names().is_none());
assert!(TrueTypeFont::empty_for_test().post_glyph_names().is_none());
}
#[test]
fn test_read_helpers() {
let data = vec![0x00, 0x10, 0xFF, 0xFE, 0x12, 0x34, 0x56, 0x78];
assert_eq!(read_u16(&data, 0).unwrap(), 0x0010);
assert_eq!(read_u16(&data, 2).unwrap(), 0xFFFE);
assert_eq!(read_i16(&data, 2).unwrap(), -2);
assert_eq!(read_u32(&data, 4).unwrap(), 0x12345678);
assert!(read_u16(&data, 7).is_err());
assert!(read_u32(&data, 5).is_err());
}
#[test]
fn test_invalid_font_signatures() {
let invalid_data = vec![
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let result = TrueTypeFont::parse(invalid_data);
assert!(result.is_err());
let short_data = vec![0x00, 0x01];
let result = TrueTypeFont::parse(short_data);
assert!(result.is_err());
}
#[test]
fn test_cmap_format_0() {
let font = TrueTypeFont::parse(
include_bytes!("../../../tests/fixtures/text_contracts/symbolic/mac-subset.ttf")
.to_vec(),
)
.unwrap();
let subtables = font.parse_cmap().unwrap();
let table = subtables
.iter()
.find(|table| table.format == 0)
.expect("format 0 fixture");
assert_eq!(table.mappings.get(&65), Some(&3));
assert_eq!(font.get_glyph_metrics(3).unwrap().0, 700);
assert!(!font
.create_subset(&HashSet::from([0, 3]))
.unwrap()
.is_empty());
}
#[test]
fn test_table_entry_creation() {
let entry = TableEntry {
tag: [b'h', b'e', b'a', b'd'],
_checksum: 0x12345678,
offset: 1024,
length: 256,
};
assert_eq!(entry.tag, *b"head");
assert_eq!(entry._checksum, 0x12345678);
assert_eq!(entry.offset, 1024);
assert_eq!(entry.length, 256);
}
#[test]
fn test_glyph_info_creation() {
let glyph = GlyphInfo {
index: 42,
unicode: vec![0x0041, 0x0061], advance_width: 600,
lsb: 50,
};
assert_eq!(glyph.index, 42);
assert_eq!(glyph.unicode.len(), 2);
assert_eq!(glyph.advance_width, 600);
assert_eq!(glyph.lsb, 50);
}
#[test]
fn test_cmap_subtable_creation() {
let mut mappings = HashMap::new();
mappings.insert(65, 1); mappings.insert(66, 2);
let subtable = CmapSubtable {
platform_id: 3, encoding_id: 1, format: 4,
mappings,
};
assert_eq!(subtable.platform_id, 3);
assert_eq!(subtable.encoding_id, 1);
assert_eq!(subtable.format, 4);
assert_eq!(subtable.mappings.get(&65), Some(&1));
assert_eq!(subtable.mappings.get(&66), Some(&2));
}
#[test]
fn test_read_u16() {
let data = vec![0xAB, 0xCD];
let result = read_u16(&data, 0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0xABCD);
let result = read_u16(&data, 1);
assert!(result.is_err());
}
#[test]
fn test_read_u32() {
let data = vec![0x12, 0x34, 0x56, 0x78];
let result = read_u32(&data, 0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0x12345678);
let result = read_u32(&data, 2);
assert!(result.is_err());
}
#[test]
fn test_read_i16() {
let data = vec![0xFF, 0xFE]; let result = read_i16(&data, 0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), -2);
let data = vec![0x00, 0x7F]; let result = read_i16(&data, 0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 127);
}
#[test]
fn test_required_tables() {
assert_eq!(TRUETYPE_REQUIRED_TABLES.len(), 7);
assert!(TRUETYPE_REQUIRED_TABLES.contains(&HEAD_TABLE.as_slice()));
assert!(TRUETYPE_REQUIRED_TABLES.contains(&CMAP_TABLE.as_slice()));
assert!(TRUETYPE_REQUIRED_TABLES.contains(&GLYF_TABLE.as_slice()));
assert!(TRUETYPE_REQUIRED_TABLES.contains(&LOCA_TABLE.as_slice()));
assert!(TRUETYPE_REQUIRED_TABLES.contains(&MAXP_TABLE.as_slice()));
assert!(TRUETYPE_REQUIRED_TABLES.contains(&HHEA_TABLE.as_slice()));
assert!(TRUETYPE_REQUIRED_TABLES.contains(&HMTX_TABLE.as_slice()));
assert_eq!(CFF_REQUIRED_TABLES.len(), 6);
assert!(CFF_REQUIRED_TABLES.contains(&HEAD_TABLE.as_slice()));
assert!(CFF_REQUIRED_TABLES.contains(&CMAP_TABLE.as_slice()));
assert!(CFF_REQUIRED_TABLES.contains(&CFF_TABLE.as_slice()));
assert!(CFF_REQUIRED_TABLES.contains(&MAXP_TABLE.as_slice()));
assert!(CFF_REQUIRED_TABLES.contains(&HHEA_TABLE.as_slice()));
assert!(CFF_REQUIRED_TABLES.contains(&HMTX_TABLE.as_slice()));
}
#[test]
fn test_minimal_font_too_small() {
let data = vec![0; 11]; let result = TrueTypeFont::parse(data);
assert!(result.is_err());
}
#[test]
fn test_font_with_invalid_signature() {
let mut data = vec![0; 100];
data[0..4].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
let result = TrueTypeFont::parse(data);
assert!(result.is_err());
}
#[test]
fn test_font_with_zero_tables() {
let mut data = vec![0; 12];
data[0..4].copy_from_slice(&[0x00, 0x01, 0x00, 0x00]);
data[4..6].copy_from_slice(&[0x00, 0x00]);
let result = TrueTypeFont::parse(data);
assert!(result.is_err()); }
#[test]
fn test_get_table_not_found() {
let font = TrueTypeFont {
data: vec![],
tables: HashMap::new(),
num_glyphs: 0,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
let result = font.get_table(b"test");
assert!(result.is_err());
}
#[test]
fn test_get_table_found() {
let mut tables = HashMap::new();
tables.insert(
[b'h', b'e', b'a', b'd'],
TableEntry {
tag: [b'h', b'e', b'a', b'd'],
_checksum: 0,
offset: 100,
length: 54,
},
);
let font = TrueTypeFont {
data: vec![],
tables,
num_glyphs: 0,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
let result = font.get_table(b"head");
assert!(result.is_ok());
assert_eq!(result.unwrap().offset, 100);
}
#[test]
fn test_parse_head_table_too_small() {
let font = TrueTypeFont {
data: vec![0; 50], tables: HashMap::new(),
num_glyphs: 0,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
assert!(font.tables.is_empty());
}
#[test]
fn test_parse_maxp_table_too_small() {
let font = TrueTypeFont {
data: vec![0; 5], tables: HashMap::new(),
num_glyphs: 0,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
assert_eq!(font.num_glyphs, 0);
}
#[test]
fn test_parse_loca_short_format() {
let mut data = vec![0; 100];
for i in 0..10 {
let offset = (i * 100) as u16;
data[i * 2] = (offset >> 8) as u8;
data[i * 2 + 1] = (offset & 0xFF) as u8;
}
let font = TrueTypeFont {
data: data.clone(),
tables: HashMap::new(),
num_glyphs: 10,
units_per_em: 1000,
loca_format: 0, is_cff: false,
};
assert_eq!(data.len(), 100);
assert_eq!(font.loca_format, 0);
}
#[test]
fn test_parse_loca_long_format() {
let mut data = vec![0; 100];
for i in 0..10 {
let offset = (i * 1000) as u32;
data[i * 4] = (offset >> 24) as u8;
data[i * 4 + 1] = ((offset >> 16) & 0xFF) as u8;
data[i * 4 + 2] = ((offset >> 8) & 0xFF) as u8;
data[i * 4 + 3] = (offset & 0xFF) as u8;
}
let font = TrueTypeFont {
data: data.clone(),
tables: HashMap::new(),
num_glyphs: 10,
units_per_em: 1000,
loca_format: 1, is_cff: false,
};
assert_eq!(data.len(), 100);
assert_eq!(font.loca_format, 1);
}
#[test]
fn test_get_glyph_data() {
let mut data = vec![0; 1000];
data[100..110].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let mut tables = HashMap::new();
tables.insert(
[b'g', b'l', b'y', b'f'],
TableEntry {
tag: [b'g', b'l', b'y', b'f'],
_checksum: 0,
offset: 0,
length: 1000,
},
);
let font = TrueTypeFont {
data,
tables,
num_glyphs: 10,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
assert_eq!(font.num_glyphs, 10);
assert_eq!(font.data.len(), 1000);
}
#[test]
fn test_subset_font_empty() {
let font = TrueTypeFont {
data: vec![],
tables: HashMap::new(),
num_glyphs: 0,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
let result = font.create_subset(&HashSet::new());
assert!(result.is_err()); }
#[test]
fn test_font_metrics() {
let font = TrueTypeFont {
data: vec![],
tables: HashMap::new(),
num_glyphs: 100,
units_per_em: 2048,
loca_format: 1,
is_cff: false,
};
assert_eq!(font.num_glyphs, 100);
assert_eq!(font.units_per_em, 2048);
assert_eq!(font.loca_format, 1);
}
#[test]
fn test_subset_font_data_too_small() {
let font = TrueTypeFont {
data: vec![1, 2, 3], tables: HashMap::new(),
num_glyphs: 10,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
let glyphs = HashSet::from([0, 1, 2]);
let result = font.create_subset(&glyphs);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Font data too small"));
}
#[test]
fn test_subset_with_missing_glyphs() {
let mut font = TrueTypeFont {
data: vec![0; 100], tables: HashMap::new(),
num_glyphs: 10,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
font.tables.insert(
*b"head",
TableEntry {
tag: *b"head",
_checksum: 0,
offset: 12,
length: 20,
},
);
let glyphs = HashSet::from([100, 200, 300]); let result = font.create_subset(&glyphs);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_parse_table_entries_edge_cases() {
let _font = TrueTypeFont {
data: vec![0; 1000],
tables: HashMap::new(),
num_glyphs: 10,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
let empty_table = TableEntry {
tag: *b"test",
_checksum: 0,
offset: 0,
length: 0,
};
assert_eq!(empty_table.length, 0);
let end_table = TableEntry {
tag: *b"end ",
_checksum: 0,
offset: 990,
length: 10,
};
assert_eq!(end_table.offset + end_table.length, 1000);
}
#[test]
fn test_get_glyph_data_bounds_checking() {
let font = TrueTypeFont {
data: vec![0; 100],
tables: HashMap::new(),
num_glyphs: 5,
units_per_em: 1000,
loca_format: 0,
is_cff: false,
};
let glyph_id = 3;
assert!(glyph_id < font.num_glyphs);
let invalid_glyph = 10;
assert!(invalid_glyph >= font.num_glyphs);
}
#[test]
fn test_loca_format_variations() {
let font_short = TrueTypeFont {
data: vec![],
tables: HashMap::new(),
num_glyphs: 10,
units_per_em: 1000,
loca_format: 0, is_cff: false,
};
assert_eq!(font_short.loca_format, 0);
let font_long = TrueTypeFont {
data: vec![],
tables: HashMap::new(),
num_glyphs: 10,
units_per_em: 1000,
loca_format: 1, is_cff: false,
};
assert_eq!(font_long.loca_format, 1);
}
#[test]
fn test_cff_font_detection() {
let font_cff = TrueTypeFont {
data: vec![0x4F, 0x54, 0x54, 0x4F], tables: HashMap::new(),
num_glyphs: 100,
units_per_em: 1000,
loca_format: 0,
is_cff: true, };
assert!(font_cff.is_cff, "CFF font should have is_cff = true");
assert!(font_cff.is_cff_font(), "is_cff_font() should return true");
let font_ttf = TrueTypeFont {
data: vec![0x00, 0x01, 0x00, 0x00], tables: HashMap::new(),
num_glyphs: 100,
units_per_em: 1000,
loca_format: 0,
is_cff: false, };
assert!(!font_ttf.is_cff, "TrueType font should have is_cff = false");
assert!(!font_ttf.is_cff_font(), "is_cff_font() should return false");
}
#[test]
fn test_cff_table_detection() {
let mut tables = HashMap::new();
tables.insert(*b"CFF ", (100, 200)); tables.insert(*b"head", (300, 54));
tables.insert(*b"cmap", (400, 100));
let has_cff = tables.contains_key(&CFF_TABLE);
assert!(has_cff, "Should detect CFF table in font");
let mut tables_no_cff = HashMap::new();
tables_no_cff.insert(*b"glyf", (100, 200));
tables_no_cff.insert(*b"head", (300, 54));
tables_no_cff.insert(*b"cmap", (400, 100));
let has_cff_2 = tables_no_cff.contains_key(&CFF_TABLE);
assert!(!has_cff_2, "Should not detect CFF table when absent");
}
#[test]
fn test_required_tables_for_cff() {
let cff_tables = vec![
&HEAD_TABLE,
&CMAP_TABLE,
&CFF_TABLE,
&MAXP_TABLE,
&HHEA_TABLE,
&HMTX_TABLE,
];
assert!(
!cff_tables.contains(&&GLYF_TABLE),
"CFF fonts should not require glyf table"
);
assert!(
!cff_tables.contains(&&LOCA_TABLE),
"CFF fonts should not require loca table"
);
let ttf_tables = vec![
&HEAD_TABLE,
&CMAP_TABLE,
&GLYF_TABLE,
&LOCA_TABLE,
&MAXP_TABLE,
&HHEA_TABLE,
&HMTX_TABLE,
];
assert!(
ttf_tables.contains(&&GLYF_TABLE),
"TrueType fonts should require glyf table"
);
assert!(
ttf_tables.contains(&&LOCA_TABLE),
"TrueType fonts should require loca table"
);
}
}