use std::collections::BTreeMap;
use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use crate::font::{self, ParsedFont};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FontEntry {
pub path: String,
pub collection_index: u32,
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 file_sha256: String,
pub cmap_sha256: String,
pub cmap_len: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub cmap: Option<Vec<(u32, u16)>>,
}
impl FontEntry {
fn from_parsed(path: &Path, f: &ParsedFont, include_cmap: bool) -> Self {
FontEntry {
path: path.display().to_string(),
collection_index: f.collection_index,
family: f.family.clone(),
subfamily: f.subfamily.clone(),
full_name: f.full_name.clone(),
postscript_name: f.postscript_name.clone(),
version: f.version.clone(),
copyright: f.copyright.clone(),
manufacturer: f.manufacturer.clone(),
num_glyphs: f.num_glyphs,
units_per_em: f.units_per_em,
file_sha256: f.file_sha256.clone(),
cmap_sha256: f.cmap_sha256.clone(),
cmap_len: f.cmap.len(),
cmap: if include_cmap {
Some(f.cmap.iter().map(|(cp, g)| (*cp, *g)).collect())
} else {
None
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FontRegistry {
pub tool: String,
pub source_dir: String,
pub count: usize,
pub skipped: Vec<String>,
pub fonts: Vec<FontEntry>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Identification {
Pristine,
KnownButCmapModified,
KnownVariant,
Unidentified,
}
impl FontRegistry {
pub fn build_from_dir(dir: &Path, include_cmap: bool) -> Result<Self> {
let mut fonts = Vec::new();
let mut skipped = Vec::new();
for entry in WalkDir::new(dir).follow_links(false).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if !p.is_file() {
continue;
}
let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
if !font::is_font_ext(ext) {
continue;
}
match font::parse_file(p) {
Ok(faces) if !faces.is_empty() => {
for f in &faces {
fonts.push(FontEntry::from_parsed(p, f, include_cmap));
}
}
_ => skipped.push(p.display().to_string()),
}
}
Ok(FontRegistry {
tool: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
source_dir: dir.display().to_string(),
count: fonts.len(),
skipped,
fonts,
})
}
pub fn save_json(&self, path: &Path, pretty: bool) -> Result<()> {
let data = if pretty {
serde_json::to_vec_pretty(self)?
} else {
serde_json::to_vec(self)?
};
std::fs::write(path, data).with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
pub fn load_json(path: &Path) -> Result<Self> {
let data = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
Ok(serde_json::from_slice(&data)?)
}
fn index(&self) -> (BTreeMap<&str, &FontEntry>, BTreeMap<String, Vec<&FontEntry>>) {
let mut by_sha = BTreeMap::new();
let mut by_id: BTreeMap<String, Vec<&FontEntry>> = BTreeMap::new();
for e in &self.fonts {
by_sha.insert(e.file_sha256.as_str(), e);
for id in [&e.postscript_name, &e.family, &e.full_name].into_iter().flatten() {
by_id.entry(id.to_lowercase()).or_default().push(e);
}
}
(by_sha, by_id)
}
pub fn identify(&self, f: &ParsedFont) -> Identification {
let (by_sha, by_id) = self.index();
if by_sha.contains_key(f.file_sha256.as_str()) {
return Identification::Pristine;
}
let ids = [&f.postscript_name, &f.family, &f.full_name];
for id in ids.into_iter().flatten() {
if let Some(cands) = by_id.get(&id.to_lowercase()) {
if cands.iter().any(|c| c.cmap_sha256 == f.cmap_sha256) {
return Identification::KnownVariant;
}
return Identification::KnownButCmapModified;
}
}
Identification::Unidentified
}
}