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";
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 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 head_offset + 54 > data.len() {
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 maxp_offset + 6 > data.len() {
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>> {
let cmap_key: [u8; 4] = CMAP_TABLE;
let cmap_table = self
.tables
.get(&cmap_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing cmap table".to_string(),
})?;
let offset = cmap_table.offset as usize;
if offset + 4 > self.data.len() {
return Err(ParseError::SyntaxError {
position: offset,
message: "Cmap table too small".to_string(),
});
}
let _version = read_u16(&self.data, offset)?;
let num_subtables = read_u16(&self.data, offset + 2)?;
let mut subtables = Vec::new();
let mut subtable_offset = offset + 4;
for _ in 0..num_subtables {
if subtable_offset + 8 > self.data.len() {
break;
}
let platform_id = read_u16(&self.data, subtable_offset)?;
let encoding_id = read_u16(&self.data, subtable_offset + 2)?;
let offset = read_u32(&self.data, subtable_offset + 4)? as usize;
if let Ok(subtable) = self.parse_cmap_subtable(offset, platform_id, encoding_id) {
subtables.push(subtable);
}
subtable_offset += 8;
}
Ok(subtables)
}
fn parse_cmap_subtable(
&self,
subtable_offset: usize,
platform_id: u16,
encoding_id: u16,
) -> ParseResult<CmapSubtable> {
let cmap_key: [u8; 4] = CMAP_TABLE;
let cmap_table = self
.tables
.get(&cmap_key)
.ok_or_else(|| ParseError::SyntaxError {
position: 0,
message: "Missing cmap table".to_string(),
})?;
let absolute_offset = cmap_table.offset as usize + subtable_offset;
if absolute_offset + 6 > self.data.len() {
return Err(ParseError::SyntaxError {
position: absolute_offset,
message: "Cmap subtable extends beyond file".to_string(),
});
}
let format = read_u16(&self.data, absolute_offset)?;
let mut mappings = HashMap::new();
match format {
0 => {
if absolute_offset + 262 > self.data.len() {
return Err(ParseError::SyntaxError {
position: absolute_offset,
message: "Format 0 cmap subtable too small".to_string(),
});
}
for i in 0..256 {
let glyph_id = self.data[absolute_offset + 6 + i] as u16;
if glyph_id != 0 {
mappings.insert(i as u32, glyph_id);
}
}
}
4 => {
let length = read_u16(&self.data, absolute_offset + 2)? as usize;
if absolute_offset + length > self.data.len() {
return Err(ParseError::SyntaxError {
position: absolute_offset,
message: "Format 4 cmap subtable extends beyond file".to_string(),
});
}
let seg_count_x2 = read_u16(&self.data, absolute_offset + 6)? as usize;
let seg_count = seg_count_x2 / 2;
let end_codes_offset = absolute_offset + 14;
let start_codes_offset = end_codes_offset + seg_count_x2 + 2;
let id_deltas_offset = start_codes_offset + seg_count_x2;
let id_range_offsets_offset = id_deltas_offset + seg_count_x2;
for i in 0..seg_count {
let end_code = read_u16(&self.data, end_codes_offset + i * 2)?;
let start_code = read_u16(&self.data, start_codes_offset + i * 2)?;
let id_delta = read_i16(&self.data, id_deltas_offset + i * 2)?;
let id_range_offset = read_u16(&self.data, id_range_offsets_offset + i * 2)?;
if end_code == 0xFFFF {
break;
}
for code in start_code..=end_code {
let glyph_id = if id_range_offset == 0 {
((code as i32 + id_delta as i32) & 0xFFFF) as u16
} else {
let glyph_index_offset = id_range_offsets_offset
+ i * 2
+ id_range_offset as usize
+ 2 * (code - start_code) as usize;
if glyph_index_offset + 2 <= self.data.len() {
let glyph_id = read_u16(&self.data, glyph_index_offset)?;
if glyph_id != 0 {
((glyph_id as i32 + id_delta as i32) & 0xFFFF) as u16
} else {
0
}
} else {
0
}
};
if glyph_id != 0 {
mappings.insert(code as u32, glyph_id);
}
}
}
}
6 => {
let _length = read_u16(&self.data, absolute_offset + 2)?;
let first_code = read_u16(&self.data, absolute_offset + 6)?;
let entry_count = read_u16(&self.data, absolute_offset + 8)?;
let mut glyph_offset = absolute_offset + 10;
for i in 0..entry_count {
if glyph_offset + 2 > self.data.len() {
break;
}
let glyph_id = read_u16(&self.data, glyph_offset)?;
if glyph_id != 0 {
mappings.insert((first_code + i) as u32, glyph_id);
}
glyph_offset += 2;
}
}
12 => {
let _length = read_u32(&self.data, absolute_offset + 4)?;
let num_groups = read_u32(&self.data, absolute_offset + 12)?;
let mut group_offset = absolute_offset + 16;
for _ in 0..num_groups {
if group_offset + 12 > self.data.len() {
break;
}
let start_char_code = read_u32(&self.data, group_offset)?;
let end_char_code = read_u32(&self.data, group_offset + 4)?;
let start_glyph_id = read_u32(&self.data, group_offset + 8)?;
for i in 0..=(end_char_code - start_char_code) {
let char_code = start_char_code + i;
let glyph_id = (start_glyph_id + i) as u16;
if glyph_id != 0 && glyph_id < self.num_glyphs {
mappings.insert(char_code, glyph_id);
}
}
group_offset += 12;
}
}
_ => {
return Err(ParseError::SyntaxError {
position: absolute_offset,
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 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 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 read_u16(data: &[u8], offset: usize) -> ParseResult<u16> {
if offset + 2 > data.len() {
return Err(ParseError::SyntaxError {
position: offset,
message: "Insufficient data for u16".to_string(),
});
}
Ok(((data[offset] as u16) << 8) | (data[offset + 1] as u16))
}
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> {
if offset + 4 > data.len() {
return Err(ParseError::SyntaxError {
position: offset,
message: "Insufficient data for u32".to_string(),
});
}
Ok(((data[offset] as u32) << 24)
| ((data[offset + 1] as u32) << 16)
| ((data[offset + 2] as u32) << 8)
| (data[offset + 3] as u32))
}
#[cfg(test)]
mod tests {
use super::*;
#[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 mut font_data = vec![
0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let tables = [
(b"cmap", 32 + 7 * 16, 266), (b"head", 32 + 7 * 16 + 266, 54),
(b"glyf", 32 + 7 * 16 + 266 + 54, 0),
(b"loca", 32 + 7 * 16 + 266 + 54, 0),
(b"maxp", 32 + 7 * 16 + 266 + 54, 6),
(b"hhea", 32 + 7 * 16 + 266 + 54 + 6, 36),
(b"hmtx", 32 + 7 * 16 + 266 + 54 + 6 + 36, 0),
];
for (tag, offset, length) in &tables {
font_data.extend(*tag);
font_data.extend(&[0, 0, 0, 0]); font_data.extend(&(*offset as u32).to_be_bytes());
font_data.extend(&(*length as u32).to_be_bytes());
}
while font_data.len() < tables[0].1 {
font_data.push(0);
}
font_data.extend(&[
0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, ]);
for i in 0..=255u8 {
font_data.push(i);
}
let tables = vec![
(HEAD_TABLE, 36 + 18 + 32), (GLYF_TABLE, 0),
(LOCA_TABLE, 0),
(MAXP_TABLE, 6),
(HHEA_TABLE, 36),
(HMTX_TABLE, 0),
];
font_data[4] = 0x00;
font_data[5] = tables.len() as u8 + 1;
let mut offset = 32 + 266; for (table, min_size) in &tables {
font_data.extend(*table);
font_data.extend(&[0, 0, 0, 0]); font_data.extend(&(offset as u32).to_be_bytes()); font_data.extend(&(*min_size as u32).to_be_bytes());
let _table_start = font_data.len();
while font_data.len() < offset {
font_data.push(0);
}
match *table {
HEAD_TABLE => {
while font_data.len() < offset + *min_size {
font_data.push(0);
}
if offset + 19 < font_data.len() {
font_data[offset + 18] = 0x04;
font_data[offset + 19] = 0x00; }
if offset + 51 < font_data.len() {
font_data[offset + 50] = 0x00;
font_data[offset + 51] = 0x00; }
}
MAXP_TABLE => {
while font_data.len() < offset + *min_size {
font_data.push(0);
}
if offset + 5 < font_data.len() {
font_data[offset + 4] = 0x01;
font_data[offset + 5] = 0x00; }
}
HHEA_TABLE => {
while font_data.len() < offset + *min_size {
font_data.push(0);
}
if offset + 35 < font_data.len() {
font_data[offset + 34] = 0x01;
font_data[offset + 35] = 0x00; }
}
_ => {}
}
offset += min_size;
}
while font_data.len() < offset {
font_data.push(0);
}
let font = TrueTypeFont::parse(font_data).expect("Failed to parse test font");
let subtables = font.parse_cmap().unwrap();
assert!(subtables.len() <= 1, "Should have 0 or 1 subtables");
if !subtables.is_empty() {
assert_eq!(subtables[0].format, 0);
}
}
#[test]
fn test_glyph_metrics() {
}
#[test]
fn test_font_name_parsing() {
}
#[test]
fn test_subset_creation() {
let glyphs = HashSet::from([0, 1, 2, 3]);
assert_eq!(glyphs.len(), 4);
}
#[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"
);
}
}