font_index/
builder.rs

1#[cfg(feature = "emacs")]
2use super::emacs::{EMACS_CHARSET_MAP, SCRIPT_REPRESENTATIVE_CHARS};
3use super::index::*;
4use super::index_data::*;
5use super::library::FontLibrary;
6use super::system::{Os, OS};
7use super::types::*;
8use crate::util::string::SmallString;
9use std::{
10    fs,
11    path::{Path, PathBuf},
12    sync::RwLock,
13    time::SystemTime,
14};
15#[cfg(feature = "emacs")]
16use swash::text::Language;
17use swash::Tag;
18use swash::{Attributes, CacheKey, FontDataRef, FontRef, Stretch, StringId, Style, Weight};
19
20/// Hint for specifying whether font files should be memory mapped.
21#[derive(Copy, Clone, PartialEq, Eq, Debug)]
22pub enum MmapHint {
23    /// Never memory map.
24    Never,
25    /// Always memory map.
26    Always,
27    /// Memory map when file size is greater than or equal to a
28    /// threshold value.
29    Threshold(usize),
30}
31
32impl Default for MmapHint {
33    fn default() -> Self {
34        Self::Never
35    }
36}
37
38/// Builder for configuring a font library.
39#[derive(Default)]
40pub struct FontLibraryBuilder {
41    inner: Inner,
42    scanner: Scanner,
43    all_names: bool,
44    generics: bool,
45    fallbacks: bool,
46}
47
48impl FontLibraryBuilder {
49    /// Specifies whether all localized family names should be included
50    /// in the context.
51    pub fn all_names(&mut self, yes: bool) -> &mut Self {
52        self.all_names = yes;
53        self
54    }
55
56    /// Specifies a memory mapping hint.
57    pub fn mmap(&mut self, hint: MmapHint) -> &mut Self {
58        self.inner.mmap_hint = hint;
59        self
60    }
61
62    /// Adds fonts from the specified directory to the library.
63    pub fn add_dir(&mut self, path: impl AsRef<Path>) -> &mut Self {
64        self.scanner.scan_dir(path, self.all_names, &mut self.inner);
65        self
66    }
67
68    /// Adds a font file to the library.
69    pub fn add_file(&mut self, path: impl AsRef<Path>) -> &mut Self {
70        self.scanner
71            .scan_file(path, self.all_names, &mut self.inner);
72        self
73    }
74
75    /// Adds system fonts to the library.
76    pub fn add_system_fonts(&mut self) -> &mut Self {
77        match OS {
78            Os::Windows => {
79                if let Some(mut windir) = std::env::var_os("SYSTEMROOT") {
80                    windir.push("\\Fonts\\");
81                    self.add_dir(windir);
82                } else {
83                    self.add_dir("C:\\Windows\\Fonts\\");
84                }
85            }
86            Os::MacOs => {
87                self.add_dir("/System/Library/Fonts/");
88                self.add_dir("/Library/Fonts/");
89            }
90            Os::Ios => {
91                self.add_dir("/System/Library/Fonts/");
92                self.add_dir("/Library/Fonts/");
93            }
94            Os::Android => {
95                self.add_dir("/system/fonts/");
96            }
97            Os::Unix => {
98                self.add_dir("/usr/share/fonts/");
99                self.add_dir("/usr/local/share/fonts/");
100            }
101            Os::Other => {}
102        }
103        self
104    }
105
106    /// Adds user fonts to the library.
107    pub fn add_user_fonts(&mut self) -> &mut Self {
108        match OS {
109            Os::Windows => {}
110            Os::MacOs => {
111                if let Some(mut homedir) = std::env::var_os("HOME") {
112                    homedir.push("/Library/Fonts/");
113                    self.add_dir(&homedir);
114                }
115            }
116            Os::Ios => {}
117            Os::Android => {}
118            Os::Unix => {
119                if let Some(mut homedir) = std::env::var_os("HOME") {
120                    homedir.push("/.local/share/fonts/");
121                    self.add_dir(&homedir);
122                }
123            }
124            Os::Other => {}
125        }
126        self
127    }
128
129    /// Specifies whether default generic families should be mapped for the
130    /// current platform.
131    pub fn map_generic_families(&mut self, yes: bool) -> &mut Self {
132        self.generics = yes;
133        self
134    }
135
136    /// Specifies whether default fallbacks should be mapped for the current
137    /// platform.
138    pub fn map_fallbacks(&mut self, yes: bool) -> &mut Self {
139        self.fallbacks = yes;
140        self
141    }
142
143    /// Builds a library for the current configuration.
144    pub fn build(&mut self) -> FontLibrary {
145        #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
146        let fontconfig = load_fontconfig();
147        #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
148        {
149            let home = std::env::var("HOME");
150            fontconfig.dirs.iter().for_each(|dir| {
151                if dir.path.starts_with("~") {
152                    if let Ok(ref home) = home {
153                        let path = Path::new(home).join(dir.path.strip_prefix("~").unwrap());
154                        self.add_dir(path);
155                    }
156                } else {
157                    self.add_dir(dir.clone().path);
158                };
159            });
160        }
161        let mut index = StaticIndex::default();
162        core::mem::swap(&mut index, &mut self.inner.index);
163        for family in index.families.iter_mut() {
164            family
165                .fonts
166                .sort_unstable_by(|a, b| a.weight.cmp(&b.weight));
167        }
168        if self.generics {
169            index.setup_default_generic();
170        }
171        if self.fallbacks {
172            index.setup_default_fallbacks();
173        }
174
175        #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
176        {
177            use fontconfig_parser::Alias;
178
179            if self.generics {
180                fontconfig.aliases.iter().for_each(
181                    |Alias {
182                         alias,
183                         default,
184                         prefer,
185                         accept,
186                     }| {
187                        let names = [&prefer[..], &accept[..], &default[..]].concat();
188                        let find_family = |families: Vec<String>| {
189                            for family in families {
190                                if let Some(id) = index
191                                    .base
192                                    .family_map
193                                    .get(family.trim().to_lowercase().as_str())
194                                {
195                                    return Some(*id);
196                                }
197                            }
198                            None
199                        };
200                        if let Some(generic_family) = GenericFamily::parse(alias) {
201                            if let Some(id) = find_family(names.clone()) {
202                                index.generic[generic_family as usize] = Some(id);
203                            };
204                        }
205                    },
206                )
207            }
208        }
209
210        FontLibrary::new(index)
211    }
212}
213
214struct Inner {
215    path: PathBuf,
216    mmap: bool,
217    timestamp: SystemTime,
218    source: SourceId,
219    file_added: bool,
220    mmap_hint: MmapHint,
221    index: StaticIndex,
222    lowercase_name: String,
223}
224
225impl Default for Inner {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231impl Inner {
232    fn new() -> Self {
233        Self {
234            path: PathBuf::new(),
235            mmap: false,
236            timestamp: SystemTime::UNIX_EPOCH,
237            source: SourceId(0),
238            file_added: false,
239            mmap_hint: MmapHint::default(),
240            index: StaticIndex::default(),
241            lowercase_name: String::default(),
242        }
243    }
244}
245
246impl ScannerSink for Inner {
247    fn enter_file(&mut self, path: PathBuf, timestamp: SystemTime, size: u64) {
248        let mmap = match self.mmap_hint {
249            MmapHint::Never => false,
250            MmapHint::Always => true,
251            MmapHint::Threshold(value) => (value as u64) < size,
252        };
253        self.path = path;
254        self.mmap = mmap;
255        self.timestamp = timestamp;
256        self.source = SourceId(self.index.base.sources.len() as u32);
257        self.file_added = false;
258    }
259
260    fn add_font(&mut self, font: &FontInfo) {
261        self.lowercase_name.clear();
262        self.lowercase_name
263            .extend(font.name.chars().map(|c| c.to_lowercase()).flatten());
264        let index = &mut self.index;
265        let family =
266            if let Some(family_id) = index.base.family_map.get(self.lowercase_name.as_str()) {
267                let family = &mut index.families[family_id.to_usize()];
268                if family.contains(font.stretch, font.weight, font.style) {
269                    return;
270                }
271                family
272            } else {
273                let family_id = FamilyId(index.families.len() as u32);
274                let family = FamilyData {
275                    id: family_id,
276                    name: SmallString::new(&font.name),
277                    fonts: Vec::new(),
278                    has_stretch: true,
279                };
280                index.families.push(family);
281                index
282                    .base
283                    .family_map
284                    .insert(SmallString::new(&self.lowercase_name), family_id);
285                &mut index.families[family_id.to_usize()]
286            };
287        if !self.file_added {
288            self.file_added = true;
289            let mut path2 = PathBuf::new();
290            core::mem::swap(&mut path2, &mut self.path);
291            index.base.sources.push(SourceData {
292                id: self.source,
293                kind: SourceKind::File(FileData {
294                    path: path2.into(),
295                    mmap: self.mmap,
296                    timestamp: self.timestamp,
297                    status: RwLock::new(FileDataStatus::Empty),
298                }),
299            });
300        }
301        let font_id = FontId(index.base.fonts.len() as u32);
302        let family_id = family.id;
303        let font_data = FontData {
304            id: font_id,
305            family: family_id,
306            source: self.source,
307            index: font.index,
308            offset: font.offset,
309            attributes: font.attrs,
310            key: CacheKey::new(),
311        };
312        index.base.fonts.push(font_data);
313        family.fonts.push(FamilyFontData {
314            id: font_id,
315            stretch: font.stretch,
316            weight: font.weight,
317            style: font.style,
318            writing_systems: font.writing_systems.clone(),
319        });
320        if font.stretch != Stretch::NORMAL {
321            family.has_stretch = true;
322        }
323        for name in font.all_names() {
324            if !index.base.family_map.contains_key(name.as_str()) {
325                index
326                    .base
327                    .family_map
328                    .insert(SmallString::new(name.as_str()), family_id);
329            }
330        }
331
332        font.writing_systems
333            .iter()
334            .for_each(|(script_tag, language_tag, _)| {
335                index
336                    .script_tag_map
337                    .entry(*script_tag)
338                    .and_modify(|families| families.push(family_id))
339                    .or_insert(vec![family_id]);
340
341                index
342                    .language_tag_map
343                    .entry(*language_tag)
344                    .and_modify(|families| families.push(family_id))
345                    .or_insert(vec![family_id]);
346            });
347
348        #[cfg(feature = "emacs")]
349        for supported_charset in &font.supported_charsets {
350            index
351                .emacs_charset_map
352                .entry(SmallString::new(supported_charset.as_str()))
353                .and_modify(|familes| {
354                    if !familes.contains(&family_id) {
355                        familes.push(family_id);
356                    }
357                })
358                .or_insert(vec![family_id]);
359        }
360
361        #[cfg(feature = "emacs")]
362        for supported_script in &font.supported_scripts {
363            index
364                .emacs_script_map
365                .entry(SmallString::new(supported_script.as_str()))
366                .and_modify(|families| {
367                    if !families.contains(&family_id) {
368                        families.push(family_id);
369                    }
370                })
371                .or_insert(vec![family_id]);
372        }
373    }
374}
375
376#[derive(Default)]
377pub struct FontInfo {
378    pub offset: u32,
379    pub index: u32,
380    pub name: String,
381    pub attrs: Attributes,
382    pub stretch: Stretch,
383    pub weight: Weight,
384    pub style: Style,
385    all_names: Vec<String>,
386    name_count: usize,
387    pub writing_systems: Vec<(Tag, Tag, Vec<Tag>)>,
388    #[cfg(feature = "emacs")]
389    pub supported_charsets: Vec<SmallString>,
390    #[cfg(feature = "emacs")]
391    pub supported_scripts: Vec<SmallString>,
392}
393
394impl FontInfo {
395    pub fn all_names(&self) -> &[String] {
396        &self.all_names[..self.name_count]
397    }
398}
399
400pub trait ScannerSink {
401    fn enter_file(&mut self, path: PathBuf, timestamp: SystemTime, size: u64);
402    fn add_font(&mut self, font: &FontInfo);
403}
404
405#[derive(Default)]
406pub struct Scanner {
407    font: FontInfo,
408    name: String,
409}
410
411impl Scanner {
412    pub fn scan_dir(
413        &mut self,
414        path: impl AsRef<Path>,
415        all_names: bool,
416        sink: &mut impl ScannerSink,
417    ) -> Option<()> {
418        self.scan_dir_impl(path, all_names, sink, 0)
419    }
420
421    pub fn scan_file(
422        &mut self,
423        path: impl AsRef<Path>,
424        all_names: bool,
425        sink: &mut impl ScannerSink,
426    ) -> Option<()> {
427        let file = fs::File::open(path.as_ref()).ok()?;
428        let metadata = file.metadata().ok()?;
429        let timestamp = metadata.modified().ok()?;
430        let size = metadata.len();
431        let data = unsafe { memmap2::Mmap::map(&file).ok()? };
432        sink.enter_file(path.as_ref().into(), timestamp, size);
433        self.scan_data(&*data, all_names, |f| sink.add_font(f))
434    }
435
436    pub fn scan_data(
437        &mut self,
438        data: &[u8],
439        all_names: bool,
440        mut f: impl FnMut(&FontInfo),
441    ) -> Option<()> {
442        self.font.name.clear();
443        let font_data = FontDataRef::new(data)?;
444        for i in 0..font_data.len() {
445            if let Some(font) = font_data.get(i) {
446                self.scan_font(font, i as u32, all_names, &mut f);
447            }
448        }
449        Some(())
450    }
451
452    fn scan_dir_impl(
453        &mut self,
454        path: impl AsRef<Path>,
455        all_names: bool,
456        sink: &mut impl ScannerSink,
457        recurse: u32,
458    ) -> Option<()> {
459        if recurse > 4 {
460            return Some(());
461        }
462        let mut lower_ext = [0u8; 3];
463        for entry in fs::read_dir(path).ok()? {
464            if let Ok(entry) = entry {
465                let path = entry.path();
466                if path.is_file() {
467                    let mut is_dfont = false;
468                    match path.extension().map(|e| e.to_str()).flatten() {
469                        Some("dfont") => is_dfont = true,
470                        Some(ext) => {
471                            let ext = ext.as_bytes();
472                            if ext.len() != 3 {
473                                continue;
474                            }
475                            for i in 0..3 {
476                                lower_ext[i] = ext[i].to_ascii_lowercase();
477                            }
478                        }
479                        None => continue,
480                    };
481                    if !is_dfont {
482                        match &lower_ext {
483                            b"ttf" | b"otf" | b"ttc" | b"otc" => {}
484                            _ => continue,
485                        }
486                    }
487                    self.scan_file(&path, all_names, sink);
488                } else if path.is_dir() {
489                    self.scan_dir_impl(&path, all_names, sink, recurse + 1);
490                }
491            }
492        }
493        Some(())
494    }
495
496    fn scan_font(
497        &mut self,
498        font: FontRef,
499        index: u32,
500        all_names: bool,
501        f: &mut impl FnMut(&FontInfo),
502    ) -> Option<()> {
503        self.font.name_count = 0;
504        let strings = font.localized_strings();
505        let vars = font.variations();
506        let var_count = vars.len();
507        self.font.name.clear();
508        // Use typographic family for variable fonts that tend to encode the
509        // full style in the standard family name.
510        let mut nid = if var_count != 0 {
511            StringId::TypographicFamily
512        } else {
513            StringId::Family
514        };
515        if let Some(name) = strings.find_by_id(nid, Some("en")) {
516            self.font.name.extend(name.chars());
517        } else if let Some(name) = strings.find_by_id(nid, None) {
518            self.font.name.extend(name.chars());
519        }
520        if self.font.name.is_empty() {
521            nid = if nid == StringId::Family {
522                StringId::TypographicFamily
523            } else {
524                StringId::Family
525            };
526            if let Some(name) = strings.find_by_id(nid, Some("en")) {
527                self.name.extend(name.chars());
528            } else if let Some(name) = strings.find_by_id(nid, None) {
529                self.name.extend(name.chars());
530            }
531        }
532        if !self.name.is_empty() && self.name.len() < self.font.name.len() {
533            core::mem::swap(&mut self.name, &mut self.font.name);
534        }
535        if self.font.name.is_empty() {
536            if let Some(name) = strings.find_by_id(nid, Some("en")) {
537                self.font.name.extend(name.chars());
538            } else if let Some(name) = strings.find_by_id(nid, None) {
539                self.font.name.extend(name.chars());
540            }
541        }
542        if self.font.name.is_empty() {
543            return None;
544        }
545        self.font.attrs = font.attributes();
546        let (stretch, weight, style) = self.font.attrs.parts();
547        self.font.stretch = stretch;
548        self.font.weight = weight;
549        self.font.style = style;
550        self.font.index = index;
551        self.font.offset = font.offset;
552        let mut count = 0;
553        if all_names {
554            for name in strings
555                .clone()
556                .filter(|name| name.id() == nid && name.is_unicode())
557            {
558                if count >= self.font.all_names.len() {
559                    self.font.all_names.push(String::default());
560                }
561                let name_buf = &mut self.font.all_names[count];
562                count += 1;
563                name_buf.clear();
564                for ch in name.chars() {
565                    name_buf.extend(ch.to_lowercase());
566                }
567            }
568        }
569        self.font.name_count = count;
570        self.font.writing_systems = font
571            .writing_systems()
572            .map(|w| {
573                (
574                    w.script_tag(),
575                    w.language_tag(),
576                    w.features().map(|feature| feature.tag()).collect(),
577                )
578            })
579            .collect();
580
581        #[cfg(feature = "emacs")]
582        {
583            let supported_charsets: Vec<SmallString> = EMACS_CHARSET_MAP
584                .iter()
585                .filter_map(|(charset, (chars, language))| {
586                    if let Some(language) = language
587                        .as_ref()
588                        .map(|language| Language::parse(language.as_str()))
589                        .flatten()
590                        .map(|lang| lang.to_opentype())
591                        .flatten()
592                    {
593                        if self
594                            .font
595                            .writing_systems
596                            .iter()
597                            .find(|(_, language_tag, _)| *language_tag == language)
598                            .is_none()
599                        {
600                            return None;
601                        }
602                    }
603                    if chars
604                        .iter()
605                        .all(|codepoint| font.charmap().map(*codepoint) != 0)
606                    {
607                        return Some(SmallString::new(charset.as_str()));
608                    }
609
610                    None
611                })
612                .collect();
613
614            self.font.supported_charsets = supported_charsets;
615
616            let supported_scripts: Vec<SmallString> = SCRIPT_REPRESENTATIVE_CHARS
617                .iter()
618                .filter_map(|(script, chars)| {
619                    if chars
620                        .iter()
621                        .all(|codepoint| font.charmap().map(*codepoint) != 0)
622                    {
623                        return Some(SmallString::new(script.as_str()));
624                    }
625
626                    None
627                })
628                .collect();
629
630            self.font.supported_scripts = supported_scripts;
631        }
632
633        f(&self.font);
634        Some(())
635    }
636}
637
638#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
639fn load_fontconfig() -> fontconfig_parser::FontConfig {
640    let mut fontconfig = fontconfig_parser::FontConfig::default();
641    let home = std::env::var("HOME");
642
643    if let Ok(ref config_file) = std::env::var("FONTCONFIG_FILE") {
644        let _ = fontconfig.merge_config(Path::new(config_file));
645    } else {
646        let xdg_config_home = if let Ok(val) = std::env::var("XDG_CONFIG_HOME") {
647            Some(val.into())
648        } else if let Ok(ref home) = home {
649            // according to https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
650            // $XDG_CONFIG_HOME should default to $HOME/.config if not set
651            Some(Path::new(home).join(".config"))
652        } else {
653            None
654        };
655
656        let read_global = match xdg_config_home {
657            Some(p) => fontconfig
658                .merge_config(&p.join("fontconfig/fonts.conf"))
659                .is_err(),
660            None => true,
661        };
662
663        if read_global {
664            let _ = fontconfig.merge_config(Path::new("/etc/fonts/local.conf"));
665        }
666        let _ = fontconfig.merge_config(Path::new("/etc/fonts/fonts.conf"));
667    }
668    fontconfig
669}