tinymist_world/font/
system.rs

1//! The font searcher to run the compiler in the system environment.
2
3use std::borrow::Cow;
4use std::path::{Path, PathBuf};
5
6use fontdb::Database;
7use rayon::iter::{IntoParallelIterator, ParallelIterator};
8use tinymist_std::error::prelude::*;
9use tinymist_vfs::system::LazyFile;
10use typst::diag::{FileError, FileResult};
11use typst::foundations::Bytes;
12use typst::text::FontInfo;
13
14use super::memory::MemoryFontSearcher;
15use super::{FontResolverImpl, FontSlot, LazyBufferFontLoader};
16use crate::config::CompileFontOpts;
17use crate::debug_loc::{DataSource, FsDataSource};
18
19/// Searches for fonts in the system.
20#[derive(Debug)]
21pub struct SystemFontSearcher {
22    /// The base font searcher.
23    base: MemoryFontSearcher,
24    /// Records user-specific font path when loading from directory or file for
25    /// debug.
26    pub font_paths: Vec<PathBuf>,
27    /// Stores font data loaded from file
28    db: Database,
29}
30
31impl SystemFontSearcher {
32    /// Creates a new searcher.
33    pub fn new() -> Self {
34        Self {
35            base: MemoryFontSearcher::default(),
36            font_paths: vec![],
37            db: Database::new(),
38        }
39    }
40
41    /// Builds a font resolver.
42    pub fn build(self) -> FontResolverImpl {
43        self.base.build().with_font_paths(self.font_paths)
44    }
45}
46
47impl SystemFontSearcher {
48    /// Resolves fonts from given options.
49    pub fn resolve_opts(&mut self, opts: CompileFontOpts) -> Result<()> {
50        // Note: the order of adding fonts is important.
51        // See: https://github.com/typst/typst/blob/9c7f31870b4e1bf37df79ebbe1df9a56df83d878/src/font/book.rs#L151-L154
52        // Source1: add the fonts specified by the user.
53        for path in opts.font_paths {
54            if path.is_dir() {
55                self.search_dir(&path);
56            } else {
57                let _ = self.search_file(&path);
58            }
59        }
60
61        // Source2: add the fonts from system paths.
62        if !opts.no_system_fonts {
63            self.search_system();
64        }
65
66        // Flush font db before adding fonts in memory
67        self.flush();
68
69        // Source3: add the fonts in memory.
70        self.add_memory_fonts(opts.with_embedded_fonts.into_par_iter().map(|font_data| {
71            match font_data {
72                Cow::Borrowed(data) => Bytes::new(data),
73                Cow::Owned(data) => Bytes::new(data),
74            }
75        }));
76
77        Ok(())
78    }
79
80    /// Flushes the searcher, needed before adding fonts in memory.
81    pub fn flush(&mut self) {
82        use fontdb::Source;
83
84        let face = self.db.faces().collect::<Vec<_>>();
85        let info = face.into_par_iter().flat_map(|face| {
86            let path = match &face.source {
87                Source::File(path) | Source::SharedFile(path, _) => path,
88                // We never add binary sources to the database, so there
89                // shouln't be any.
90                Source::Binary(_) => unreachable!(),
91            };
92
93            let info = self.db.with_face_data(face.id, FontInfo::new)??;
94            let slot = FontSlot::new(LazyBufferFontLoader::new(
95                LazyFile::new(path.clone()),
96                face.index,
97            ))
98            .with_describe(DataSource::Fs(FsDataSource {
99                path: path.to_str().unwrap_or_default().to_owned(),
100            }));
101
102            Some((info, slot))
103        });
104
105        self.base.extend(info.collect::<Vec<_>>());
106        self.db = Database::new();
107    }
108
109    /// Adds an in-memory font to the searcher.
110    pub fn add_memory_font(&mut self, data: Bytes) {
111        if !self.db.is_empty() {
112            panic!("dirty font search state, please flush the searcher before adding memory fonts");
113        }
114
115        self.base.add_memory_font(data);
116    }
117
118    /// Adds in-memory fonts to the searcher.
119    pub fn add_memory_fonts(&mut self, data: impl ParallelIterator<Item = Bytes>) {
120        if !self.db.is_empty() {
121            panic!("dirty font search state, please flush the searcher before adding memory fonts");
122        }
123
124        self.base.add_memory_fonts(data);
125    }
126
127    /// Searches for fonts in the system and adds them to the searcher.
128    pub fn search_system(&mut self) {
129        self.db.load_system_fonts();
130    }
131
132    /// Records a path to the searcher.
133    fn record_path(&mut self, path: &Path) {
134        self.font_paths.push(if !path.is_relative() {
135            path.to_owned()
136        } else {
137            let current_dir = std::env::current_dir();
138            match current_dir {
139                Ok(current_dir) => current_dir.join(path),
140                Err(_) => path.to_owned(),
141            }
142        });
143    }
144
145    /// Searches for all fonts in a directory recursively.
146    pub fn search_dir(&mut self, path: impl AsRef<Path>) {
147        self.record_path(path.as_ref());
148
149        self.db.load_fonts_dir(path);
150    }
151
152    /// Indexes the fonts in the file at the given path.
153    pub fn search_file(&mut self, path: impl AsRef<Path>) -> FileResult<()> {
154        self.record_path(path.as_ref());
155
156        self.db
157            .load_font_file(path.as_ref())
158            .map_err(|e| FileError::from_io(e, path.as_ref()))
159    }
160}
161
162impl Default for SystemFontSearcher {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170
171    #[test]
172    fn edit_fonts() {
173        use clap::Parser as _;
174
175        use crate::args::CompileOnceArgs;
176
177        let args = CompileOnceArgs::parse_from(["tinymist", "main.typ"]);
178        let mut verse = args
179            .resolve_system()
180            .expect("failed to resolve system universe");
181
182        // todo: a good way to edit fonts
183        let new_fonts = verse.font_resolver.clone();
184
185        verse.increment_revision(|verse| verse.set_fonts(new_fonts));
186    }
187}