Created
January 5, 2025 11:38
-
-
Save jaytaph/f86ac0ee5d14666cc48fe3a90cd2353a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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