use byteorder::{BigEndian, ReadBytesExt};
use std::collections::HashMap;
use std::io::Cursor;
#[rustfmt::skip]
const MAC_ROMAN_HIGH: [char; 128] = [
'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á',
'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è',
'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó',
'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü',
'†', '°', '¢', '£', '§', '•', '¶', 'ß',
'®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø',
'∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑',
'∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø',
'¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«',
'»', '…', '\u{00A0}', 'À', 'Ã', 'Õ', 'Œ', 'œ',
'–', '—', '"', '"', '\'', '\'', '÷', '◊',
'ÿ', 'Ÿ', '⁄', '€', '‹', '›', 'fi', 'fl',
'‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á',
'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô',
'\u{F8FF}', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜',
'¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ',
];
#[inline]
fn mac_roman_to_unicode(byte: u8) -> char {
debug_assert!(byte >= 0x80);
MAC_ROMAN_HIGH[(byte - 0x80) as usize]
}
#[derive(Debug, Clone)]
pub struct TrueTypeCMap {
gid_to_unicode: HashMap<u16, char>,
symbol_code_to_gid: HashMap<u32, u16>,
}
impl TrueTypeCMap {
pub fn from_font_data(data: &[u8]) -> Result<Self, String> {
let mut cursor = Cursor::new(data);
let (num_tables, search_range, entry_selector, range_shift) =
Self::parse_sfnt_header(&mut cursor)?;
let cmap_offset = Self::find_cmap_table(
&mut cursor,
num_tables,
search_range,
entry_selector,
range_shift,
)?;
cursor.set_position(cmap_offset as u64);
let cmap_version = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read cmap version: {}", e))?;
if cmap_version != 0 {
return Err(format!("Unsupported cmap table version: {}", cmap_version));
}
let num_subtables = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read cmap subtable count: {}", e))?;
let mut best_subtable: Option<(u32, u32, u32)> = None;
let mut best_priority = -1i32;
for _ in 0..num_subtables {
let platform_id = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read platform ID: {}", e))?;
let encoding_id = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read encoding ID: {}", e))?;
let offset = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read subtable offset: {}", e))?;
let priority = match (platform_id, encoding_id) {
(3, 10) => 30, (3, 1) => 20, (0, 3) => 10, _ => 0,
};
if priority > best_priority {
best_priority = priority;
best_subtable = Some((platform_id as u32, encoding_id as u32, offset));
}
}
let (platform_id, encoding_id, subtable_offset) =
best_subtable.ok_or_else(|| "No suitable cmap subtable found".to_string())?;
tracing::debug!(
"TrueType cmap: selected platform={} encoding={} offset={}",
platform_id,
encoding_id,
subtable_offset
);
cursor.set_position((cmap_offset + subtable_offset) as u64);
let gid_to_unicode = Self::parse_cmap_subtable(&mut cursor)?;
Ok(TrueTypeCMap {
gid_to_unicode,
symbol_code_to_gid: Self::build_symbol_code_to_gid(data),
})
}
fn build_symbol_code_to_gid(data: &[u8]) -> HashMap<u32, u16> {
use skrifa::raw::{TableProvider, tables::cmap::PlatformId};
let mut map = HashMap::new();
let Ok(font) = skrifa::raw::FontRef::new(data) else {
return map;
};
let Ok(cmap) = font.cmap() else {
return map;
};
let mut chosen = None;
let mut best = 0;
for record in cmap.encoding_records() {
let priority = match (record.platform_id(), record.encoding_id()) {
(PlatformId::Windows, 0) => 2,
(PlatformId::Macintosh, 0) => 1,
_ => 0,
};
if priority > best
&& let Ok(subtable) = record.subtable(cmap.offset_data())
{
best = priority;
chosen = Some(subtable);
}
}
if let Some(subtable) = chosen {
for code in 0u32..256 {
if let Some(gid) = subtable
.map_codepoint(code)
.or_else(|| subtable.map_codepoint(0xF000 | code))
{
map.insert(code, gid.to_u32() as u16);
}
}
}
map
}
pub fn code_to_gid(&self, code: u16) -> Option<u16> {
self.symbol_code_to_gid.get(&(code as u32)).copied()
}
pub fn get_unicode(&self, gid: u16) -> Option<char> {
self.gid_to_unicode.get(&gid).copied()
}
pub fn len(&self) -> usize {
self.gid_to_unicode.len()
}
pub fn is_empty(&self) -> bool {
self.gid_to_unicode.is_empty()
}
fn parse_sfnt_header(cursor: &mut Cursor<&[u8]>) -> Result<(u16, u16, u16, u16), String> {
let version = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read sfnt version: {}", e))?;
if version != 0x00010000 && version != 0x4F54544F && version != 0x74727565 {
return Err(format!("Invalid sfnt version: 0x{:08X}", version));
}
let num_tables = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read table count: {}", e))?;
let search_range = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read search range: {}", e))?;
let entry_selector = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read entry selector: {}", e))?;
let range_shift = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read range shift: {}", e))?;
Ok((num_tables, search_range, entry_selector, range_shift))
}
fn find_cmap_table(
cursor: &mut Cursor<&[u8]>,
num_tables: u16,
_search_range: u16,
_entry_selector: u16,
_range_shift: u16,
) -> Result<u32, String> {
const CMAP_TAG: u32 = 0x636D6170;
for _ in 0..num_tables {
let tag = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read table tag: {}", e))?;
let _checksum = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read table checksum: {}", e))?;
let offset = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read table offset: {}", e))?;
let _length = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read table length: {}", e))?;
if tag == CMAP_TAG {
return Ok(offset);
}
}
Err("cmap table not found in font".to_string())
}
fn parse_cmap_subtable(cursor: &mut Cursor<&[u8]>) -> Result<HashMap<u16, char>, String> {
let format = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read cmap format: {}", e))?;
match format {
0 => Self::parse_cmap_format0(cursor),
4 => Self::parse_cmap_format4(cursor),
6 => Self::parse_cmap_format6(cursor),
12 => Self::parse_cmap_format12(cursor),
_ => Err(format!("Unsupported cmap format: {}", format)),
}
}
fn parse_cmap_format0(cursor: &mut Cursor<&[u8]>) -> Result<HashMap<u16, char>, String> {
let _length = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read format 0 length: {}", e))?;
let _language = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read format 0 language: {}", e))?;
let mut glyph_ids = [0u8; 256];
std::io::Read::read_exact(cursor, &mut glyph_ids)
.map_err(|e| format!("Failed to read format 0 glyphIdArray: {}", e))?;
let mut gid_to_unicode = HashMap::new();
for (byte, &gid) in glyph_ids.iter().enumerate() {
if gid == 0 {
continue;
}
let ch = if byte < 0x80 {
char::from_u32(byte as u32)
} else {
Some(mac_roman_to_unicode(byte as u8))
};
if let Some(ch) = ch {
gid_to_unicode.insert(gid as u16, ch);
}
}
Ok(gid_to_unicode)
}
fn parse_cmap_format4(cursor: &mut Cursor<&[u8]>) -> Result<HashMap<u16, char>, String> {
let _length = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read format 4 length: {}", e))?
as u32;
let _language = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read format 4 language: {}", e))?;
let seg_count_x2 = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read segCountX2: {}", e))?
as usize;
let seg_count = seg_count_x2 / 2;
let _search_range = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read searchRange: {}", e))?;
let _entry_selector = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read entrySelector: {}", e))?;
let _range_shift = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read rangeShift: {}", e))?;
let mut end_codes = vec![0u16; seg_count];
for i in 0..seg_count {
end_codes[i] = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read endCode[{}]: {}", i, e))?;
}
let _reserved = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read reserved pad: {}", e))?;
let mut start_codes = vec![0u16; seg_count];
for i in 0..seg_count {
start_codes[i] = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read startCode[{}]: {}", i, e))?;
}
let mut id_deltas = vec![0i16; seg_count];
for i in 0..seg_count {
id_deltas[i] = cursor
.read_i16::<BigEndian>()
.map_err(|e| format!("Failed to read idDelta[{}]: {}", i, e))?;
}
let mut id_range_offsets = vec![0u16; seg_count];
for i in 0..seg_count {
id_range_offsets[i] = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read idRangeOffset[{}]: {}", i, e))?;
}
let mut glyph_id_array = Vec::new();
while let Ok(val) = cursor.read_u16::<BigEndian>() {
glyph_id_array.push(val);
}
let mut gid_to_unicode = HashMap::new();
for seg in 0..seg_count {
let start = start_codes[seg] as u32;
let end = end_codes[seg] as u32;
let id_delta = id_deltas[seg] as i32;
for char_code in start..=end {
if char_code == 0xFFFF {
break; }
let gid = if id_range_offsets[seg] == 0 {
(char_code as i32 + id_delta) as u16
} else {
let offset = (id_range_offsets[seg] as usize) / 2
+ (char_code as usize - start as usize)
+ seg
- seg_count;
if offset < glyph_id_array.len() {
let raw = glyph_id_array[offset];
if raw != 0 {
(raw as i32 + id_delta) as u16
} else {
0
}
} else {
0
}
};
if gid != 0
&& let Some(ch) = char::from_u32(char_code)
{
gid_to_unicode.insert(gid, ch);
}
}
}
Ok(gid_to_unicode)
}
fn parse_cmap_format6(cursor: &mut Cursor<&[u8]>) -> Result<HashMap<u16, char>, String> {
let _length = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read format 6 length: {}", e))?;
let _language = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read format 6 language: {}", e))?;
let first_code = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read firstCode: {}", e))?;
let count = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read entryCount: {}", e))? as usize;
let mut gid_to_unicode = HashMap::new();
for i in 0..count {
let gid = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read glyphId[{}]: {}", i, e))?;
let char_code = first_code as u32 + i as u32;
if let Some(ch) = char::from_u32(char_code) {
gid_to_unicode.insert(gid, ch);
}
}
Ok(gid_to_unicode)
}
fn parse_cmap_format12(cursor: &mut Cursor<&[u8]>) -> Result<HashMap<u16, char>, String> {
let _reserved = cursor
.read_u16::<BigEndian>()
.map_err(|e| format!("Failed to read reserved: {}", e))?;
let _length = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read format 12 length: {}", e))?;
let _language = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read format 12 language: {}", e))?;
let num_groups = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read numGroups: {}", e))?
as usize;
let mut gid_to_unicode = HashMap::new();
for _ in 0..num_groups {
let start_char_code = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read startCharCode: {}", e))?;
let end_char_code = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read endCharCode: {}", e))?;
let start_gid = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Failed to read startGlyphId: {}", e))?;
for (offset, char_code) in (start_char_code..=end_char_code).enumerate() {
let gid = (start_gid + offset as u32) as u16;
if let Some(ch) = char::from_u32(char_code) {
gid_to_unicode.insert(gid, ch);
}
}
}
Ok(gid_to_unicode)
}
}
#[cfg(test)]
mod tests {
use super::*;
use byteorder::{BigEndian, WriteBytesExt};
fn build_truetype_with_cmap_format4(mappings: &[(u16, u16)]) -> Vec<u8> {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
let subtable_offset: u32 = 4 + 8;
data.write_u16::<BigEndian>(3).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u32::<BigEndian>(subtable_offset).unwrap();
let mut segments: Vec<(u16, u16, i16)> = Vec::new();
for &(char_code, gid) in mappings {
let delta = gid as i16 - char_code as i16;
segments.push((char_code, char_code, delta));
}
segments.push((0xFFFF, 0xFFFF, 1));
let seg_count = segments.len();
let seg_count_x2 = (seg_count * 2) as u16;
data.write_u16::<BigEndian>(4).unwrap();
let length_pos = data.len();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(seg_count_x2).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
for seg in &segments {
data.write_u16::<BigEndian>(seg.1).unwrap();
}
data.write_u16::<BigEndian>(0).unwrap();
for seg in &segments {
data.write_u16::<BigEndian>(seg.0).unwrap();
}
for seg in &segments {
data.write_i16::<BigEndian>(seg.2).unwrap();
}
for _ in &segments {
data.write_u16::<BigEndian>(0).unwrap();
}
let fmt4_start = length_pos - 2;
let fmt4_len = data.len() - fmt4_start;
let len_bytes = (fmt4_len as u16).to_be_bytes();
data[length_pos] = len_bytes[0];
data[length_pos + 1] = len_bytes[1];
data
}
fn build_truetype_with_cmap_format0(glyph_ids: [u8; 256]) -> Vec<u8> {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
let subtable_offset: u32 = 4 + 8;
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(subtable_offset).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(262).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.extend_from_slice(&glyph_ids);
data
}
fn build_truetype_with_cmap_format6(first_code: u16, gids: &[u16]) -> Vec<u8> {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(3).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u32::<BigEndian>(4 + 8).unwrap();
data.write_u16::<BigEndian>(6).unwrap();
data.write_u16::<BigEndian>((10 + gids.len() * 2) as u16)
.unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(first_code).unwrap();
data.write_u16::<BigEndian>(gids.len() as u16).unwrap();
for &gid in gids {
data.write_u16::<BigEndian>(gid).unwrap();
}
data
}
fn build_truetype_with_cmap_format12(groups: &[(u32, u32, u32)]) -> Vec<u8> {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(3).unwrap();
data.write_u16::<BigEndian>(10).unwrap();
data.write_u32::<BigEndian>(4 + 8).unwrap();
data.write_u16::<BigEndian>(12).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>((16 + groups.len() * 12) as u32)
.unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(groups.len() as u32).unwrap();
for &(start, end, start_gid) in groups {
data.write_u32::<BigEndian>(start).unwrap();
data.write_u32::<BigEndian>(end).unwrap();
data.write_u32::<BigEndian>(start_gid).unwrap();
}
data
}
fn fmt4_subtable(mappings: &[(u16, u16)]) -> Vec<u8> {
let mut segs: Vec<(u16, u16, i16)> = mappings
.iter()
.map(|&(c, g)| (c, c, (g as i32 - c as i32) as i16))
.collect();
segs.push((0xFFFF, 0xFFFF, 1));
let mut s = Vec::new();
s.write_u16::<BigEndian>(4).unwrap();
let len_pos = s.len();
s.write_u16::<BigEndian>(0).unwrap();
s.write_u16::<BigEndian>(0).unwrap();
s.write_u16::<BigEndian>((segs.len() * 2) as u16).unwrap();
for _ in 0..3 {
s.write_u16::<BigEndian>(0).unwrap();
}
for seg in &segs {
s.write_u16::<BigEndian>(seg.1).unwrap();
}
s.write_u16::<BigEndian>(0).unwrap();
for seg in &segs {
s.write_u16::<BigEndian>(seg.0).unwrap();
}
for seg in &segs {
s.write_i16::<BigEndian>(seg.2).unwrap();
}
for _ in &segs {
s.write_u16::<BigEndian>(0).unwrap();
}
let len = (s.len() as u16).to_be_bytes();
s[len_pos] = len[0];
s[len_pos + 1] = len[1];
s
}
fn cmap_two_subtables(unicode: &[(u16, u16)], symbol: &[(u16, u16)]) -> Vec<u8> {
let body_uni = fmt4_subtable(unicode);
let body_sym = fmt4_subtable(symbol);
let off_uni = 4 + 2 * 8;
let off_sym = off_uni + body_uni.len();
let mut t = Vec::new();
t.write_u16::<BigEndian>(0).unwrap();
t.write_u16::<BigEndian>(2).unwrap();
for (plat, enc, off) in [(3u16, 1u16, off_uni), (3, 0, off_sym)] {
t.write_u16::<BigEndian>(plat).unwrap();
t.write_u16::<BigEndian>(enc).unwrap();
t.write_u32::<BigEndian>(off as u32).unwrap();
}
t.extend(body_uni);
t.extend(body_sym);
t
}
fn assemble(mut tables: Vec<(u32, Vec<u8>)>) -> Vec<u8> {
tables.sort_by_key(|(tag, _)| *tag);
let mut out = Vec::new();
out.write_u32::<BigEndian>(0x0001_0000).unwrap();
out.write_u16::<BigEndian>(tables.len() as u16).unwrap();
for _ in 0..3 {
out.write_u16::<BigEndian>(0).unwrap();
}
let mut offset = 12 + tables.len() * 16;
for (tag, data) in &tables {
out.write_u32::<BigEndian>(*tag).unwrap();
out.write_u32::<BigEndian>(0).unwrap();
out.write_u32::<BigEndian>(offset as u32).unwrap();
out.write_u32::<BigEndian>(data.len() as u32).unwrap();
offset += (data.len() + 3) & !3;
}
for (_, data) in &tables {
out.extend_from_slice(data);
while out.len() % 4 != 0 {
out.push(0);
}
}
out
}
#[test]
fn symbolic_byte_resolves_via_symbol_cmap() {
let mut head = Vec::new();
head.write_u16::<BigEndian>(1).unwrap();
head.write_u16::<BigEndian>(0).unwrap();
head.write_u32::<BigEndian>(0).unwrap();
head.write_u32::<BigEndian>(0).unwrap();
head.write_u32::<BigEndian>(0x5F0F_3CF5).unwrap();
head.write_u16::<BigEndian>(0).unwrap();
head.write_u16::<BigEndian>(1000).unwrap();
head.write_u64::<BigEndian>(0).unwrap();
head.write_u64::<BigEndian>(0).unwrap();
for v in [0i16, 0, 1000, 1000, 0, 0, 0, 0, 0] {
head.write_i16::<BigEndian>(v).unwrap();
}
assert_eq!(head.len(), 54);
let mut hhea = Vec::new();
hhea.write_u16::<BigEndian>(1).unwrap();
hhea.write_u16::<BigEndian>(0).unwrap();
for v in [800i16, -200, 0, 1000, 0, 0, 1000, 1, 0, 0, 0, 0, 0, 0, 0] {
hhea.write_i16::<BigEndian>(v).unwrap();
}
hhea.write_u16::<BigEndian>(1).unwrap();
assert_eq!(hhea.len(), 36);
let mut maxp = Vec::new();
maxp.write_u32::<BigEndian>(0x0000_5000).unwrap();
maxp.write_u16::<BigEndian>(4).unwrap();
let cmap = cmap_two_subtables(&[(0x00C7, 2), (0x00CA, 3)], &[(3, 2)]);
let font = assemble(vec![
(0x6865_6164, head),
(0x6868_6561, hhea),
(0x6D61_7870, maxp),
(0x636D_6170, cmap),
]);
let parsed = TrueTypeCMap::from_font_data(&font).expect("parse synthetic font");
assert_eq!(parsed.code_to_gid(3), Some(2)); assert_eq!(parsed.get_unicode(2), Some('Ç')); assert_eq!(parsed.get_unicode(3), Some('Ê')); assert_eq!(parsed.code_to_gid(99), None);
}
#[test]
fn test_sfnt_header_parsing() {
let data = build_truetype_with_cmap_format4(&[]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert!(cmap.is_empty());
}
#[test]
fn test_invalid_sfnt_version() {
let mut data = vec![0u8; 100];
data[0] = 0xFF;
data[1] = 0xFF;
data[2] = 0xFF;
data[3] = 0xFF;
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid sfnt version"));
}
#[test]
fn test_opentype_version_accepted() {
let mut data = build_truetype_with_cmap_format4(&[(65, 1)]);
data[0] = 0x4F;
data[1] = 0x54;
data[2] = 0x54;
data[3] = 0x4F;
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_ok());
}
#[test]
fn test_apple_truetype_version_accepted() {
let mut data = build_truetype_with_cmap_format4(&[(65, 1)]);
data[0] = 0x74;
data[1] = 0x72;
data[2] = 0x75;
data[3] = 0x65;
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_ok());
}
#[test]
fn test_no_cmap_table() {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(0x68656164).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(28).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("cmap table not found"));
}
#[test]
fn test_format4_basic_ascii() {
let data = build_truetype_with_cmap_format4(&[(65, 1), (66, 2), (67, 3)]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.len(), 3);
assert_eq!(cmap.get_unicode(1), Some('A'));
assert_eq!(cmap.get_unicode(2), Some('B'));
assert_eq!(cmap.get_unicode(3), Some('C'));
assert_eq!(cmap.get_unicode(4), None);
}
#[test]
fn test_format4_extended_unicode() {
let data = build_truetype_with_cmap_format4(&[(233, 10), (241, 11)]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.get_unicode(10), Some('é'));
assert_eq!(cmap.get_unicode(11), Some('ñ'));
}
#[test]
fn test_format6_basic() {
let data = build_truetype_with_cmap_format6(65, &[1, 2, 3]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.len(), 3);
assert_eq!(cmap.get_unicode(1), Some('A'));
assert_eq!(cmap.get_unicode(2), Some('B'));
assert_eq!(cmap.get_unicode(3), Some('C'));
}
#[test]
fn test_format6_non_zero_first_code() {
let data = build_truetype_with_cmap_format6(48, &[10, 11, 12]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.get_unicode(10), Some('0'));
assert_eq!(cmap.get_unicode(11), Some('1'));
assert_eq!(cmap.get_unicode(12), Some('2'));
}
#[test]
fn test_format12_basic() {
let data = build_truetype_with_cmap_format12(&[(65, 67, 1)]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.len(), 3);
assert_eq!(cmap.get_unicode(1), Some('A'));
assert_eq!(cmap.get_unicode(2), Some('B'));
assert_eq!(cmap.get_unicode(3), Some('C'));
}
#[test]
fn test_format12_multiple_groups() {
let data = build_truetype_with_cmap_format12(&[(65, 67, 1), (48, 50, 10)]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.len(), 6);
assert_eq!(cmap.get_unicode(1), Some('A'));
assert_eq!(cmap.get_unicode(10), Some('0'));
assert_eq!(cmap.get_unicode(12), Some('2'));
}
#[test]
fn test_get_unicode_missing() {
let data = build_truetype_with_cmap_format4(&[(65, 1)]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.get_unicode(999), None);
}
#[test]
fn test_len_and_is_empty() {
let data_empty = build_truetype_with_cmap_format4(&[]);
let cmap_empty = TrueTypeCMap::from_font_data(&data_empty).unwrap();
assert_eq!(cmap_empty.len(), 0);
assert!(cmap_empty.is_empty());
let data_one = build_truetype_with_cmap_format4(&[(65, 1)]);
let cmap_one = TrueTypeCMap::from_font_data(&data_one).unwrap();
assert_eq!(cmap_one.len(), 1);
assert!(!cmap_one.is_empty());
}
#[test]
fn test_cmap_format0_byte_indexed() {
let mut gids = [0u8; 256];
gids[0x41] = 10;
gids[0x42] = 11;
gids[0x7A] = 50;
let data = build_truetype_with_cmap_format0(gids);
let cmap = TrueTypeCMap::from_font_data(&data).expect("format 0 parse");
assert_eq!(cmap.get_unicode(10), Some('A'));
assert_eq!(cmap.get_unicode(11), Some('B'));
assert_eq!(cmap.get_unicode(50), Some('z'));
assert_eq!(cmap.get_unicode(99), None);
}
#[test]
fn test_cmap_format0_mac_roman_high_half() {
let mut gids = [0u8; 256];
gids[0x41] = 10;
gids[0x8A] = 20;
gids[0xA5] = 30;
let data = build_truetype_with_cmap_format0(gids);
let cmap = TrueTypeCMap::from_font_data(&data).expect("format 0 parse");
assert_eq!(cmap.get_unicode(10), Some('A'));
assert_eq!(
cmap.get_unicode(20),
Some('ä'),
"Mac Roman 0x8A must map to U+00E4 (a-umlaut), not raw 0x8A"
);
assert_eq!(cmap.get_unicode(30), Some('•'));
}
#[test]
fn test_cmap_format0_rejects_truncated() {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(4 + 8).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(262).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.extend_from_slice(&[0u8; 8]);
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_err());
}
#[test]
fn test_unsupported_cmap_format() {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(3).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u32::<BigEndian>(4 + 8).unwrap();
data.write_u16::<BigEndian>(2).unwrap();
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unsupported cmap format"));
}
#[test]
fn test_unsupported_cmap_version() {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(99).unwrap();
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("Unsupported cmap table version")
);
}
#[test]
fn test_no_suitable_subtable() {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("No suitable cmap subtable"));
}
#[test]
fn test_truncated_data() {
let data = vec![0u8; 4];
let result = TrueTypeCMap::from_font_data(&data);
assert!(result.is_err());
}
#[test]
fn test_clone_and_debug() {
let data = build_truetype_with_cmap_format4(&[(65, 1)]);
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
let cloned = cmap.clone();
assert_eq!(cloned.get_unicode(1), Some('A'));
let debug = format!("{:?}", cmap);
assert!(debug.contains("TrueTypeCMap"));
}
#[test]
fn test_platform_priority_windows_full_over_bmp() {
let mut data = Vec::new();
data.write_u32::<BigEndian>(0x00010000).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u16::<BigEndian>(16).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
let cmap_offset: u32 = 12 + 16;
data.write_u32::<BigEndian>(0x636D6170).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(cmap_offset).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u16::<BigEndian>(2).unwrap();
let subtable_off: u32 = 4 + 8 * 2;
data.write_u16::<BigEndian>(3).unwrap();
data.write_u16::<BigEndian>(1).unwrap();
data.write_u32::<BigEndian>(subtable_off).unwrap();
data.write_u16::<BigEndian>(3).unwrap();
data.write_u16::<BigEndian>(10).unwrap();
data.write_u32::<BigEndian>(subtable_off).unwrap();
data.write_u16::<BigEndian>(12).unwrap();
data.write_u16::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(28).unwrap();
data.write_u32::<BigEndian>(0).unwrap();
data.write_u32::<BigEndian>(1).unwrap();
data.write_u32::<BigEndian>(65).unwrap();
data.write_u32::<BigEndian>(65).unwrap();
data.write_u32::<BigEndian>(1).unwrap();
let cmap = TrueTypeCMap::from_font_data(&data).unwrap();
assert_eq!(cmap.get_unicode(1), Some('A'));
}
}