use std::collections::{BTreeMap, HashSet};
use ttf_parser::Face;
use super::FontKind;
use super::ttf_subset::{GlyphRemapper, subset as subsetter_subset};
use crate::error::{Error, Result};
pub struct SubsetResult {
pub bytes: Vec<u8>,
pub gid_to_char: BTreeMap<u16, char>,
pub char_to_gid: BTreeMap<char, u16>,
pub gid_to_advance: BTreeMap<u16, u16>,
pub units_per_em: u16,
pub font_kind: FontKind,
}
fn is_cff_font(data: &[u8]) -> bool {
if data.len() < 12 {
return false;
}
let num_tables = u16::from_be_bytes([data[4], data[5]]) as usize;
for i in 0..num_tables {
let base = 12 + i * 16;
if base + 4 > data.len() {
break;
}
if &data[base..base + 4] == b"CFF " || &data[base..base + 4] == b"CFF2" {
return true;
}
}
false
}
pub fn subset_font(ttf_bytes: &[u8], chars: &[char]) -> Result<SubsetResult> {
let font_kind = match FontKind::detect(ttf_bytes) {
Some(kind) => kind,
None => return Err(Error::FontParse("unrecognised font magic bytes".into())),
};
if is_cff_font(ttf_bytes) {
return Err(Error::FontParse(
"CFF fonts are not supported by subsetter; \
use the TrueType variant (e.g. NotoSansCJKjp-Regular.ttf) instead"
.into(),
));
}
let face = Face::parse(ttf_bytes, 0).map_err(|e| Error::FontParse(e.to_string()))?;
let units_per_em = face.units_per_em();
let mut gids: Vec<u16> = vec![0]; let mut gids_seen: HashSet<u16> = HashSet::new();
gids_seen.insert(0);
let mut orig_gid_to_char: BTreeMap<u16, char> = BTreeMap::new();
for &ch in chars {
if let Some(glyph_id) = face.glyph_index(ch) {
let gid = glyph_id.0;
if gid != 0 {
orig_gid_to_char.entry(gid).or_insert(ch);
if gids_seen.insert(gid) {
gids.push(gid);
}
}
}
}
gids.sort_unstable();
let mut orig_gid_to_advance: BTreeMap<u16, u16> = BTreeMap::new();
for &gid in &gids {
let advance = face
.glyph_hor_advance(ttf_parser::GlyphId(gid))
.unwrap_or(units_per_em);
orig_gid_to_advance.insert(gid, advance);
}
let mut remapper = GlyphRemapper::new();
for &gid in &gids {
remapper.remap(gid);
}
let (subsetted, gids_to_keep) = subsetter_subset(ttf_bytes, 0, &remapper)
.map_err(|e| Error::FontParse(format!("font subsetting failed: {}", e)))?;
if gids_to_keep.len() > u16::MAX as usize {
return Err(Error::FontParse(format!(
"font has {} glyphs; maximum supported is {}",
gids_to_keep.len(),
u16::MAX
)));
}
let orig_to_new_gid: BTreeMap<u16, u16> = gids_to_keep
.iter()
.enumerate()
.map(|(new_idx, &orig_gid)| (orig_gid, new_idx as u16))
.collect();
let gid_to_char: BTreeMap<u16, char> = gids_to_keep
.iter()
.filter_map(|orig_gid| {
let new_gid = *orig_to_new_gid.get(orig_gid)?;
orig_gid_to_char.get(orig_gid).map(|&ch| (new_gid, ch))
})
.collect();
let char_to_gid: BTreeMap<char, u16> = chars
.iter()
.filter_map(|&ch| {
face.glyph_index(ch)
.filter(|gid| gid.0 != 0)
.and_then(|gid| orig_to_new_gid.get(&gid.0))
.map(|&new_gid| (ch, new_gid))
})
.collect();
let gid_to_advance: BTreeMap<u16, u16> = gids_to_keep
.iter()
.filter_map(|orig_gid| {
let new_gid = *orig_to_new_gid.get(orig_gid)?;
orig_gid_to_advance.get(orig_gid).map(|&adv| (new_gid, adv))
})
.collect();
Ok(SubsetResult {
bytes: subsetted,
gid_to_char,
char_to_gid,
gid_to_advance,
units_per_em,
font_kind,
})
}