use std::any::Any;
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
use typst_library::foundations::Bytes;
use typst_library::text::{Font, FontBook, FontInfo};
use typst_utils::LazyHash;
pub struct FontStore {
book: LazyHash<FontBook>,
slots: Vec<FontSlot>,
}
impl FontStore {
pub fn new() -> Self {
Self {
book: LazyHash::new(FontBook::new()),
slots: Vec::new(),
}
}
pub fn push(&mut self, entry: (impl FontSource, FontInfo)) {
self.book.push(entry.1);
self.slots
.push(FontSlot { source: Box::new(entry.0), font: OnceLock::new() });
}
pub fn extend<T>(&mut self, entries: impl IntoIterator<Item = (T, FontInfo)>)
where
T: FontSource,
{
for entry in entries {
self.push(entry);
}
}
pub fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
pub fn font(&self, index: usize) -> Option<Font> {
self.slots.get(index)?.get()
}
pub fn source(&self, index: usize) -> Option<&dyn FontSource> {
Some(&*self.slots.get(index)?.source)
}
}
impl Default for FontStore {
fn default() -> Self {
Self::new()
}
}
struct FontSlot {
source: Box<dyn FontSource>,
font: OnceLock<Option<Font>>,
}
impl FontSlot {
fn get(&self) -> Option<Font> {
self.font.get_or_init(|| self.source.load()).clone()
}
}
pub trait FontSource: Send + Sync + Any {
fn load(&self) -> Option<Font>;
}
impl FontSource for Font {
fn load(&self) -> Option<Font> {
Some(self.clone())
}
}
impl FontSource for FontPath {
fn load(&self) -> Option<Font> {
let _scope = typst_timing::TimingScope::new("load font");
let data = fs::read(&self.path).ok()?;
Font::new(Bytes::new(data), self.index)
}
}
#[derive(Debug)]
pub struct FontPath {
pub path: PathBuf,
pub index: u32,
}
#[cfg(feature = "embedded-fonts")]
pub fn embedded() -> impl Iterator<Item = (Font, FontInfo)> {
typst_assets::fonts().flat_map(|data| {
Font::iter(Bytes::new(data)).map(|font| {
let info = font.info().clone();
(font, info)
})
})
}
#[cfg(feature = "scan-fonts")]
pub fn system() -> impl Iterator<Item = (FontPath, FontInfo)> {
let _scope = typst_timing::TimingScope::new("scan system fonts");
with_db(|db| {
db.load_system_fonts();
#[cfg(any(target_os = "windows", target_os = "macos"))]
load_adobe_fonts(db);
})
}
#[cfg(feature = "scan-fonts")]
pub fn scan(path: &std::path::Path) -> impl Iterator<Item = (FontPath, FontInfo)> {
let _scope = typst_timing::TimingScope::new("scan system fonts");
with_db(move |db| db.load_fonts_dir(path))
}
#[cfg(feature = "scan-fonts")]
fn with_db(
f: impl FnOnce(&mut fontdb::Database),
) -> impl Iterator<Item = (FontPath, FontInfo)> {
let mut db = fontdb::Database::new();
f(&mut db);
db.faces()
.filter_map(|face| {
let path = match &face.source {
fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => path,
fontdb::Source::Binary(_) => return None,
};
let info = db
.with_face_data(face.id, FontInfo::new)
.expect("database must contain this font")?;
let path = FontPath { path: path.clone(), index: face.index };
Some((path, info))
})
.collect::<Vec<_>>()
.into_iter()
}
#[cfg(all(feature = "scan-fonts", any(target_os = "windows", target_os = "macos")))]
fn load_adobe_fonts(db: &mut fontdb::Database) {
let Some(data) = dirs::data_dir() else { return };
let base = data.join("Adobe");
let prefix = if cfg!(target_os = "macos") { "." } else { "" };
let subdirs = [
format!("CoreSync/plugins/livetype/{prefix}r"),
format!("{prefix}User Owned Fonts"),
];
for subdir in subdirs {
let Ok(entries) = fs::read_dir(base.join(subdir)) else { return };
for entry in entries.flatten() {
let Ok(metadata) = entry.metadata() else { continue };
if metadata.is_file() {
db.load_font_file(entry.path()).ok();
}
}
}
}