Skip to content

Instantly share code, notes, and snippets.

@jaytaph
Created January 5, 2025 11:38
Show Gist options
  • Save jaytaph/f86ac0ee5d14666cc48fe3a90cd2353a to your computer and use it in GitHub Desktop.
Save jaytaph/f86ac0ee5d14666cc48fe3a90cd2353a to your computer and use it in GitHub Desktop.
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use anyhow::anyhow;
use fontconfig::{Fontconfig, Pattern};
use freetype::{Library, Face};
use log::info;
// We need to deal with the following:
// - fontconfig: what about Windows and MacOS, they use different systems
// - can we somehow not copy fontfaces but use references or something?
// - deal when font is not found (it now defaults to a certain font?)
struct FontManager {
// Font config manager that LOCATE all fonts through fontconfig library
fc: Fontconfig,
// Freetype library to load actual font faces
library: Library,
/// Cache of font faces that are loaded through freetype
face_cache: Arc<Mutex<HashMap<String, Face>>>,
/// Cache of font info that is loaded through fontconfig
info_cache: Arc<Mutex<HashMap<String, FontInfo>>>,
}
#[derive(Clone, Debug)]
struct FontInfo {
#[allow(unused)]
pub family: String,
#[allow(unused)]
pub style: String,
pub path: PathBuf,
pub index: Option<i32>,
}
impl FontManager {
fn new() -> Self {
let fc = Fontconfig::new().expect("unable to init Fontconfig");
let library = Library::init().expect("unable to init freetype library");
Self {
fc,
library,
face_cache: Arc::new(Mutex::new(HashMap::new())),
info_cache: Arc::new(Mutex::new(HashMap::new())),
}
}
/// Find a font face by family and style (ie: Arial Regular, Arial Bold etc). Returns a FontInfo struct
/// if the font has been found.
fn find(&self, family: &str, style: Option<&str>) -> Option<FontInfo> {
let cache_key = format!("{}:{}", family, style.unwrap_or("Regular"));
if let Some(font_info) = self.info_cache.lock().unwrap().get(&cache_key) {
info!(target: "font", "Font info loaded from cache: {}", cache_key);
return Some(font_info.clone());
}
// Create search pattern
let mut pat = Pattern::new(&self.fc);
let cs_family = std::ffi::CString::new(family).unwrap();
pat.add_string(fontconfig::FC_FAMILY, &cs_family);
if style.is_some() {
let cs_face = std::ffi::CString::new(style.unwrap()).unwrap();
pat.add_string(fontconfig::FC_STYLE, &cs_face);
}
let m = pat.font_match();
let font_info = FontInfo {
family: family.to_string(),
style: style.unwrap_or("Regular").to_string(),
path: PathBuf::from(m.filename().expect("no filename")),
index: m.face_index()
};
info!(target: "font", "Caching font info: {}", cache_key);
self.info_cache.lock().unwrap().insert(cache_key.clone(), font_info.clone());
Some(font_info)
}
/// Load the actual font face from disk or cache.
fn load(&self, font_info: FontInfo) -> Result<Face, anyhow::Error> {
let cache_key = format!("{}:{}", font_info.family, font_info.style);
if let Some(font_face) = self.face_cache.lock().unwrap().get(&cache_key) {
info!(target: "font", "Font loaded from cache: {}", cache_key);
// @todo: Can we somehow return the face within the cache so we don't need to copy it?
return Ok(font_face.clone());
}
let face = match self.library.new_face(font_info.path, font_info.index.unwrap_or(0) as isize) {
Ok(face) => face,
Err(e) => {
eprintln!("unable to load font: {}", e);
return Err(anyhow!("unable to load font"))
}
};
info!(target: "font",
"Font loaded: {} (number of glyphs: {})",
face.family_name().unwrap_or("Unknown".parse()?),
face.num_glyphs()
);
// @todo: same here.. we use a clone to store into cache, but can we just use the data we loaded through freetype?
info!(target: "font", "Caching font face: {}", cache_key);
self.face_cache.lock().unwrap().insert(cache_key.clone(), face.clone());
Ok(face)
}
}
fn main() {
colog::init();
let fm = FontManager::new();
let f = fm.find("Arial", Some("Regular")).expect("unable to find font");
let face = fm.load(f).expect("unable to load font");
let f = fm.find("Arial", Some("Regular")).expect("unable to find font");
let _face = fm.load(f).expect("unable to load font");
let f = fm.find("Arial", Some("Bold")).expect("unable to find font");
let _face = fm.load(f).expect("unable to load font");
let f = fm.find("Arial", Some("Regular")).expect("unable to find font");
let _face = fm.load(f).expect("unable to load font");
let f = fm.find("Monospace", Some("Bold")).expect("unable to find font");
let face = fm.load(f).expect("unable to load font");
face.set_char_size(40 * 64, 0, 50, 0).unwrap();
face.load_char('@' as usize, freetype::face::LoadFlag::NO_SCALE).unwrap();
let glyph = face.glyph();
let metrics = glyph.metrics();
let xmin = metrics.horiBearingX - 5;
let width = metrics.width + 10;
let ymin = -metrics.horiBearingY - 5;
let height = metrics.height + 10;
let outline = glyph.outline().unwrap();
println!("<?xml version=\"1.0\" standalone=\"no\"?>");
println!("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"");
println!("\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">");
println!(
"<svg viewBox=\"{} {} {} {}\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">",
xmin, ymin, width, height
);
for contour in outline.contours_iter() {
let start = contour.start();
println!(
"<path fill=\"none\" stroke=\"black\" stroke-width=\"1\" d=\"M {} {}",
start.x, -start.y
);
for curve in contour {
draw_curve(curve);
}
println!("Z \" />");
}
println!("</svg>");
}
fn draw_curve(curve: freetype::outline::Curve) {
match curve {
freetype::outline::Curve::Line(pt) => println!("L {} {}", pt.x, -pt.y),
freetype::outline::Curve::Bezier2(pt1, pt2) => {
println!("Q {} {} {} {}", pt1.x, -pt1.y, pt2.x, -pt2.y)
}
freetype::outline::Curve::Bezier3(pt1, pt2, pt3) => println!(
"C {} {} {} {} {} {}",
pt1.x, -pt1.y, pt2.x, -pt2.y, pt3.x, -pt3.y
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_manager() {
let fm = FontManager::new();
let f = fm.find("Arial", Some("Regular")).expect("unable to find font");
assert_eq!(f.path, PathBuf::from("/usr/share/fonts/truetype/msttcorefonts/Arial.ttf"));
assert_eq!(f.index, Some(0));
let f = fm.find("Arial", Some("Bold")).expect("unable to find font");
assert_eq!(f.path, PathBuf::from("/usr/share/fonts/truetype/msttcorefonts/Arial_Bold.ttf"));
assert_eq!(f.index, Some(0));
let f = fm.find("WingDings", Some("Bold")).expect("unable to find font");
assert_eq!(f.path, PathBuf::from("/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"));
assert_eq!(f.index, Some(0));
}
}
/* Cargo.toml
[package]
name = "untitled"
version = "0.1.0"
edition = "2024"
[dependencies]
fontconfig = "0.9.0"
freetype-rs = "0.37.0"
anyhow = "1.0.95"
logger = "0.4.0"
colog = "1.3.0"
log = "0.4.22"
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment