use std::collections::{BTreeMap, BTreeSet};
use ttf_parser::Face;
pub(super) struct GlyphRemapper {
orig_gids: BTreeSet<u16>,
}
impl GlyphRemapper {
pub fn new() -> Self {
GlyphRemapper {
orig_gids: BTreeSet::new(),
}
}
pub fn remap(&mut self, gid: u16) {
self.orig_gids.insert(gid);
}
fn sorted_gids(&self) -> Vec<u16> {
self.orig_gids.iter().copied().collect()
}
}
pub(super) fn subset(
data: &[u8],
face_index: u32,
remapper: &GlyphRemapper,
) -> Result<(Vec<u8>, BTreeSet<u16>), Box<dyn std::error::Error>> {
let face = Face::parse(data, face_index)?;
let is_ttc = data.len() >= 4 && &data[0..4] == b"ttcf";
let font_data_start = if is_ttc {
if data.len() < 12 + (face_index as usize + 1) * 4 {
return Err("TTC header truncated".into());
}
u32::from_be_bytes([
data[12 + face_index as usize * 4],
data[12 + face_index as usize * 4 + 1],
data[12 + face_index as usize * 4 + 2],
data[12 + face_index as usize * 4 + 3],
]) as usize
} else {
0
};
let font_data = &data[font_data_start..];
let offset_table = parse_offset_table(font_data)?;
let table_recs_raw = parse_table_records_raw(font_data, &offset_table)?;
let table_records: Vec<(String, &[u8])> = {
let mut result = Vec::new();
for (tag, raw_offset, length) in table_recs_raw {
if raw_offset + length <= data.len() {
result.push((tag, &data[raw_offset..raw_offset + length]));
}
}
result
};
let mut tables = std::collections::HashMap::new();
for (tag, slice) in table_records.iter() {
tables.insert(tag.as_str(), *slice);
}
let head = *tables.get("head").ok_or("required table head not found")?;
let hhea = *tables.get("hhea").ok_or("required table hhea not found")?;
let maxp = *tables.get("maxp").ok_or("required table maxp not found")?;
let glyf = *tables.get("glyf").ok_or("required table glyf not found")?;
let loca = *tables.get("loca").ok_or("required table loca not found")?;
let hmtx = *tables.get("hmtx").ok_or("required table hmtx not found")?;
let index_to_loc_format = if head.len() >= 52 {
u16::from_be_bytes([head[50], head[51]]) & 0x0001
} else {
return Err("head table too short".into());
};
let num_h_metrics = if hhea.len() >= 36 {
u16::from_be_bytes([hhea[34], hhea[35]]) as usize
} else {
return Err("hhea table too short".into());
};
let num_glyphs = if maxp.len() >= 6 {
u16::from_be_bytes([maxp[4], maxp[5]]) as usize
} else {
return Err("maxp table too short".into());
};
let glyph_offsets = parse_loca(loca, num_glyphs, index_to_loc_format as u8)?;
let sorted_gids = remapper.sorted_gids();
let mut gids_to_keep = BTreeSet::new();
for &gid in &sorted_gids {
gids_to_keep.insert(gid);
collect_composite_deps(glyf, &glyph_offsets, gid as usize, &mut gids_to_keep)?;
}
let gid_remap: BTreeMap<u16, u16> = gids_to_keep
.iter()
.enumerate()
.map(|(new_idx, &orig_gid)| (orig_gid, new_idx as u16))
.collect();
let (new_glyf, new_glyph_offsets) = build_glyf(&glyph_offsets, glyf, &gids_to_keep, &gid_remap)?;
let max_glyph_offset = new_glyph_offsets.last().copied().unwrap_or(0);
let loca_format: u8 = if max_glyph_offset > 131070 { 1 } else { index_to_loc_format as u8 };
let new_loca = build_loca(&new_glyph_offsets, loca_format)?;
let new_hmtx = build_hmtx(
hmtx,
num_h_metrics,
face.units_per_em() as usize,
&gids_to_keep,
)?;
let mut new_head = head.to_vec();
if new_head.len() >= 12 {
new_head[8..12].copy_from_slice(&[0u8; 4]);
}
let mut new_hhea = hhea.to_vec();
let mut new_maxp = maxp.to_vec();
if loca_format as u16 != index_to_loc_format && new_head.len() >= 52 {
new_head[50..52].copy_from_slice(&(loca_format as u16).to_be_bytes());
}
let num_new_metrics = gids_to_keep.len();
if new_hhea.len() >= 36 {
new_hhea[34..36].copy_from_slice(&(num_new_metrics as u16).to_be_bytes());
}
if new_hhea.len() >= 14 && !new_hmtx.is_empty() {
let adv_max = new_hmtx.chunks(4)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.max()
.unwrap_or(0);
new_hhea[10..12].copy_from_slice(&adv_max.to_be_bytes());
let lsb_min = new_hmtx.chunks(4)
.map(|c| i16::from_be_bytes([c[2], c[3]]))
.min()
.unwrap_or(0);
new_hhea[12..14].copy_from_slice(&lsb_min.to_be_bytes());
}
{
let mut g_x_min = i16::MAX;
let mut g_y_min = i16::MAX;
let mut g_x_max = i16::MIN;
let mut g_y_max = i16::MIN;
let mut min_rsb = i16::MAX;
let mut max_extent: i16 = i16::MIN;
let mut has_outlines = false;
for new_idx in 0..gids_to_keep.len() {
let start = new_glyph_offsets[new_idx] as usize;
let end = new_glyph_offsets[new_idx + 1] as usize;
if end < start + 10 || end > new_glyf.len() { continue; }
let hdr = &new_glyf[start..end];
let x_min = i16::from_be_bytes([hdr[2], hdr[3]]);
let y_min = i16::from_be_bytes([hdr[4], hdr[5]]);
let x_max = i16::from_be_bytes([hdr[6], hdr[7]]);
let y_max = i16::from_be_bytes([hdr[8], hdr[9]]);
let adv = if new_idx * 4 + 2 <= new_hmtx.len() {
u16::from_be_bytes([new_hmtx[new_idx * 4], new_hmtx[new_idx * 4 + 1]]) as i16
} else { 0 };
let lsb = if new_idx * 4 + 4 <= new_hmtx.len() {
i16::from_be_bytes([new_hmtx[new_idx * 4 + 2], new_hmtx[new_idx * 4 + 3]])
} else { 0 };
g_x_min = g_x_min.min(x_min);
g_y_min = g_y_min.min(y_min);
g_x_max = g_x_max.max(x_max);
g_y_max = g_y_max.max(y_max);
min_rsb = min_rsb.min(adv - lsb - (x_max - x_min));
max_extent = max_extent.max(lsb + (x_max - x_min));
has_outlines = true;
}
if has_outlines {
if new_hhea.len() >= 18 {
new_hhea[14..16].copy_from_slice(&min_rsb.to_be_bytes());
new_hhea[16..18].copy_from_slice(&max_extent.to_be_bytes());
}
if new_head.len() >= 44 {
new_head[36..38].copy_from_slice(&g_x_min.to_be_bytes());
new_head[38..40].copy_from_slice(&g_y_min.to_be_bytes());
new_head[40..42].copy_from_slice(&g_x_max.to_be_bytes());
new_head[42..44].copy_from_slice(&g_y_max.to_be_bytes());
}
}
}
if new_maxp.len() >= 6 {
let bytes = (gids_to_keep.len() as u16).to_be_bytes();
new_maxp[4..6].copy_from_slice(&bytes);
}
let mut output_tables: BTreeMap<String, Vec<u8>> = BTreeMap::new();
output_tables.insert("head".into(), new_head);
output_tables.insert("hhea".into(), new_hhea);
output_tables.insert("maxp".into(), new_maxp);
output_tables.insert("glyf".into(), new_glyf);
output_tables.insert("loca".into(), new_loca);
output_tables.insert("hmtx".into(), new_hmtx);
for (tag, data_slice) in table_records.iter() {
if matches!(tag.as_str(), "head" | "hhea" | "maxp" | "glyf" | "loca" | "hmtx") {
continue;
}
if matches!(tag.as_str(), "fpgm" | "prep" | "cvt " | "gasp") {
output_tables.insert(tag.clone(), data_slice.to_vec());
}
}
let font_bytes = assemble_font(&output_tables, offset_table.is_truetype)?;
Ok((font_bytes, gids_to_keep))
}
struct OffsetTable {
is_truetype: bool,
num_tables: usize,
}
fn parse_offset_table(data: &[u8]) -> Result<OffsetTable, Box<dyn std::error::Error>> {
if data.len() < 12 {
return Err("font too short for offset table".into());
}
let scaler = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let is_truetype = scaler == 0x00010000 || scaler == 0x74727565;
let num_tables = u16::from_be_bytes([data[4], data[5]]) as usize;
Ok(OffsetTable {
is_truetype,
num_tables,
})
}
#[allow(clippy::type_complexity)]
fn parse_table_records_raw(
data: &[u8],
offset_table: &OffsetTable,
) -> Result<Vec<(String, usize, usize)>, Box<dyn std::error::Error>> {
let mut records = Vec::new();
for i in 0..offset_table.num_tables {
let base = 12 + i * 16;
if base + 16 > data.len() {
return Err("table directory truncated".into());
}
let tag = String::from_utf8_lossy(&data[base..base + 4]).to_string();
let offset = u32::from_be_bytes([
data[base + 8],
data[base + 9],
data[base + 10],
data[base + 11],
]) as usize;
let length = u32::from_be_bytes([
data[base + 12],
data[base + 13],
data[base + 14],
data[base + 15],
]) as usize;
records.push((tag, offset, length));
}
Ok(records)
}
fn parse_loca(
loca: &[u8],
num_glyphs: usize,
format: u8,
) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
let mut offsets = Vec::new();
if format == 0 {
for i in 0..=num_glyphs {
if i * 2 + 2 > loca.len() {
return Err("loca table truncated".into());
}
let offset = u16::from_be_bytes([loca[i * 2], loca[i * 2 + 1]]) as u32 * 2;
offsets.push(offset);
}
} else {
for i in 0..=num_glyphs {
if i * 4 + 4 > loca.len() {
return Err("loca table truncated".into());
}
let offset = u32::from_be_bytes([
loca[i * 4],
loca[i * 4 + 1],
loca[i * 4 + 2],
loca[i * 4 + 3],
]);
offsets.push(offset);
}
}
Ok(offsets)
}
fn collect_composite_deps(
glyf: &[u8],
glyph_offsets: &[u32],
gid: usize,
visited: &mut BTreeSet<u16>,
) -> Result<(), Box<dyn std::error::Error>> {
if gid >= glyph_offsets.len() - 1 {
return Ok(());
}
let start = glyph_offsets[gid] as usize;
let end = glyph_offsets[gid + 1] as usize;
if start >= glyf.len() || end > glyf.len() {
return Ok(()); }
let glyph_data = &glyf[start..end];
if glyph_data.len() < 2 {
return Ok(());
}
let num_contours = i16::from_be_bytes([glyph_data[0], glyph_data[1]]);
if num_contours >= 0 {
return Ok(()); }
let mut offset = 10; while offset < glyph_data.len() {
if offset + 4 > glyph_data.len() {
break;
}
let flags = u16::from_be_bytes([glyph_data[offset], glyph_data[offset + 1]]);
let comp_gid = u16::from_be_bytes([glyph_data[offset + 2], glyph_data[offset + 3]]);
if visited.insert(comp_gid) {
collect_composite_deps(glyf, glyph_offsets, comp_gid as usize, visited)?;
}
offset += 4;
if (flags & 0x0001) != 0 {
offset += 4; } else {
offset += 2; }
if (flags & 0x0008) != 0 {
offset += 2; } else if (flags & 0x0040) != 0 {
offset += 4; } else if (flags & 0x0080) != 0 {
offset += 8; }
if (flags & 0x0020) == 0 {
break; }
}
Ok(())
}
fn build_glyf(
glyph_offsets: &[u32],
glyf: &[u8],
gids_to_keep: &BTreeSet<u16>,
gid_remap: &BTreeMap<u16, u16>,
) -> Result<(Vec<u8>, Vec<u32>), Box<dyn std::error::Error>> {
let mut new_glyf = Vec::new();
let mut new_offsets = vec![0u32];
for gid in gids_to_keep.iter() {
let gid_usize = *gid as usize;
if gid_usize + 1 >= glyph_offsets.len() {
return Err("glyph index out of range".into());
}
let start = glyph_offsets[gid_usize] as usize;
let end = glyph_offsets[gid_usize + 1] as usize;
if start > glyf.len() || end > glyf.len() {
return Err("glyph offset out of bounds".into());
}
let glyph_data = &glyf[start..end];
if glyph_data.len() >= 2 {
let num_contours = i16::from_be_bytes([glyph_data[0], glyph_data[1]]);
if num_contours < 0 {
new_glyf.extend_from_slice(rewrite_composite_gids(glyph_data, gid_remap).as_slice());
} else {
new_glyf.extend_from_slice(glyph_data);
}
} else {
new_glyf.extend_from_slice(glyph_data);
}
let padding = (4 - new_glyf.len() % 4) % 4;
new_glyf.resize(new_glyf.len() + padding, 0u8);
new_offsets.push(new_glyf.len() as u32);
}
Ok((new_glyf, new_offsets))
}
fn rewrite_composite_gids(glyph_data: &[u8], gid_remap: &BTreeMap<u16, u16>) -> Vec<u8> {
let mut out = glyph_data.to_vec();
let mut offset = 10;
while offset < out.len() {
if offset + 4 > out.len() {
break;
}
let flags = u16::from_be_bytes([out[offset], out[offset + 1]]);
let orig_comp_gid = u16::from_be_bytes([out[offset + 2], out[offset + 3]]);
if let Some(&new_gid) = gid_remap.get(&orig_comp_gid) {
let bytes = new_gid.to_be_bytes();
out[offset + 2] = bytes[0];
out[offset + 3] = bytes[1];
}
offset += 4;
if (flags & 0x0001) != 0 {
offset += 4; } else {
offset += 2; }
if (flags & 0x0008) != 0 {
offset += 2; } else if (flags & 0x0040) != 0 {
offset += 4; } else if (flags & 0x0080) != 0 {
offset += 8; }
if (flags & 0x0020) == 0 {
break; }
}
out
}
fn build_loca(glyph_offsets: &[u32], format: u8) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut loca = Vec::new();
if format == 0 {
for &offset in glyph_offsets {
let half = (offset / 2) as u16;
loca.extend_from_slice(&half.to_be_bytes());
}
} else {
for &offset in glyph_offsets {
loca.extend_from_slice(&offset.to_be_bytes());
}
}
Ok(loca)
}
fn build_hmtx(
hmtx: &[u8],
num_h_metrics: usize,
units_per_em: usize,
gids_to_keep: &BTreeSet<u16>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut new_hmtx = Vec::new();
let last_adv_off = num_h_metrics.saturating_sub(1) * 4;
for &gid in gids_to_keep.iter() {
let g = gid as usize;
if g < num_h_metrics {
let off = g * 4;
if off + 4 <= hmtx.len() {
new_hmtx.extend_from_slice(&hmtx[off..off + 4]);
} else {
new_hmtx.extend_from_slice(&(units_per_em as u16).to_be_bytes());
new_hmtx.extend_from_slice(&0i16.to_be_bytes());
}
} else {
if last_adv_off + 2 <= hmtx.len() {
new_hmtx.extend_from_slice(&hmtx[last_adv_off..last_adv_off + 2]);
} else {
new_hmtx.extend_from_slice(&(units_per_em as u16).to_be_bytes());
}
let lsb_off = num_h_metrics * 4 + (g - num_h_metrics) * 2;
if lsb_off + 2 <= hmtx.len() {
new_hmtx.extend_from_slice(&hmtx[lsb_off..lsb_off + 2]);
} else {
new_hmtx.extend_from_slice(&0i16.to_be_bytes());
}
}
}
Ok(new_hmtx)
}
fn calc_checksum(data: &[u8]) -> u32 {
let mut sum = 0u32;
let mut i = 0;
while i + 4 <= data.len() {
sum = sum.wrapping_add(u32::from_be_bytes([data[i], data[i+1], data[i+2], data[i+3]]));
i += 4;
}
if i < data.len() {
let mut buf = [0u8; 4];
buf[..data.len() - i].copy_from_slice(&data[i..]);
sum = sum.wrapping_add(u32::from_be_bytes(buf));
}
sum
}
fn assemble_font(
tables: &BTreeMap<String, Vec<u8>>,
is_truetype: bool,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let num_tables = tables.len();
let search_range: u16 = (1u16 << (15 - (num_tables as u16).leading_zeros())) * 16;
let entry_selector: u16 = 15 - (num_tables as u16).leading_zeros() as u16;
let range_shift: u16 = num_tables as u16 * 16 - search_range;
let mut font = Vec::new();
let scaler = if is_truetype {
0x00010000u32
} else {
0x4F544F54u32
}; font.extend_from_slice(&scaler.to_be_bytes());
font.extend_from_slice(&(num_tables as u16).to_be_bytes());
font.extend_from_slice(&search_range.to_be_bytes());
font.extend_from_slice(&entry_selector.to_be_bytes());
font.extend_from_slice(&range_shift.to_be_bytes());
let mut dir_offset = 12 + num_tables * 16;
let mut table_offsets = Vec::new();
for (tag, data) in tables.iter() {
dir_offset = (dir_offset + 3) & !3;
table_offsets.push((tag.clone(), dir_offset, data.len()));
dir_offset += data.len();
}
for (tag, offset, length) in table_offsets.iter() {
font.extend_from_slice(tag.as_bytes());
font.extend_from_slice(&0u32.to_be_bytes());
font.extend_from_slice(&(*offset as u32).to_be_bytes());
font.extend_from_slice(&(*length as u32).to_be_bytes());
}
let mut current_offset = font.len();
for (_tag, data) in tables.iter() {
current_offset = (current_offset + 3) & !3;
while font.len() < current_offset {
font.push(0);
}
font.extend_from_slice(data);
current_offset += data.len();
}
for (i, (_tag, offset, length)) in table_offsets.iter().enumerate() {
let checksum = calc_checksum(&font[*offset..*offset + *length]);
let cs_pos = 12 + i * 16 + 4;
font[cs_pos..cs_pos + 4].copy_from_slice(&checksum.to_be_bytes());
}
let full_sum = calc_checksum(&font);
let adjustment = 0xB1B0AFBAu32.wrapping_sub(full_sum);
for (tag, offset, _length) in &table_offsets {
if tag == "head" {
let pos = offset + 8;
if pos + 4 <= font.len() {
font[pos..pos + 4].copy_from_slice(&adjustment.to_be_bytes());
}
break;
}
}
Ok(font)
}