use std::collections::HashSet;
pub fn subset_truetype(font: &[u8], chars: &HashSet<char>) -> Option<Vec<u8>> {
let face = ttf_parser::Face::parse(font, 0).ok()?;
let num_glyphs = face.number_of_glyphs();
let mut keep: HashSet<u16> = HashSet::new();
keep.insert(0); for &ch in chars {
if let Some(gid) = face.glyph_index(ch) {
keep.insert(gid.0);
}
}
let glyf = raw_table(font, b"glyf")?;
let loca = raw_table(font, b"loca")?;
let head = raw_table(font, b"head")?;
let long_loca = read_u16(&font[head.0..], 50)? == 1;
let offsets = parse_loca(&font[loca.0..loca.0 + loca.1], num_glyphs, long_loca)?;
let mut queue: Vec<u16> = keep.iter().copied().collect();
while let Some(gid) = queue.pop() {
let Some((start, end)) = glyph_range(&offsets, gid) else {
continue;
};
if end <= start {
continue;
}
let data = glyf
.1
.checked_sub(start)
.and_then(|_| font.get(glyf.0 + start..glyf.0 + end.min(glyf.1)))?;
for comp in composite_components(data) {
if keep.insert(comp) {
queue.push(comp);
}
}
}
let mut new_glyf: Vec<u8> = Vec::with_capacity(glyf.1 / 2);
let mut new_offsets: Vec<u32> = Vec::with_capacity(offsets.len());
new_offsets.push(0);
for gid in 0..num_glyphs {
if keep.contains(&gid) {
if let Some((start, end)) = glyph_range(&offsets, gid) {
if end > start && end <= glyf.1 {
new_glyf.extend_from_slice(&font[glyf.0 + start..glyf.0 + end]);
while !new_glyf.len().is_multiple_of(4) {
new_glyf.push(0);
}
}
}
}
new_offsets.push(new_glyf.len() as u32);
}
let mut new_loca = Vec::with_capacity(new_offsets.len() * 4);
for ofs in &new_offsets {
new_loca.extend_from_slice(&ofs.to_be_bytes());
}
rebuild_sfnt(font, &[(*b"glyf", new_glyf), (*b"loca", new_loca)], true)
}
fn raw_table(font: &[u8], tag: &[u8; 4]) -> Option<(usize, usize)> {
let num_tables = read_u16(font, 4)? as usize;
for i in 0..num_tables {
let rec = 12 + i * 16;
if font.get(rec..rec + 4)? == tag {
let offset = read_u32(font, rec + 8)? as usize;
let length = read_u32(font, rec + 12)? as usize;
if offset.checked_add(length)? <= font.len() {
return Some((offset, length));
}
}
}
None
}
fn parse_loca(data: &[u8], num_glyphs: u16, long: bool) -> Option<Vec<u32>> {
let count = num_glyphs as usize + 1;
let mut out = Vec::with_capacity(count);
if long {
if data.len() < count * 4 {
return None;
}
for i in 0..count {
out.push(read_u32(data, i * 4)?);
}
} else {
if data.len() < count * 2 {
return None;
}
for i in 0..count {
out.push(read_u16(data, i * 2)? as u32 * 2);
}
}
Some(out)
}
fn glyph_range(offsets: &[u32], gid: u16) -> Option<(usize, usize)> {
let start = *offsets.get(gid as usize)? as usize;
let end = *offsets.get(gid as usize + 1)? as usize;
Some((start, end))
}
fn composite_components(data: &[u8]) -> Vec<u16> {
let mut out = Vec::new();
let Some(n_contours) = read_u16(data, 0) else {
return out;
};
if (n_contours as i16) >= 0 {
return out; }
let mut pos = 10usize;
while let Some(flags) = read_u16(data, pos) {
let Some(gid) = read_u16(data, pos + 2) else {
break;
};
out.push(gid);
pos += 4;
pos += if flags & 0x0001 != 0 { 4 } else { 2 }; if flags & 0x0008 != 0 {
pos += 2; } else if flags & 0x0040 != 0 {
pos += 4; } else if flags & 0x0080 != 0 {
pos += 8; }
if flags & 0x0020 == 0 {
break; }
}
out
}
pub(crate) fn rebuild_sfnt(
font: &[u8],
replacements: &[([u8; 4], Vec<u8>)],
force_long_loca: bool,
) -> Option<Vec<u8>> {
let num_tables = read_u16(font, 4)? as usize;
let mut tables: Vec<([u8; 4], Vec<u8>)> = Vec::with_capacity(num_tables);
for i in 0..num_tables {
let rec = 12 + i * 16;
let mut tag = [0u8; 4];
tag.copy_from_slice(font.get(rec..rec + 4)?);
let data = match replacements.iter().find(|(t, _)| *t == tag) {
Some((_, d)) => d.clone(),
None => {
let offset = read_u32(font, rec + 8)? as usize;
let length = read_u32(font, rec + 12)? as usize;
let mut d = font.get(offset..offset + length)?.to_vec();
if &tag == b"head" && force_long_loca && d.len() >= 52 {
d[50] = 0;
d[51] = 1; }
d
}
};
tables.push((tag, data));
}
let n = tables.len() as u16;
let mut search_range = 1u16;
let mut entry_selector = 0u16;
while search_range * 2 <= n {
search_range *= 2;
entry_selector += 1;
}
search_range *= 16;
let range_shift = n * 16 - search_range;
let mut out = Vec::new();
out.extend_from_slice(&font[0..4]); out.extend_from_slice(&n.to_be_bytes());
out.extend_from_slice(&search_range.to_be_bytes());
out.extend_from_slice(&entry_selector.to_be_bytes());
out.extend_from_slice(&range_shift.to_be_bytes());
let dir_len = 12 + tables.len() * 16;
let mut offset = dir_len;
let mut dir: Vec<u8> = Vec::with_capacity(tables.len() * 16);
for (tag, data) in &tables {
let padded = (data.len() + 3) & !3;
dir.extend_from_slice(tag);
dir.extend_from_slice(&table_checksum(data).to_be_bytes());
dir.extend_from_slice(&(offset as u32).to_be_bytes());
dir.extend_from_slice(&(data.len() as u32).to_be_bytes());
offset += padded;
}
out.extend_from_slice(&dir);
for (_, data) in &tables {
out.extend_from_slice(data);
while out.len() % 4 != 0 {
out.push(0);
}
}
Some(out)
}
fn table_checksum(data: &[u8]) -> u32 {
let mut sum = 0u32;
for chunk in data.chunks(4) {
let mut word = [0u8; 4];
word[..chunk.len()].copy_from_slice(chunk);
sum = sum.wrapping_add(u32::from_be_bytes(word));
}
sum
}
fn read_u16(data: &[u8], pos: usize) -> Option<u16> {
Some(u16::from_be_bytes([*data.get(pos)?, *data.get(pos + 1)?]))
}
fn read_u32(data: &[u8], pos: usize) -> Option<u32> {
Some(u32::from_be_bytes([
*data.get(pos)?,
*data.get(pos + 1)?,
*data.get(pos + 2)?,
*data.get(pos + 3)?,
]))
}