use skrifa::outline::{DrawSettings, OutlinePen};
use skrifa::prelude::*;
use skrifa::raw::{FileRef, FontRef, TableProvider};
use skrifa::string::StringId;
use skrifa::{GlyphId, MetadataProvider};
use std::collections::BTreeMap;
use std::path::Path;
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone)]
pub struct ParsedFont {
pub family: Option<String>,
pub subfamily: Option<String>,
pub full_name: Option<String>,
pub postscript_name: Option<String>,
pub version: Option<String>,
pub copyright: Option<String>,
pub manufacturer: Option<String>,
pub num_glyphs: u16,
pub units_per_em: u16,
pub cmap: BTreeMap<u32, u16>,
pub outlines: BTreeMap<u32, u64>,
pub cmap_sha256: String,
pub file_sha256: String,
pub collection_index: u32,
}
pub fn sha256_hex(bytes: &[u8]) -> String {
let mut h = Sha256::new();
h.update(bytes);
hex(&h.finalize())
}
fn hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}
pub fn is_font_ext(ext: &str) -> bool {
matches!(ext.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc")
}
pub fn parse_file(path: &Path) -> Result<Vec<ParsedFont>> {
let data = std::fs::read(path).with_context(|| format!("reading font {}", path.display()))?;
Ok(parse_data(&data))
}
pub fn parse_data(data: &[u8]) -> Vec<ParsedFont> {
let file_sha = sha256_hex(data);
let count = match FileRef::new(data) {
Ok(FileRef::Collection(c)) => c.len(),
Ok(FileRef::Font(_)) => 1,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
for i in 0..count {
if let Ok(face) = FontRef::from_index(data, i) {
out.push(from_face(&face, &file_sha, i));
}
}
out
}
fn name(face: &FontRef, id: u16) -> Option<String> {
face.localized_strings(StringId::new(id))
.map(|s| s.chars().collect::<String>().trim().to_string())
.find(|s| !s.is_empty())
}
fn extract_cmap(face: &FontRef) -> BTreeMap<u32, u16> {
let charmap = face.charmap();
if charmap.is_symbol() {
return BTreeMap::new();
}
charmap.mappings().map(|(cp, gid)| (cp, gid.to_u32() as u16)).collect()
}
fn cmap_hash(cmap: &BTreeMap<u32, u16>) -> String {
let mut h = Sha256::new();
for (cp, g) in cmap {
h.update(cp.to_le_bytes());
h.update(g.to_le_bytes());
}
hex(&h.finalize())
}
struct OutlineSig {
h: u64,
empty: bool,
start: Option<(f32, f32)>,
cur: Option<(f32, f32)>,
}
impl OutlineSig {
fn new() -> Self {
Self { h: 0xcbf29ce484222325, empty: true, start: None, cur: None }
}
fn mix(&mut self, v: i64) {
self.empty = false;
self.h ^= v as u64;
self.h = self.h.wrapping_mul(0x100000001b3);
}
}
impl OutlinePen for OutlineSig {
fn move_to(&mut self, x: f32, y: f32) {
self.start = Some((x, y));
self.cur = Some((x, y));
self.mix(1);
self.mix(x as i64);
self.mix(y as i64);
}
fn line_to(&mut self, x: f32, y: f32) {
self.cur = Some((x, y));
self.mix(2);
self.mix(x as i64);
self.mix(y as i64);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.cur = Some((x, y));
self.mix(3);
for v in [x1, y1, x, y] {
self.mix(v as i64);
}
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.cur = Some((x, y));
self.mix(4);
for v in [x1, y1, x2, y2, x, y] {
self.mix(v as i64);
}
}
fn close(&mut self) {
if let (Some(s), Some(c)) = (self.start, self.cur) {
if s != c {
self.line_to(s.0, s.1);
}
}
self.mix(5);
}
}
pub(crate) fn glyph_outline_hash(face: &FontRef, gid: u16) -> Option<u64> {
let mut b = OutlineSig::new();
let glyph = face.outline_glyphs().get(GlyphId::from(gid))?;
glyph.draw(DrawSettings::unhinted(Size::unscaled(), LocationRef::default()), &mut b).ok()?;
(!b.empty).then_some(b.h)
}
fn extract_outlines(face: &FontRef, cmap: &BTreeMap<u32, u16>) -> BTreeMap<u32, u64> {
use unicode_script::{Script, UnicodeScript};
cmap.iter()
.filter(|(cp, _)| {
char::from_u32(**cp).map(|c| c.is_alphabetic() && c.script() == Script::Latin).unwrap_or(false)
})
.filter_map(|(cp, gid)| glyph_outline_hash(face, *gid).map(|h| (*cp, h)))
.collect()
}
fn from_face(face: &FontRef, file_sha: &str, index: u32) -> ParsedFont {
let cmap = extract_cmap(face);
let cmap_sha256 = cmap_hash(&cmap);
let outlines = extract_outlines(face, &cmap);
ParsedFont {
family: name(face, 1),
subfamily: name(face, 2),
full_name: name(face, 4),
postscript_name: name(face, 6),
version: name(face, 5),
copyright: name(face, 0),
manufacturer: name(face, 8),
num_glyphs: face.maxp().map(|m| m.num_glyphs()).unwrap_or(0),
units_per_em: face.head().map(|h| h.units_per_em()).unwrap_or(0),
cmap,
outlines,
cmap_sha256,
file_sha256: file_sha.to_string(),
collection_index: index,
}
}
impl ParsedFont {
pub fn pua_ratio(&self) -> f32 {
if self.cmap.is_empty() {
return 0.0;
}
let pua = self.cmap.keys().filter(|cp| is_pua(**cp)).count();
pua as f32 / self.cmap.len() as f32
}
}
pub fn is_pua(cp: u32) -> bool {
(0xE000..=0xF8FF).contains(&cp)
|| (0xF_0000..=0xF_FFFD).contains(&cp)
|| (0x10_0000..=0x10_FFFD).contains(&cp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pua_classifier() {
assert!(is_pua(0xE000));
assert!(is_pua(0xF8FF));
assert!(!is_pua(0x0041)); assert!(is_pua(0xF_0001));
}
#[test]
fn hex_roundtrip() {
assert_eq!(sha256_hex(b""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
}
}