diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a83707c..13bab2e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -676,6 +676,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -686,6 +695,18 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -2159,6 +2180,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -3901,11 +3928,13 @@ dependencies = [ "base64 0.21.0", "chrono", "comemo", + "dirs", "elsa", "enumset", "env_logger", "hex", "log", + "memmap2", "notify", "once_cell", "png", @@ -3919,6 +3948,7 @@ dependencies = [ "tokio", "typst", "typst-library", + "walkdir", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c93a2ce..9f4e9af 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,6 +34,9 @@ chrono = "0.4" png = "0.17" log = "0.4" env_logger = "0.10" +dirs = "5.0" +walkdir = "2.3" +memmap2 = "0.5" typst = { git = "https://github.com/typst/typst" } typst-library = { git = "https://github.com/typst/typst" } @@ -42,7 +45,8 @@ comemo = "0.3" [features] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL -default = ["custom-protocol"] +default = ["custom-protocol", "embed-fonts"] # this feature is used used for production builds where `devPath` points to the filesystem # DO NOT remove this custom-protocol = ["tauri/custom-protocol"] +embed-fonts = [] diff --git a/src-tauri/src/engine/engine.rs b/src-tauri/src/engine/engine.rs index c94685d..a2b5a6c 100644 --- a/src-tauri/src/engine/engine.rs +++ b/src-tauri/src/engine/engine.rs @@ -1,44 +1,23 @@ +use crate::engine::{FontSearcher, FontSlot}; use comemo::Prehashed; use typst::eval::Library; use typst::font::{Font, FontBook}; -use typst::util::Buffer; pub struct TypstEngine { pub library: Prehashed, pub fontbook: Prehashed, - pub fonts: Vec, + pub fonts: Vec, } impl TypstEngine { pub fn new() -> Self { - // https://github.com/typst/typst/blob/085282c138899dd5aaa06bc6ae7bd2f79d75d7e1/cli/src/main.rs#L695 - const EMBEDDED_FONTS: [&[u8]; 10] = [ - include_bytes!("../../assets/fonts/LinLibertine_R.ttf"), - include_bytes!("../../assets/fonts/LinLibertine_RB.ttf"), - include_bytes!("../../assets/fonts/LinLibertine_RBI.ttf"), - include_bytes!("../../assets/fonts/LinLibertine_RI.ttf"), - include_bytes!("../../assets/fonts/NewCMMath-Book.otf"), - include_bytes!("../../assets/fonts/NewCMMath-Regular.otf"), - include_bytes!("../../assets/fonts/DejaVuSansMono.ttf"), - include_bytes!("../../assets/fonts/DejaVuSansMono-Bold.ttf"), - include_bytes!("../../assets/fonts/DejaVuSansMono-Oblique.ttf"), - include_bytes!("../../assets/fonts/DejaVuSansMono-BoldOblique.ttf"), - ]; - - let mut fontbook = FontBook::new(); - let mut fonts = vec![]; - - for file in EMBEDDED_FONTS { - for font in Font::iter(Buffer::from_static(file)) { - fontbook.push(font.info().clone()); - fonts.push(font); - } - } + let mut searcher = FontSearcher::new(); + searcher.search(&[]); Self { library: Prehashed::new(typst_library::build()), - fontbook: Prehashed::new(fontbook), - fonts, + fontbook: Prehashed::new(searcher.book), + fonts: searcher.fonts, } } } diff --git a/src-tauri/src/engine/font.rs b/src-tauri/src/engine/font.rs new file mode 100644 index 0000000..5d6eebe --- /dev/null +++ b/src-tauri/src/engine/font.rs @@ -0,0 +1,153 @@ +use log::{debug, trace}; +use memmap2::Mmap; +use once_cell::sync::OnceCell; +use std::fs::File; +use std::path::{Path, PathBuf}; +use typst::font::{Font, FontBook, FontInfo}; +use typst::util::Buffer; +use walkdir::WalkDir; + +// Taken from typst-cli + +/// Holds details about the location of a font and lazily the font itself. +pub struct FontSlot { + pub path: PathBuf, + pub index: u32, + pub font: OnceCell>, +} + +pub struct FontSearcher { + pub book: FontBook, + pub fonts: Vec, +} + +impl FontSearcher { + /// Create a new, empty system searcher. + pub fn new() -> Self { + Self { + book: FontBook::new(), + fonts: vec![], + } + } + + /// Search everything that is available. + pub fn search(&mut self, font_paths: &[PathBuf]) { + self.search_system(); + + #[cfg(feature = "embed-fonts")] + self.search_embedded(); + + for path in font_paths { + self.search_dir(path); + } + + debug!("discovered {} fonts", self.fonts.len()); + } + + /// Add fonts that are embedded in the binary. + #[cfg(feature = "embed-fonts")] + fn search_embedded(&mut self) { + let mut search = |bytes: &'static [u8]| { + let buffer = Buffer::from_static(bytes); + for (i, font) in Font::iter(buffer).enumerate() { + self.book.push(font.info().clone()); + self.fonts.push(FontSlot { + path: PathBuf::new(), + index: i as u32, + font: OnceCell::from(Some(font)), + }); + } + }; + + // Embed default fonts. + search(include_bytes!("../../assets/fonts/LinLibertine_R.ttf")); + search(include_bytes!("../../assets/fonts/LinLibertine_RB.ttf")); + search(include_bytes!("../../assets/fonts/LinLibertine_RBI.ttf")); + search(include_bytes!("../../assets/fonts/LinLibertine_RI.ttf")); + search(include_bytes!("../../assets/fonts/NewCMMath-Book.otf")); + search(include_bytes!("../../assets/fonts/NewCMMath-Regular.otf")); + search(include_bytes!("../../assets/fonts/DejaVuSansMono.ttf")); + search(include_bytes!("../../assets/fonts/DejaVuSansMono-Bold.ttf")); + search(include_bytes!( + "../../assets/fonts/DejaVuSansMono-Oblique.ttf" + )); + search(include_bytes!( + "../../assets/fonts/DejaVuSansMono-BoldOblique.ttf" + )); + } + + /// Search for fonts in the linux system font directories. + #[cfg(all(unix, not(target_os = "macos")))] + fn search_system(&mut self) { + self.search_dir("/usr/share/fonts"); + self.search_dir("/usr/local/share/fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_dir(dir); + } + } + + /// Search for fonts in the macOS system font directories. + #[cfg(target_os = "macos")] + fn search_system(&mut self) { + self.search_dir("/Library/Fonts"); + self.search_dir("/Network/Library/Fonts"); + self.search_dir("/System/Library/Fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_dir(dir); + } + } + + /// Search for fonts in the Windows system font directories. + #[cfg(windows)] + fn search_system(&mut self) { + let windir = std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); + + self.search_dir(Path::new(&windir).join("Fonts")); + + if let Some(roaming) = dirs::config_dir() { + self.search_dir(roaming.join("Microsoft\\Windows\\Fonts")); + } + + if let Some(local) = dirs::cache_dir() { + self.search_dir(local.join("Microsoft\\Windows\\Fonts")); + } + } + + /// Search for all fonts in a directory recursively. + fn search_dir(&mut self, path: impl AsRef) { + for entry in WalkDir::new(path) + .follow_links(true) + .sort_by(|a, b| a.file_name().cmp(b.file_name())) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if matches!( + path.extension().and_then(|s| s.to_str()), + Some("ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC"), + ) { + self.search_file(path); + } + } + } + + /// Index the fonts in the file at the given path. + fn search_file(&mut self, path: impl AsRef) { + trace!("searching font file {:?}", path.as_ref()); + let path = path.as_ref(); + if let Ok(file) = File::open(path) { + if let Ok(mmap) = unsafe { Mmap::map(&file) } { + for (i, info) in FontInfo::iter(&mmap).enumerate() { + self.book.push(info); + self.fonts.push(FontSlot { + path: path.into(), + index: i as u32, + font: OnceCell::new(), + }); + } + } + } + } +} diff --git a/src-tauri/src/engine/mod.rs b/src-tauri/src/engine/mod.rs index 520992d..95bc43d 100644 --- a/src-tauri/src/engine/mod.rs +++ b/src-tauri/src/engine/mod.rs @@ -1,3 +1,5 @@ mod engine; +mod font; pub use engine::*; +pub use font::*; diff --git a/src-tauri/src/project/world.rs b/src-tauri/src/project/world.rs index 7730979..2daa20b 100644 --- a/src-tauri/src/project/world.rs +++ b/src-tauri/src/project/world.rs @@ -128,7 +128,13 @@ impl World for ProjectWorld { } fn font(&self, id: usize) -> Option { - self.engine.fonts.get(id).cloned() + let slot = &self.engine.fonts[id]; + slot.font + .get_or_init(|| { + let data = fs::read(&slot.path).map(Buffer::from).ok()?; + Font::new(data, slot.index) + }) + .clone() } fn file(&self, path: &Path) -> FileResult { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a5bf2c4..654cc8f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,10 @@ "distDir": "../build", "devPath": "http://localhost:5173", "beforeDevCommand": "pnpm run dev", - "beforeBuildCommand": "pnpm run build" + "beforeBuildCommand": "pnpm run build", + "features": [ + "embed-fonts" + ] }, "tauri": { "bundle": {