use byteorder::{BigEndian, ReadBytesExt};
use std::collections::HashMap;
use std::io::Cursor;
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]
}
struct Format4Segment {
start: u32,
seg: usize,
seg_count: usize,
id_delta: i32,
id_range_offset: u16,
}
#[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 seg_count = Self::read_cmap_format4_header(cursor)?;
let (end_codes, start_codes, id_deltas, id_range_offsets, glyph_id_array) =
Self::read_cmap_format4_segments(cursor, seg_count)?;
Ok(Self::build_cmap_format4_gid_map(
seg_count,
&end_codes,
&start_codes,
&id_deltas,
&id_range_offsets,
&glyph_id_array,
))
}
fn read_cmap_format4_header(cursor: &mut Cursor<&[u8]>) -> Result<usize, 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))?;
Ok(seg_count)
}
#[allow(clippy::type_complexity)]
fn read_cmap_format4_segments(
cursor: &mut Cursor<&[u8]>,
seg_count: usize,
) -> Result<(Vec<u16>, Vec<u16>, Vec<i16>, Vec<u16>, Vec<u16>), String> {
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);
}
Ok((end_codes, start_codes, id_deltas, id_range_offsets, glyph_id_array))
}
fn build_cmap_format4_gid_map(
seg_count: usize,
end_codes: &[u16],
start_codes: &[u16],
id_deltas: &[i16],
id_range_offsets: &[u16],
glyph_id_array: &[u16],
) -> HashMap<u16, char> {
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 segment = Format4Segment {
start,
seg,
seg_count,
id_delta,
id_range_offset: id_range_offsets[seg],
};
let gid = Self::resolve_cmap_format4_gid(char_code, &segment, glyph_id_array);
if gid != 0
&& let Some(ch) = char::from_u32(char_code)
{
gid_to_unicode.insert(gid, ch);
}
}
}
gid_to_unicode
}
fn resolve_cmap_format4_gid(char_code: u32, segment: &Format4Segment, glyph_id_array: &[u16]) -> u16 {
if segment.id_range_offset == 0 {
return (char_code as i32 + segment.id_delta) as u16;
}
let offset =
(segment.id_range_offset as usize) / 2 + (char_code as usize - segment.start as usize) + segment.seg
- segment.seg_count;
if offset < glyph_id_array.len() {
let raw = glyph_id_array[offset];
if raw != 0 {
(raw as i32 + segment.id_delta) as u16
} else {
0
}
} else {
0
}
}
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;