use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::OnceLock;
pub struct OutlinePathBuilder {
pub builder: tiny_skia::PathBuilder,
}
impl ttf_parser::OutlineBuilder for OutlinePathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.builder.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.builder.line_to(x, y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.builder.quad_to(x1, y1, x, y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.builder.cubic_to(x1, y1, x2, y2, x, y);
}
fn close(&mut self) {
self.builder.close();
}
}
fn standard14_substitutes(base_font: &str) -> &'static [&'static str] {
match base_font {
"Helvetica" | "Helvetica-Oblique" => &[
"Arial",
"Liberation Sans",
"DejaVu Sans",
"Nimbus Sans",
"FreeSans",
],
"Helvetica-Bold" | "Helvetica-BoldOblique" => &[
"Arial Bold",
"Liberation Sans Bold",
"DejaVu Sans Bold",
"Nimbus Sans Bold",
],
"Times-Roman" | "Times-Italic" => &[
"Times New Roman",
"Liberation Serif",
"DejaVu Serif",
"Nimbus Roman",
"FreeSerif",
],
"Times-Bold" | "Times-BoldItalic" => &[
"Times New Roman Bold",
"Liberation Serif Bold",
"DejaVu Serif Bold",
],
"Courier" | "Courier-Oblique" => &[
"Courier New",
"Liberation Mono",
"DejaVu Sans Mono",
"Nimbus Mono",
"FreeMono",
],
"Courier-Bold" | "Courier-BoldOblique" => &[
"Courier New Bold",
"Liberation Mono Bold",
"DejaVu Sans Mono Bold",
],
"Symbol" => &["Symbol", "OpenSymbol", "Standard Symbols PS"],
"ZapfDingbats" => &["Dingbats", "ZapfDingbats", "D050000L"],
_ => &[],
}
}
fn system_font_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
#[cfg(target_os = "macos")]
{
dirs.push(PathBuf::from("/Library/Fonts"));
dirs.push(PathBuf::from("/System/Library/Fonts"));
if let Some(home) = std::env::var_os("HOME") {
dirs.push(PathBuf::from(home).join("Library/Fonts"));
}
}
#[cfg(target_os = "windows")]
{
dirs.push(PathBuf::from(r"C:\Windows\Fonts"));
if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
dirs.push(
PathBuf::from(local_app_data)
.join("Microsoft")
.join("Windows")
.join("Fonts"),
);
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
dirs.push(PathBuf::from("/usr/share/fonts"));
dirs.push(PathBuf::from("/usr/local/share/fonts"));
if let Some(home) = std::env::var_os("HOME") {
let home_path = PathBuf::from(home);
dirs.push(home_path.join(".fonts"));
dirs.push(home_path.join(".local/share/fonts"));
}
}
dirs
}
fn scan_font_dir(dir: &std::path::Path, out: &mut HashMap<String, (PathBuf, u32)>) {
let read_dir = match std::fs::read_dir(dir) {
Ok(rd) => rd,
Err(_) => return,
};
for entry in read_dir.flatten() {
let path = entry.path();
if path.is_dir() {
scan_font_dir(&path, out);
continue;
}
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_ascii_lowercase();
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc") {
continue;
}
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(_) => continue,
};
for ttc_index in 0u32..4 {
let face = match ttf_parser::Face::parse(&bytes, ttc_index) {
Ok(f) => f,
Err(_) => break,
};
for name_record in face.names() {
if name_record.name_id == ttf_parser::name_id::FAMILY {
if let Some(family) = name_record.to_string() {
let key = family.to_lowercase();
out.entry(key).or_insert_with(|| (path.clone(), ttc_index));
}
}
}
}
}
}
static SYSTEM_FONT_SCAN: OnceLock<HashMap<String, (PathBuf, u32)>> = OnceLock::new();
fn get_system_font_scan() -> &'static HashMap<String, (PathBuf, u32)> {
SYSTEM_FONT_SCAN.get_or_init(|| {
let mut map: HashMap<String, (PathBuf, u32)> = HashMap::new();
for dir in system_font_dirs() {
scan_font_dir(&dir, &mut map);
}
map
})
}
type ResolvedFontMap = HashMap<String, Option<(PathBuf, u32)>>;
static RESOLVED_CACHE: OnceLock<std::sync::Mutex<ResolvedFontMap>> = OnceLock::new();
fn get_resolved_cache() -> &'static std::sync::Mutex<ResolvedFontMap> {
RESOLVED_CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()))
}
pub fn resolve_standard14_substitute(base_font: &str) -> Option<(PathBuf, u32)> {
{
let cache = get_resolved_cache();
if let Ok(guard) = cache.lock() {
if let Some(cached) = guard.get(base_font) {
return cached.clone();
}
}
}
let scan = get_system_font_scan();
let substitutes = standard14_substitutes(base_font);
let result = substitutes.iter().find_map(|&family| {
let key = family.to_lowercase();
scan.get(&key).cloned()
});
{
let cache = get_resolved_cache();
if let Ok(mut guard) = cache.lock() {
guard.insert(base_font.to_string(), result.clone());
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_outline_builder_records_move_and_line() {
let mut b = OutlinePathBuilder {
builder: tiny_skia::PathBuilder::new(),
};
ttf_parser::OutlineBuilder::move_to(&mut b, 0.0, 0.0);
ttf_parser::OutlineBuilder::line_to(&mut b, 100.0, 0.0);
ttf_parser::OutlineBuilder::line_to(&mut b, 100.0, 100.0);
ttf_parser::OutlineBuilder::close(&mut b);
assert!(b.builder.finish().is_some());
}
#[test]
fn test_resolve_unknown_basefont_returns_none() {
assert!(resolve_standard14_substitute("NotARealFont-9999").is_none());
}
#[test]
fn test_resolve_helvetica_consistent() {
let r1 = resolve_standard14_substitute("Helvetica");
let r2 = resolve_standard14_substitute("Helvetica");
assert_eq!(r1.is_some(), r2.is_some());
if let (Some(p1), Some(p2)) = (&r1, &r2) {
assert_eq!(p1.0, p2.0);
}
}
}