use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FontVerticalMetrics {
pub ascent: i16,
pub descent: i16,
pub line_gap: i16,
}
impl FontVerticalMetrics {
pub const fn new(ascent: i16, descent: i16, line_gap: i16) -> Self {
Self {
ascent,
descent,
line_gap,
}
}
pub fn ascender_ratio(self, units_per_em: u16) -> f32 {
if units_per_em == 0 {
return 0.0;
}
f32::from(self.ascent).max(0.0) / f32::from(units_per_em)
}
pub fn descender_ratio(self, units_per_em: u16) -> f32 {
if units_per_em == 0 {
return 0.0;
}
(-f32::from(self.descent)).max(0.0) / f32::from(units_per_em)
}
pub fn line_height_ratio(self, units_per_em: u16) -> f32 {
if units_per_em == 0 {
return 1.0;
}
let height = i32::from(self.ascent) - i32::from(self.descent) + i32::from(self.line_gap);
(height.max(0) as f32 / f32::from(units_per_em)).max(1.0)
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct TtfFont {
pub font_name: String,
pub units_per_em: u16,
pub bbox: [i16; 4],
pub pdf_metrics: FontVerticalMetrics,
pub layout_metrics: FontVerticalMetrics,
pub cmap: HashMap<u16, u16>,
pub glyph_widths: Vec<u16>,
pub num_h_metrics: u16,
pub flags: u32,
pub data: Vec<u8>,
}
impl TtfFont {
pub const fn pdf_vertical_metrics(&self) -> FontVerticalMetrics {
self.pdf_metrics
}
pub const fn layout_vertical_metrics(&self) -> FontVerticalMetrics {
self.layout_metrics
}
pub fn glyph_width(&self, glyph_id: u16) -> u16 {
if (glyph_id as usize) < self.glyph_widths.len() {
self.glyph_widths[glyph_id as usize]
} else {
self.glyph_widths.last().copied().unwrap_or(0)
}
}
#[cfg(test)]
pub fn char_width(&self, ch: u16) -> u16 {
let glyph_id = self.cmap.get(&ch).copied().unwrap_or(0);
self.glyph_width(glyph_id)
}
#[cfg(test)]
pub fn glyph_width_pdf(&self, glyph_id: u16) -> u16 {
self.glyph_width_pdf_value(glyph_id).trunc() as u16
}
pub fn glyph_width_pdf_value(&self, glyph_id: u16) -> f32 {
if self.units_per_em == 0 {
return 0.0;
}
self.glyph_width(glyph_id) as f32 * 1000.0 / self.units_per_em as f32
}
pub fn glyph_width_scaled(&self, glyph_id: u16, font_size: f32) -> f32 {
if self.units_per_em == 0 {
return 0.0;
}
let width = self.glyph_width(glyph_id) as f32;
width * font_size / self.units_per_em as f32
}
#[cfg(test)]
pub fn char_width_pdf(&self, ch: u16) -> u16 {
self.char_width_pdf_value(ch).trunc() as u16
}
#[cfg(test)]
pub fn char_width_pdf_value(&self, ch: u16) -> f32 {
if self.units_per_em == 0 {
return 0.0;
}
self.char_width(ch) as f32 * 1000.0 / self.units_per_em as f32
}
#[cfg(test)]
pub fn char_width_scaled(&self, ch: u16, font_size: f32) -> f32 {
let glyph_id = self.cmap.get(&ch).copied().unwrap_or(0);
self.glyph_width_scaled(glyph_id, font_size)
}
}
#[derive(Debug)]
struct TableRecord {
offset: u32,
#[allow(dead_code)]
length: u32,
}
pub fn parse_ttf_with_index(data: Vec<u8>, face_index: usize) -> Result<TtfFont, String> {
if data.len() >= 12 && &data[0..4] == b"ttcf" {
let num_fonts = read_u32(&data, 8) as usize;
if face_index >= num_fonts {
return Err(format!(
"TTC face index {face_index} out of range (collection has {num_fonts} fonts)"
));
}
let offset_pos = 12 + face_index * 4;
if data.len() < offset_pos + 4 {
return Err("TTC offset table too short".to_string());
}
let font_offset = read_u32(&data, offset_pos) as usize;
if font_offset >= data.len() {
return Err("TTC font offset out of bounds".to_string());
}
return parse_ttf_at_offset(data, font_offset);
}
parse_ttf_at_offset(data, 0)
}
pub fn parse_ttf(data: Vec<u8>) -> Result<TtfFont, String> {
if data.len() >= 12 && &data[0..4] == b"ttcf" {
let num_fonts = read_u32(&data, 8);
if num_fonts == 0 || data.len() < 16 {
return Err("TTC contains no fonts".to_string());
}
let first_offset = read_u32(&data, 12) as usize;
if first_offset >= data.len() {
return Err("TTC first font offset out of bounds".to_string());
}
return parse_ttf_at_offset(data, first_offset);
}
parse_ttf_at_offset(data, 0)
}
fn parse_ttf_at_offset(data: Vec<u8>, base: usize) -> Result<TtfFont, String> {
if data.len() < base + 12 {
return Err("TTF data too short for offset table".to_string());
}
let num_tables = read_u16(&data, base + 4);
if data.len() < base + 12 + num_tables as usize * 16 {
return Err("TTF data too short for table directory".to_string());
}
let mut tables: HashMap<[u8; 4], TableRecord> = HashMap::new();
for i in 0..num_tables as usize {
let offset = base + 12 + i * 16;
let mut tag = [0u8; 4];
tag.copy_from_slice(&data[offset..offset + 4]);
tables.insert(
tag,
TableRecord {
offset: read_u32(&data, offset + 8),
length: read_u32(&data, offset + 12),
},
);
}
let head = tables.get(b"head").ok_or("Missing head table")?;
let head_off = head.offset as usize;
if data.len() < head_off + 54 {
return Err("head table too short".to_string());
}
let units_per_em = read_u16(&data, head_off + 18);
if units_per_em == 0 {
return Err("Invalid units_per_em (0) in head table".to_string());
}
let x_min = read_i16(&data, head_off + 36);
let y_min = read_i16(&data, head_off + 38);
let x_max = read_i16(&data, head_off + 40);
let y_max = read_i16(&data, head_off + 42);
let bbox = [x_min, y_min, x_max, y_max];
let hhea = tables.get(b"hhea").ok_or("Missing hhea table")?;
let hhea_off = hhea.offset as usize;
if data.len() < hhea_off + 36 {
return Err("hhea table too short".to_string());
}
let hhea_ascent = read_i16(&data, hhea_off + 4);
let hhea_descent = read_i16(&data, hhea_off + 6);
let hhea_line_gap = read_i16(&data, hhea_off + 8);
let num_h_metrics = read_u16(&data, hhea_off + 34);
let pdf_metrics = FontVerticalMetrics::new(hhea_ascent, hhea_descent, hhea_line_gap);
let layout_metrics =
parse_os2_typographic_metrics(&data, tables.get(b"OS/2")).unwrap_or(pdf_metrics);
let maxp = tables.get(b"maxp").ok_or("Missing maxp table")?;
let maxp_off = maxp.offset as usize;
if data.len() < maxp_off + 6 {
return Err("maxp table too short".to_string());
}
let num_glyphs = read_u16(&data, maxp_off + 4);
let hmtx = tables.get(b"hmtx").ok_or("Missing hmtx table")?;
let hmtx_off = hmtx.offset as usize;
let mut glyph_widths = Vec::with_capacity(num_glyphs as usize);
let mut last_width = 0u16;
for i in 0..num_glyphs as usize {
if i < num_h_metrics as usize {
let entry_off = hmtx_off + i * 4;
if data.len() < entry_off + 2 {
break;
}
last_width = read_u16(&data, entry_off);
glyph_widths.push(last_width);
} else {
glyph_widths.push(last_width);
}
}
let cmap_table = tables.get(b"cmap").ok_or("Missing cmap table")?;
let cmap = parse_cmap(&data, cmap_table.offset as usize)?;
let name_table = tables.get(b"name").ok_or("Missing name table")?;
let font_name = parse_name_table(&data, name_table.offset as usize)?;
let flags = 32u32;
Ok(TtfFont {
font_name,
units_per_em,
bbox,
pdf_metrics,
layout_metrics,
cmap,
glyph_widths,
num_h_metrics,
flags,
data,
})
}
fn parse_os2_typographic_metrics(
data: &[u8],
os2: Option<&TableRecord>,
) -> Option<FontVerticalMetrics> {
let os2 = os2?;
let os2_off = os2.offset as usize;
if data.len() < os2_off + 74 {
return None;
}
let ascent = read_i16(data, os2_off + 68);
let descent = read_i16(data, os2_off + 70);
let line_gap = read_i16(data, os2_off + 72);
Some(FontVerticalMetrics::new(ascent, descent, line_gap))
}
fn parse_cmap(data: &[u8], offset: usize) -> Result<HashMap<u16, u16>, String> {
if data.len() < offset + 4 {
return Err("cmap table too short".to_string());
}
let num_subtables = read_u16(data, offset + 2);
let mut subtable_offset = None;
for i in 0..num_subtables as usize {
let record_off = offset + 4 + i * 8;
if data.len() < record_off + 8 {
break;
}
let platform_id = read_u16(data, record_off);
let encoding_id = read_u16(data, record_off + 2);
let sub_offset = read_u32(data, record_off + 4) as usize;
if (platform_id == 3 && encoding_id == 1) || (platform_id == 0) {
subtable_offset = Some(offset + sub_offset);
if platform_id == 3 {
break;
}
}
}
let sub_off = subtable_offset.ok_or("No suitable cmap subtable found")?;
if data.len() < sub_off + 2 {
return Err("cmap subtable too short".to_string());
}
let format = read_u16(data, sub_off);
match format {
4 => parse_cmap_format4(data, sub_off),
0 => parse_cmap_format0(data, sub_off),
_ => {
Ok(HashMap::new())
}
}
}
fn parse_cmap_format0(data: &[u8], offset: usize) -> Result<HashMap<u16, u16>, String> {
if data.len() < offset + 262 {
return Err("cmap format 0 table too short".to_string());
}
let mut map = HashMap::new();
for i in 0..256u16 {
let glyph_id = data[offset + 6 + i as usize] as u16;
if glyph_id != 0 {
map.insert(i, glyph_id);
}
}
Ok(map)
}
fn parse_cmap_format4(data: &[u8], offset: usize) -> Result<HashMap<u16, u16>, String> {
if data.len() < offset + 14 {
return Err("cmap format 4 header too short".to_string());
}
let seg_count_x2 = read_u16(data, offset + 6);
let seg_count = seg_count_x2 as usize / 2;
let end_code_off = offset + 14;
let start_code_off = end_code_off + seg_count * 2 + 2;
let id_delta_off = start_code_off + seg_count * 2;
let id_range_offset_off = id_delta_off + seg_count * 2;
let needed = id_range_offset_off + seg_count * 2;
if data.len() < needed {
return Err("cmap format 4 data too short".to_string());
}
let mut map = HashMap::new();
for i in 0..seg_count {
let end_code = read_u16(data, end_code_off + i * 2);
let start_code = read_u16(data, start_code_off + i * 2);
let id_delta = read_i16(data, id_delta_off + i * 2) as i32;
let id_range_offset = read_u16(data, id_range_offset_off + i * 2);
if start_code == 0xFFFF {
break;
}
for c in start_code..=end_code {
let glyph_id = if id_range_offset == 0 {
((c as i32 + id_delta) & 0xFFFF) as u16
} else {
let range_off = id_range_offset_off + i * 2;
let glyph_off =
range_off + id_range_offset as usize + (c as usize - start_code as usize) * 2;
if glyph_off + 1 < data.len() {
let gid = read_u16(data, glyph_off);
if gid != 0 {
((gid as i32 + id_delta) & 0xFFFF) as u16
} else {
0
}
} else {
0
}
};
if glyph_id != 0 {
map.insert(c, glyph_id);
}
}
}
Ok(map)
}
fn parse_name_table(data: &[u8], offset: usize) -> Result<String, String> {
if data.len() < offset + 6 {
return Err("name table too short".to_string());
}
let count = read_u16(data, offset + 2);
let string_offset = read_u16(data, offset + 4) as usize;
let storage_off = offset + string_offset;
let mut best_name: Option<String> = None;
let mut best_priority = 0u8;
for i in 0..count as usize {
let rec_off = offset + 6 + i * 12;
if data.len() < rec_off + 12 {
break;
}
let platform_id = read_u16(data, rec_off);
let encoding_id = read_u16(data, rec_off + 2);
let name_id = read_u16(data, rec_off + 6);
let length = read_u16(data, rec_off + 8) as usize;
let str_offset = read_u16(data, rec_off + 10) as usize;
let priority = match name_id {
6 => 3,
4 => 2,
1 => 1,
_ => continue,
};
if priority <= best_priority {
continue;
}
let start = storage_off + str_offset;
let end = start + length;
if end > data.len() {
continue;
}
let name_bytes = &data[start..end];
let name = if platform_id == 3 || (platform_id == 0 && encoding_id > 0) {
decode_utf16be(name_bytes)
} else {
String::from_utf8_lossy(name_bytes).to_string()
};
if !name.is_empty() {
best_name = Some(name);
best_priority = priority;
}
}
best_name.ok_or_else(|| "No font name found in name table".to_string())
}
fn decode_utf16be(data: &[u8]) -> String {
let mut result = String::new();
let mut i = 0;
while i + 1 < data.len() {
let code_unit = ((data[i] as u16) << 8) | data[i + 1] as u16;
if let Some(ch) = char::from_u32(code_unit as u32) {
result.push(ch);
}
i += 2;
}
result
}
fn read_u16(data: &[u8], offset: usize) -> u16 {
u16::from_be_bytes([data[offset], data[offset + 1]])
}
fn read_i16(data: &[u8], offset: usize) -> i16 {
i16::from_be_bytes([data[offset], data[offset + 1]])
}
fn read_u32(data: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
#[cfg(test)]
mod tests {
use super::*;
fn build_test_ttf() -> Vec<u8> {
let mut buf = Vec::new();
let num_tables: u16 = 6;
buf.extend_from_slice(&[0, 1, 0, 0]); buf.extend_from_slice(&num_tables.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes());
let dir_start = buf.len();
buf.resize(dir_start + num_tables as usize * 16, 0);
let _data_start = buf.len();
let head_offset = buf.len();
buf.extend_from_slice(&[0, 1, 0, 0]); buf.extend_from_slice(&[0, 0, 0, 0]); buf.extend_from_slice(&[0, 0, 0, 0]); buf.extend_from_slice(&[0x5F, 0x0F, 0x3C, 0xF5]); buf.extend_from_slice(&0x000Bu16.to_be_bytes()); buf.extend_from_slice(&1000u16.to_be_bytes()); buf.extend_from_slice(&[0; 8]); buf.extend_from_slice(&[0; 8]); buf.extend_from_slice(&(-100i16).to_be_bytes()); buf.extend_from_slice(&(-200i16).to_be_bytes()); buf.extend_from_slice(&800i16.to_be_bytes()); buf.extend_from_slice(&900i16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&8u16.to_be_bytes()); buf.extend_from_slice(&2i16.to_be_bytes()); buf.extend_from_slice(&1i16.to_be_bytes()); buf.extend_from_slice(&0i16.to_be_bytes()); let head_len = buf.len() - head_offset;
let hhea_offset = buf.len();
buf.extend_from_slice(&[0, 1, 0, 0]); buf.extend_from_slice(&800i16.to_be_bytes()); buf.extend_from_slice(&(-200i16).to_be_bytes()); buf.extend_from_slice(&0i16.to_be_bytes()); buf.extend_from_slice(&700u16.to_be_bytes()); buf.extend_from_slice(&0i16.to_be_bytes()); buf.extend_from_slice(&0i16.to_be_bytes()); buf.extend_from_slice(&700i16.to_be_bytes()); buf.extend_from_slice(&1i16.to_be_bytes()); buf.extend_from_slice(&0i16.to_be_bytes()); buf.extend_from_slice(&0i16.to_be_bytes()); buf.extend_from_slice(&[0; 8]); buf.extend_from_slice(&0i16.to_be_bytes()); buf.extend_from_slice(&3u16.to_be_bytes()); let hhea_len = buf.len() - hhea_offset;
let maxp_offset = buf.len();
buf.extend_from_slice(&[0, 0, 0x50, 0]); buf.extend_from_slice(&3u16.to_be_bytes()); let maxp_len = buf.len() - maxp_offset;
let hmtx_offset = buf.len();
buf.extend_from_slice(&500u16.to_be_bytes());
buf.extend_from_slice(&0i16.to_be_bytes());
buf.extend_from_slice(&250u16.to_be_bytes());
buf.extend_from_slice(&0i16.to_be_bytes());
buf.extend_from_slice(&700u16.to_be_bytes());
buf.extend_from_slice(&0i16.to_be_bytes());
let hmtx_len = buf.len() - hmtx_offset;
let cmap_offset = buf.len();
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&3u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&12u32.to_be_bytes());
let seg_count = 3u16; let seg_count_x2 = seg_count * 2;
buf.extend_from_slice(&4u16.to_be_bytes()); let subtable_len_pos = buf.len();
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&seg_count_x2.to_be_bytes()); buf.extend_from_slice(&4u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&2u16.to_be_bytes());
buf.extend_from_slice(&32u16.to_be_bytes());
buf.extend_from_slice(&65u16.to_be_bytes());
buf.extend_from_slice(&0xFFFFu16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&32u16.to_be_bytes());
buf.extend_from_slice(&65u16.to_be_bytes());
buf.extend_from_slice(&0xFFFFu16.to_be_bytes());
buf.extend_from_slice(&(-31i16).to_be_bytes());
buf.extend_from_slice(&(-63i16).to_be_bytes());
buf.extend_from_slice(&1i16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
let subtable_start = cmap_offset + 12; let subtable_len = (buf.len() - subtable_start) as u16;
buf[subtable_len_pos] = (subtable_len >> 8) as u8;
buf[subtable_len_pos + 1] = subtable_len as u8;
let cmap_len = buf.len() - cmap_offset;
let name_offset = buf.len();
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes());
let string_storage_offset = 18u16;
buf.extend_from_slice(&string_storage_offset.to_be_bytes());
let font_name_str = b"TestFont";
buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&(font_name_str.len() as u16).to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(font_name_str);
let name_len = buf.len() - name_offset;
let tables_info: [(&[u8; 4], usize, usize); 6] = [
(b"head", head_offset, head_len),
(b"hhea", hhea_offset, hhea_len),
(b"maxp", maxp_offset, maxp_len),
(b"hmtx", hmtx_offset, hmtx_len),
(b"cmap", cmap_offset, cmap_len),
(b"name", name_offset, name_len),
];
for (i, (tag, offset, length)) in tables_info.iter().enumerate() {
let dir_off = dir_start + i * 16;
buf[dir_off..dir_off + 4].copy_from_slice(*tag);
buf[dir_off + 4..dir_off + 8].copy_from_slice(&0u32.to_be_bytes()); buf[dir_off + 8..dir_off + 12].copy_from_slice(&(*offset as u32).to_be_bytes());
buf[dir_off + 12..dir_off + 16].copy_from_slice(&(*length as u32).to_be_bytes());
}
buf
}
#[test]
fn parse_ttf_offset_table() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.units_per_em, 1000);
}
#[test]
fn parse_ttf_head_bbox() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.bbox, [-100, -200, 800, 900]);
}
#[test]
fn parse_ttf_hhea_ascent_descent() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.pdf_metrics, FontVerticalMetrics::new(800, -200, 0));
assert_eq!(font.layout_metrics, FontVerticalMetrics::new(800, -200, 0));
}
#[test]
fn parse_ttf_cmap_format4() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.cmap.get(&32), Some(&1));
assert_eq!(font.cmap.get(&65), Some(&2));
assert_eq!(font.cmap.get(&90), None);
}
#[test]
fn parse_ttf_char_widths() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.glyph_widths.len(), 3);
assert_eq!(font.glyph_widths[0], 500); assert_eq!(font.glyph_widths[1], 250); assert_eq!(font.glyph_widths[2], 700); }
#[test]
fn parse_ttf_char_width_lookup() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.char_width(32), 250);
assert_eq!(font.char_width(65), 700);
assert_eq!(font.char_width(90), 500);
}
#[test]
fn parse_ttf_font_name() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.font_name, "TestFont");
}
#[test]
fn parse_ttf_char_width_scaled() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
let w = font.char_width_scaled(65, 12.0);
assert!((w - 8.4).abs() < 0.01);
}
#[test]
fn parse_ttf_char_width_pdf() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.char_width_pdf(65), 700);
assert_eq!(font.char_width_pdf(32), 250);
}
#[test]
fn parse_ttf_too_short() {
let data = vec![0; 4];
assert!(parse_ttf(data).is_err());
}
#[test]
fn parse_ttf_num_h_metrics() {
let data = build_test_ttf();
let font = parse_ttf(data).unwrap();
assert_eq!(font.num_h_metrics, 3);
}
#[test]
fn char_width_glyph_beyond_widths_falls_back_to_last() {
let font = TtfFont {
font_name: String::new(),
units_per_em: 1000,
bbox: [0; 4],
pdf_metrics: FontVerticalMetrics::new(0, 0, 0),
layout_metrics: FontVerticalMetrics::new(0, 0, 0),
cmap: {
let mut m = HashMap::new();
m.insert(65, 999); m
},
glyph_widths: vec![500, 700],
num_h_metrics: 2,
flags: 32,
data: vec![],
};
assert_eq!(font.char_width(65), 700); }
#[test]
fn char_width_empty_glyph_widths_returns_zero() {
let font = TtfFont {
font_name: String::new(),
units_per_em: 1000,
bbox: [0; 4],
pdf_metrics: FontVerticalMetrics::new(0, 0, 0),
layout_metrics: FontVerticalMetrics::new(0, 0, 0),
cmap: HashMap::new(),
glyph_widths: vec![],
num_h_metrics: 0,
flags: 32,
data: vec![],
};
assert_eq!(font.char_width(65), 0);
}
#[test]
fn parse_ttf_table_directory_too_short() {
let mut data = vec![0u8; 12];
data[4] = 0;
data[5] = 1; let err = parse_ttf(data).unwrap_err();
assert!(err.contains("too short for table directory"));
}
fn build_ttf_with_tables(tables: &[(&[u8; 4], &[u8])]) -> Vec<u8> {
let num_tables = tables.len() as u16;
let mut buf = Vec::new();
buf.extend_from_slice(&[0, 1, 0, 0]);
buf.extend_from_slice(&num_tables.to_be_bytes());
buf.extend_from_slice(&[0; 6]);
let dir_start = buf.len();
buf.resize(dir_start + tables.len() * 16, 0);
for (i, (tag, table_data)) in tables.iter().enumerate() {
let offset = buf.len();
buf.extend_from_slice(table_data);
let dir_off = dir_start + i * 16;
buf[dir_off..dir_off + 4].copy_from_slice(*tag);
buf[dir_off + 8..dir_off + 12].copy_from_slice(&(offset as u32).to_be_bytes());
buf[dir_off + 12..dir_off + 16]
.copy_from_slice(&(table_data.len() as u32).to_be_bytes());
}
buf
}
fn make_head_table(units_per_em: u16) -> Vec<u8> {
let mut t = vec![0u8; 54];
t[18] = (units_per_em >> 8) as u8;
t[19] = units_per_em as u8;
t
}
fn make_hhea_table(ascent: i16, descent: i16, num_h_metrics: u16) -> Vec<u8> {
let mut t = vec![0u8; 36];
t[4..6].copy_from_slice(&ascent.to_be_bytes());
t[6..8].copy_from_slice(&descent.to_be_bytes());
t[34..36].copy_from_slice(&num_h_metrics.to_be_bytes());
t
}
fn make_maxp_table(num_glyphs: u16) -> Vec<u8> {
let mut t = vec![0u8; 6];
t[4..6].copy_from_slice(&num_glyphs.to_be_bytes());
t
}
fn make_hmtx_table(widths: &[u16]) -> Vec<u8> {
let mut t = Vec::new();
for &w in widths {
t.extend_from_slice(&w.to_be_bytes());
t.extend_from_slice(&0i16.to_be_bytes()); }
t
}
fn make_cmap_format4(start: u16, end: u16, delta: i16) -> Vec<u8> {
let mut t = Vec::new();
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&1u16.to_be_bytes()); t.extend_from_slice(&3u16.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes());
t.extend_from_slice(&12u32.to_be_bytes());
let seg_count: u16 = 2; t.extend_from_slice(&4u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&(seg_count * 2).to_be_bytes());
t.extend_from_slice(&2u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&end.to_be_bytes());
t.extend_from_slice(&0xFFFFu16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&start.to_be_bytes());
t.extend_from_slice(&0xFFFFu16.to_be_bytes());
t.extend_from_slice(&delta.to_be_bytes());
t.extend_from_slice(&1i16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t
}
fn make_cmap_format4_with_range_offset(start: u16, end: u16, glyph_ids: &[u16]) -> Vec<u8> {
let mut t = Vec::new();
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&1u16.to_be_bytes()); t.extend_from_slice(&3u16.to_be_bytes()); t.extend_from_slice(&1u16.to_be_bytes()); t.extend_from_slice(&12u32.to_be_bytes());
let seg_count: u16 = 2; t.extend_from_slice(&4u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&(seg_count * 2).to_be_bytes());
t.extend_from_slice(&2u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&end.to_be_bytes());
t.extend_from_slice(&0xFFFFu16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&start.to_be_bytes());
t.extend_from_slice(&0xFFFFu16.to_be_bytes());
t.extend_from_slice(&0i16.to_be_bytes());
t.extend_from_slice(&1i16.to_be_bytes());
let range_offset = seg_count * 2; t.extend_from_slice(&range_offset.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
for &gid in glyph_ids {
t.extend_from_slice(&gid.to_be_bytes());
}
t
}
fn make_name_table_ascii(name_id: u16, name: &[u8]) -> Vec<u8> {
let mut t = Vec::new();
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&1u16.to_be_bytes()); let storage_offset: u16 = 6 + 12; t.extend_from_slice(&storage_offset.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&name_id.to_be_bytes());
t.extend_from_slice(&(name.len() as u16).to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(name);
t
}
fn make_name_table_utf16be(
name_id: u16,
platform_id: u16,
encoding_id: u16,
name: &str,
) -> Vec<u8> {
let mut t = Vec::new();
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&1u16.to_be_bytes()); let storage_offset: u16 = 6 + 12;
t.extend_from_slice(&storage_offset.to_be_bytes());
t.extend_from_slice(&platform_id.to_be_bytes());
t.extend_from_slice(&encoding_id.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&name_id.to_be_bytes());
let name_bytes: Vec<u8> = name.encode_utf16().flat_map(|c| c.to_be_bytes()).collect();
t.extend_from_slice(&(name_bytes.len() as u16).to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&name_bytes);
t
}
fn build_full_ttf(
head: &[u8],
hhea: &[u8],
maxp: &[u8],
hmtx: &[u8],
cmap: &[u8],
name: &[u8],
os2: Option<&[u8]>,
) -> Vec<u8> {
let mut table_list: Vec<(&[u8; 4], &[u8])> = vec![
(b"head", head),
(b"hhea", hhea),
(b"maxp", maxp),
(b"hmtx", hmtx),
(b"cmap", cmap),
(b"name", name),
];
if let Some(os2_data) = os2 {
table_list.push((b"OS/2", os2_data));
}
build_ttf_with_tables(&table_list)
}
#[test]
fn parse_ttf_head_too_short() {
let data = build_ttf_with_tables(&[(b"head", &[0u8; 10])]);
let err = parse_ttf(data).unwrap_err();
assert!(err.contains("head table too short") || err.contains("Missing"));
}
#[test]
fn parse_ttf_missing_hhea() {
let head = make_head_table(1000);
let data = build_ttf_with_tables(&[(b"head", &head)]);
let err = parse_ttf(data).unwrap_err();
assert!(err.contains("Missing hhea"));
}
#[test]
fn parse_ttf_hhea_too_short() {
let head = make_head_table(1000);
let data = build_ttf_with_tables(&[(b"head", &head), (b"hhea", &[0u8; 10])]);
let err = parse_ttf(data).unwrap_err();
assert!(err.contains("hhea table too short"));
}
#[test]
fn parse_ttf_uses_os2_typographic_metrics_for_layout() {
let head = make_head_table(1000);
let hhea = make_hhea_table(800, -200, 1);
let maxp = make_maxp_table(1);
let hmtx = make_hmtx_table(&[500]);
let cmap = make_cmap_format4(65, 65, -64); let name = make_name_table_ascii(1, b"Test");
let mut os2 = vec![0u8; 74];
os2[68..70].copy_from_slice(&900i16.to_be_bytes());
os2[70..72].copy_from_slice(&(-300i16).to_be_bytes());
os2[72..74].copy_from_slice(&50i16.to_be_bytes());
let data = build_full_ttf(&head, &hhea, &maxp, &hmtx, &cmap, &name, Some(&os2));
let font = parse_ttf(data).unwrap();
assert_eq!(font.pdf_metrics, FontVerticalMetrics::new(800, -200, 0));
assert_eq!(font.layout_metrics, FontVerticalMetrics::new(900, -300, 50));
}
#[test]
fn parse_ttf_os2_table_too_short_falls_back_to_hhea() {
let head = make_head_table(1000);
let hhea = make_hhea_table(800, -200, 1);
let maxp = make_maxp_table(1);
let hmtx = make_hmtx_table(&[500]);
let cmap = make_cmap_format4(65, 65, -64);
let name = make_name_table_ascii(1, b"Test");
let os2 = vec![0u8; 10]; let data = build_full_ttf(&head, &hhea, &maxp, &hmtx, &cmap, &name, Some(&os2));
let font = parse_ttf(data).unwrap();
assert_eq!(font.pdf_metrics, FontVerticalMetrics::new(800, -200, 0));
assert_eq!(font.layout_metrics, FontVerticalMetrics::new(800, -200, 0));
}
#[test]
fn parse_ttf_maxp_too_short() {
let head = make_head_table(1000);
let hhea = make_hhea_table(800, -200, 1);
let data =
build_ttf_with_tables(&[(b"head", &head), (b"hhea", &hhea), (b"maxp", &[0u8; 2])]);
let err = parse_ttf(data).unwrap_err();
assert!(err.contains("maxp table too short"));
}
#[test]
fn parse_ttf_hmtx_break_on_short_data() {
let head = make_head_table(1000);
let hhea = make_hhea_table(800, -200, 3); let maxp = make_maxp_table(3); let cmap = make_cmap_format4(65, 65, -64);
let name = make_name_table_ascii(1, b"Test");
let hmtx = make_hmtx_table(&[500]);
let tables: Vec<(&[u8; 4], &[u8])> = vec![
(b"head", &head),
(b"hhea", &hhea),
(b"maxp", &maxp),
(b"cmap", &cmap),
(b"name", &name),
(b"hmtx", &hmtx),
];
let mut data = build_ttf_with_tables(&tables);
let font = parse_ttf(data.clone()).unwrap();
assert_eq!(font.glyph_widths.len(), 1);
assert_eq!(font.glyph_widths[0], 500);
let hmtx_off = data.len() - 4;
data.truncate(hmtx_off); let font2 = parse_ttf(data).unwrap();
assert!(font2.glyph_widths.is_empty());
}
#[test]
fn parse_cmap_table_too_short() {
let result = parse_cmap(&[0u8; 2], 0);
assert!(result.unwrap_err().contains("cmap table too short"));
}
#[test]
fn parse_cmap_subtable_record_break() {
let mut data = vec![0u8; 100];
data[2] = 0;
data[3] = 2;
let _result = parse_cmap(&data[..11], 0);
let mut data2 = vec![0u8; 20];
data2[3] = 2; data2[4] = 0;
data2[5] = 5;
let result2 = parse_cmap(&data2[..15], 0);
assert!(result2.unwrap_err().contains("No suitable cmap subtable"));
}
#[test]
fn parse_cmap_subtable_too_short() {
let mut data = vec![0u8; 20];
data[3] = 1; data[4] = 0;
data[5] = 3;
data[6] = 0;
data[7] = 1;
let sub_off = 18u32; data[8..12].copy_from_slice(&sub_off.to_be_bytes());
let result = parse_cmap(&data[..19], 0);
assert!(result.unwrap_err().contains("cmap subtable too short"));
}
#[test]
fn parse_cmap_unsupported_format() {
let mut data = vec![0u8; 30];
data[3] = 1; data[5] = 3; data[7] = 1; let sub_off = 12u32;
data[8..12].copy_from_slice(&sub_off.to_be_bytes());
data[12] = 0;
data[13] = 6;
let result = parse_cmap(&data, 0).unwrap();
assert!(result.is_empty());
}
#[test]
fn parse_cmap_format0() {
let mut data = vec![0u8; 300];
data[3] = 1; data[5] = 3; data[7] = 1; let sub_off = 12u32;
data[8..12].copy_from_slice(&sub_off.to_be_bytes());
data[12] = 0;
data[13] = 0;
data[18 + 65] = 5; data[18 + 66] = 6; let result = parse_cmap(&data, 0).unwrap();
assert_eq!(result.get(&65), Some(&5));
assert_eq!(result.get(&66), Some(&6));
assert_eq!(result.get(&0), None); }
#[test]
fn parse_cmap_format0_too_short() {
let mut data = vec![0u8; 100];
data[3] = 1;
data[5] = 3;
data[7] = 1;
let sub_off = 12u32;
data[8..12].copy_from_slice(&sub_off.to_be_bytes());
data[12] = 0;
data[13] = 0; let result = parse_cmap(&data, 0);
assert!(
result
.unwrap_err()
.contains("cmap format 0 table too short")
);
}
#[test]
fn parse_cmap_format4_header_too_short() {
let result = parse_cmap_format4(&[0u8; 10], 0);
assert!(
result
.unwrap_err()
.contains("cmap format 4 header too short")
);
}
#[test]
fn parse_cmap_format4_data_too_short() {
let mut data = vec![0u8; 20];
data[0] = 0;
data[1] = 4;
data[6] = 0;
data[7] = 4; let result = parse_cmap_format4(&data, 0);
assert!(result.unwrap_err().contains("cmap format 4 data too short"));
}
#[test]
fn parse_cmap_format4_with_id_range_offset() {
let cmap_data = make_cmap_format4_with_range_offset(65, 67, &[10, 20, 0]);
let head = make_head_table(1000);
let hhea = make_hhea_table(800, -200, 21);
let maxp = make_maxp_table(21);
let hmtx = make_hmtx_table(&[
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 500, 500,
]);
let name = make_name_table_ascii(1, b"Test");
let data = build_full_ttf(&head, &hhea, &maxp, &hmtx, &cmap_data, &name, None);
let font = parse_ttf(data).unwrap();
assert_eq!(font.cmap.get(&65), Some(&10));
assert_eq!(font.cmap.get(&66), Some(&20));
assert_eq!(font.cmap.get(&67), None); }
#[test]
fn parse_cmap_format4_id_range_offset_out_of_bounds() {
let mut data = vec![0u8; 50];
data[0] = 0;
data[1] = 4;
data[6] = 0;
data[7] = 4;
let seg_count = 2usize;
let end_code_off = 14;
data[end_code_off] = 0;
data[end_code_off + 1] = 65; data[end_code_off + 2] = 0xFF;
data[end_code_off + 3] = 0xFF; let start_code_off = end_code_off + seg_count * 2 + 2;
data[start_code_off] = 0;
data[start_code_off + 1] = 65; data[start_code_off + 2] = 0xFF;
data[start_code_off + 3] = 0xFF;
let id_delta_off = start_code_off + seg_count * 2;
let id_range_off = id_delta_off + seg_count * 2;
data[id_range_off] = 0xFF;
data[id_range_off + 1] = 0xFE;
let result = parse_cmap_format4(&data, 0).unwrap();
assert_eq!(result.get(&65), None);
}
#[test]
fn parse_name_table_too_short() {
let result = parse_name_table(&[0u8; 4], 0);
assert!(result.unwrap_err().contains("name table too short"));
}
#[test]
fn parse_name_table_record_break() {
let mut data = vec![0u8; 12];
data[3] = 2; data[5] = 100; let result = parse_name_table(&data, 0);
assert!(result.unwrap_err().contains("No font name found"));
}
#[test]
fn parse_name_table_skips_non_name_ids() {
let mut t = Vec::new();
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&2u16.to_be_bytes()); let storage_offset: u16 = 6 + 24; t.extend_from_slice(&storage_offset.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&2u16.to_be_bytes()); t.extend_from_slice(&4u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&1u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes()); t.extend_from_slice(&4u16.to_be_bytes());
t.extend_from_slice(&4u16.to_be_bytes()); t.extend_from_slice(b"SkipGood");
let result = parse_name_table(&t, 0).unwrap();
assert_eq!(result, "Good");
}
#[test]
fn parse_name_table_priority_name_id_4_over_1() {
let mut t = Vec::new();
t.extend_from_slice(&0u16.to_be_bytes()); t.extend_from_slice(&2u16.to_be_bytes()); let storage_offset: u16 = 6 + 24;
t.extend_from_slice(&storage_offset.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&4u16.to_be_bytes()); t.extend_from_slice(&8u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes()); t.extend_from_slice(&6u16.to_be_bytes());
t.extend_from_slice(&8u16.to_be_bytes());
t.extend_from_slice(b"FullNameFamily");
let result = parse_name_table(&t, 0).unwrap();
assert_eq!(result, "FullName");
}
#[test]
fn parse_name_table_string_beyond_data() {
let mut t = Vec::new();
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes()); let storage_offset: u16 = 6 + 12;
t.extend_from_slice(&storage_offset.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&1u16.to_be_bytes());
t.extend_from_slice(&100u16.to_be_bytes()); t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(b"AB");
let result = parse_name_table(&t, 0);
assert!(result.unwrap_err().contains("No font name found"));
}
#[test]
fn parse_name_table_utf16be_windows_platform() {
let name = make_name_table_utf16be(1, 3, 1, "Hello");
let result = parse_name_table(&name, 0).unwrap();
assert_eq!(result, "Hello");
}
#[test]
fn parse_name_table_utf16be_unicode_platform_encoding_gt_0() {
let name = make_name_table_utf16be(1, 0, 1, "World");
let result = parse_name_table(&name, 0).unwrap();
assert_eq!(result, "World");
}
#[test]
fn decode_utf16be_basic() {
let data = [0x00, 0x48, 0x00, 0x69]; let result = decode_utf16be(&data);
assert_eq!(result, "Hi");
}
#[test]
fn decode_utf16be_empty() {
let result = decode_utf16be(&[]);
assert_eq!(result, "");
}
#[test]
fn decode_utf16be_odd_byte_ignored() {
let data = [0x00, 0x41, 0xFF]; let result = decode_utf16be(&data);
assert_eq!(result, "A");
}
#[test]
fn parse_cmap_unicode_platform_fallback() {
let mut data = vec![0u8; 300];
data[3] = 1; data[4] = 0;
data[5] = 0;
data[6] = 0;
data[7] = 0;
let sub_off = 12u32;
data[8..12].copy_from_slice(&sub_off.to_be_bytes());
data[12] = 0;
data[13] = 0;
data[18 + 65] = 3; let result = parse_cmap(&data, 0).unwrap();
assert_eq!(result.get(&65), Some(&3));
}
#[test]
fn parse_ttf_rejects_zero_units_per_em() {
let head = make_head_table(0); let hhea = make_hhea_table(800, -200, 1);
let maxp = make_maxp_table(1);
let hmtx = make_hmtx_table(&[500]);
let cmap = make_cmap_format4(65, 65, -64);
let name = make_name_table_ascii(1, b"Test");
let data = build_full_ttf(&head, &hhea, &maxp, &hmtx, &cmap, &name, None);
let err = parse_ttf(data).unwrap_err();
assert!(err.contains("units_per_em"));
}
#[test]
fn char_width_pdf_large_width_no_overflow() {
let font = TtfFont {
font_name: String::new(),
units_per_em: 1000,
bbox: [0; 4],
pdf_metrics: FontVerticalMetrics::new(0, 0, 0),
layout_metrics: FontVerticalMetrics::new(0, 0, 0),
cmap: {
let mut m = HashMap::new();
m.insert(65, 0);
m
},
glyph_widths: vec![u16::MAX], num_h_metrics: 1,
flags: 32,
data: vec![],
};
let w = font.char_width_pdf(65);
assert_eq!(w, 65535);
}
#[test]
fn char_width_scaled_zero_upm_returns_zero() {
let font = TtfFont {
font_name: String::new(),
units_per_em: 0,
bbox: [0; 4],
pdf_metrics: FontVerticalMetrics::new(0, 0, 0),
layout_metrics: FontVerticalMetrics::new(0, 0, 0),
cmap: {
let mut m = HashMap::new();
m.insert(65, 0);
m
},
glyph_widths: vec![500],
num_h_metrics: 1,
flags: 32,
data: vec![],
};
assert_eq!(font.char_width_scaled(65, 12.0), 0.0);
assert_eq!(font.char_width_pdf(65), 0);
}
#[test]
fn parse_ttf_glyphs_beyond_num_h_metrics_share_last_width() {
let head = make_head_table(1000);
let hhea = make_hhea_table(800, -200, 2); let maxp = make_maxp_table(4); let hmtx = make_hmtx_table(&[500, 700]);
let cmap = make_cmap_format4(65, 65, -64);
let name = make_name_table_ascii(1, b"Test");
let data = build_full_ttf(&head, &hhea, &maxp, &hmtx, &cmap, &name, None);
let font = parse_ttf(data).unwrap();
assert_eq!(font.glyph_widths.len(), 4);
assert_eq!(font.glyph_widths[0], 500);
assert_eq!(font.glyph_widths[1], 700);
assert_eq!(font.glyph_widths[2], 700); assert_eq!(font.glyph_widths[3], 700); }
}