dafont/
lib.rs

1//! Library for getting and matching system fonts with
2//! minimal dependencies
3//!
4//! # Usage
5//!
6//! ```rust
7//! use dafont::{FcFontCache, FcPattern};
8//!
9//! fn main() {
10//!
11//!     let cache = FcFontCache::build();
12//!     let results = cache.query(&FcPattern {
13//!         name: Some(String::from("Arial")),
14//!         .. Default::default()
15//!     });
16//!
17//!     println!("font results: {:?}", results);
18//! }
19//! ```
20
21#![allow(non_snake_case)]
22#![cfg_attr(not(feature = "std"), no_std)]
23
24#[cfg(feature = "parsing")]
25extern crate allsorts;
26#[cfg(all(not(target_family = "wasm"), feature = "std"))]
27extern crate mmapio;
28extern crate xmlparser;
29
30extern crate alloc;
31extern crate core;
32
33use alloc::borrow::ToOwned;
34use alloc::collections::btree_map::BTreeMap;
35use alloc::string::String;
36use alloc::vec::Vec;
37#[cfg(feature = "std")]
38use std::path::PathBuf;
39
40#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
41#[repr(C)]
42pub enum PatternMatch {
43    True,
44    False,
45    DontCare,
46}
47
48impl PatternMatch {
49    fn needs_to_match(&self) -> bool {
50        matches!(self, PatternMatch::True | PatternMatch::False)
51    }
52}
53
54impl Default for PatternMatch {
55    fn default() -> Self {
56        PatternMatch::DontCare
57    }
58}
59
60#[derive(Debug, Default, Clone, PartialOrd, Ord, PartialEq, Eq)]
61#[repr(C)]
62pub struct FcPattern {
63    // font name
64    pub name: Option<String>,
65    // family name
66    pub family: Option<String>,
67    // "italic" property
68    pub italic: PatternMatch,
69    // "oblique" property
70    pub oblique: PatternMatch,
71    // "bold" property
72    pub bold: PatternMatch,
73    // "monospace" property
74    pub monospace: PatternMatch,
75    // "condensed" property
76    pub condensed: PatternMatch,
77    // font weight
78    pub weight: usize,
79    // start..end unicode range
80    pub unicode_range: [usize; 2],
81}
82
83#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
84#[repr(C)]
85pub struct FcFontPath {
86    pub path: String,
87    pub font_index: usize,
88}
89
90/// Represent an in-memory font file
91#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
92#[repr(C)]
93pub struct FcFont {
94    pub bytes: Vec<u8>,
95    pub font_index: usize,
96}
97
98#[derive(Debug, Default, Clone, PartialOrd, Ord, PartialEq, Eq)]
99pub struct FcFontCache {
100    map: BTreeMap<FcPattern, FcFontPath>,
101}
102
103impl FcFontCache {
104    /// Adds in-memory font files (`path` will be base64 encoded)
105    pub fn with_memory_fonts(&mut self, f: &[(FcPattern, FcFont)]) -> &mut Self {
106        use base64::{engine::general_purpose::URL_SAFE, Engine as _};
107        self.map.extend(f.iter().map(|(k, v)| {
108            (
109                k.clone(),
110                FcFontPath {
111                    path: {
112                        let mut s = String::from("base64:");
113                        s.push_str(&URL_SAFE.encode(&v.bytes));
114                        s
115                    },
116                    font_index: v.font_index,
117                },
118            )
119        }));
120        self
121    }
122
123    /// Builds a new font cache
124    #[cfg(not(all(feature = "std", feature = "parsing")))]
125    pub fn build() -> Self {
126        Self::default()
127    }
128
129    /// Builds a new font cache from all fonts discovered on the system
130    ///
131    /// NOTE: Performance-intensive, should only be called on startup!
132    #[cfg(all(feature = "std", feature = "parsing"))]
133    pub fn build() -> Self {
134        #[cfg(target_os = "linux")]
135        {
136            FcFontCache {
137                map: FcScanDirectories()
138                    .unwrap_or_default()
139                    .into_iter()
140                    .collect(),
141            }
142        }
143
144        #[cfg(target_os = "windows")]
145        {
146            // `~` isn't actually valid on Windows, but it will be converted by `process_path`
147            let font_dirs = vec![
148                (None, "C:\\Windows\\Fonts\\".to_owned()),
149                (
150                    None,
151                    "~\\AppData\\Local\\Microsoft\\Windows\\Fonts\\".to_owned(),
152                ),
153            ];
154            FcFontCache {
155                map: FcScanDirectoriesInner(&font_dirs).into_iter().collect(),
156            }
157        }
158
159        #[cfg(target_os = "macos")]
160        {
161            let font_dirs = vec![
162                (None, "~/Library/Fonts".to_owned()),
163                (None, "/System/Library/Fonts".to_owned()),
164                (None, "/Library/Fonts".to_owned()),
165            ];
166            FcFontCache {
167                map: FcScanDirectoriesInner(&font_dirs).into_iter().collect(),
168            }
169        }
170
171        #[cfg(target_family = "wasm")]
172        {
173            Self::default()
174        }
175    }
176
177    /// Returns the list of fonts and font patterns
178    pub fn list(&self) -> &BTreeMap<FcPattern, FcFontPath> {
179        &self.map
180    }
181
182    fn query_matches_internal(k: &FcPattern, pattern: &FcPattern) -> bool {
183        let name_needs_to_match = pattern.name.is_some();
184        let family_needs_to_match = pattern.family.is_some();
185
186        let italic_needs_to_match = pattern.italic.needs_to_match();
187        let oblique_needs_to_match = pattern.oblique.needs_to_match();
188        let bold_needs_to_match = pattern.bold.needs_to_match();
189        let monospace_needs_to_match = pattern.monospace.needs_to_match();
190
191        let name_matches = k.name == pattern.name;
192        let family_matches = k.family == pattern.family;
193        let italic_matches = k.italic == pattern.italic;
194        let oblique_matches = k.oblique == pattern.oblique;
195        let bold_matches = k.bold == pattern.bold;
196        let monospace_matches = k.monospace == pattern.monospace;
197
198        if name_needs_to_match && !name_matches {
199            return false;
200        }
201
202        if family_needs_to_match && !family_matches {
203            return false;
204        }
205
206        if name_needs_to_match && !name_matches {
207            return false;
208        }
209
210        if family_needs_to_match && !family_matches {
211            return false;
212        }
213
214        if italic_needs_to_match && !italic_matches {
215            return false;
216        }
217
218        if oblique_needs_to_match && !oblique_matches {
219            return false;
220        }
221
222        if bold_needs_to_match && !bold_matches {
223            return false;
224        }
225
226        if monospace_needs_to_match && !monospace_matches {
227            return false;
228        }
229
230        true
231    }
232
233    /// Queries a font from the in-memory `font -> file` mapping, returns all matching fonts
234    pub fn query_all(&self, pattern: &FcPattern) -> Vec<&FcFontPath> {
235        self.map
236            .iter() // TODO: par_iter!
237            .filter(|(k, _)| Self::query_matches_internal(k, pattern))
238            .map(|(_, v)| v)
239            .collect()
240    }
241
242    /// Queries a font from the in-memory `font -> file` mapping, returns the first found font (early return)
243    pub fn query(&self, pattern: &FcPattern) -> Option<&FcFontPath> {
244        self.map
245            .iter() // TODO: par_iter!
246            .find(|(k, _)| Self::query_matches_internal(k, pattern))
247            .map(|(_, v)| v)
248    }
249}
250
251#[cfg(feature = "std")]
252/// Takes a path & prefix and resolves them to a usable path, or `None` if they're unsupported/unavailable.
253///
254/// Behaviour is based on: https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
255fn process_path(
256    prefix: &Option<String>,
257    mut path: PathBuf,
258    is_include_path: bool,
259) -> Option<PathBuf> {
260    use std::env::var;
261
262    const HOME_SHORTCUT: &str = "~";
263    const CWD_PATH: &str = ".";
264
265    const HOME_ENV_VAR: &str = "HOME";
266    const XDG_CONFIG_HOME_ENV_VAR: &str = "XDG_CONFIG_HOME";
267    const XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX: &str = ".config";
268    const XDG_DATA_HOME_ENV_VAR: &str = "XDG_DATA_HOME";
269    const XDG_DATA_HOME_DEFAULT_PATH_SUFFIX: &str = ".local/share";
270
271    const PREFIX_CWD: &str = "cwd";
272    const PREFIX_DEFAULT: &str = "default";
273    const PREFIX_XDG: &str = "xdg";
274
275    // These three could, in theory, be cached, but the work required to do so outweighs the minor benefits
276    fn get_home_value() -> Option<PathBuf> {
277        var(HOME_ENV_VAR).ok().map(PathBuf::from)
278    }
279    fn get_xdg_config_home_value() -> Option<PathBuf> {
280        var(XDG_CONFIG_HOME_ENV_VAR)
281            .ok()
282            .map(PathBuf::from)
283            .or_else(|| {
284                get_home_value()
285                    .map(|home_path| home_path.join(XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX))
286            })
287    }
288    fn get_xdg_data_home_value() -> Option<PathBuf> {
289        var(XDG_DATA_HOME_ENV_VAR)
290            .ok()
291            .map(PathBuf::from)
292            .or_else(|| {
293                get_home_value().map(|home_path| home_path.join(XDG_DATA_HOME_DEFAULT_PATH_SUFFIX))
294            })
295    }
296
297    // Resolve the tilde character in the path, if present
298    if path.starts_with(HOME_SHORTCUT) {
299        if let Some(home_path) = get_home_value() {
300            path = home_path.join(
301                path.strip_prefix(HOME_SHORTCUT)
302                    .expect("already checked that it starts with the prefix"),
303            );
304        } else {
305            return None;
306        }
307    }
308
309    // Resolve prefix values
310    match prefix {
311        Some(prefix) => match prefix.as_str() {
312            PREFIX_CWD | PREFIX_DEFAULT => {
313                let mut new_path = PathBuf::from(CWD_PATH);
314                new_path.push(path);
315
316                Some(new_path)
317            }
318            PREFIX_XDG => {
319                if is_include_path {
320                    get_xdg_config_home_value()
321                        .map(|xdg_config_home_path| xdg_config_home_path.join(path))
322                } else {
323                    get_xdg_data_home_value()
324                        .map(|xdg_data_home_path| xdg_data_home_path.join(path))
325                }
326            }
327            _ => None, // Unsupported prefix
328        },
329        None => Some(path),
330    }
331}
332
333#[cfg(all(feature = "std", feature = "parsing"))]
334fn FcScanDirectories() -> Option<Vec<(FcPattern, FcFontPath)>> {
335    use std::fs;
336    use std::path::Path;
337
338    const BASE_FONTCONFIG_PATH: &str = "/etc/fonts/fonts.conf";
339
340    if !Path::new(BASE_FONTCONFIG_PATH).exists() {
341        return None;
342    }
343
344    let mut font_paths = Vec::with_capacity(32);
345    let mut paths_to_visit = vec![(None, PathBuf::from(BASE_FONTCONFIG_PATH))];
346
347    while let Some((prefix, mut path_to_visit)) = paths_to_visit.pop() {
348        path_to_visit = match process_path(&prefix, path_to_visit, true) {
349            Some(path) => path,
350            None => continue,
351        };
352
353        let metadata = match fs::metadata(path_to_visit.as_path()) {
354            Ok(metadata) => metadata,
355            Err(_) => continue,
356        };
357
358        if metadata.is_file() {
359            let xml_utf8 = match fs::read_to_string(path_to_visit.as_path()) {
360                Ok(xml_utf8) => xml_utf8,
361                Err(_) => continue,
362            };
363
364            ParseFontsConf(xml_utf8.as_str(), &mut paths_to_visit, &mut font_paths);
365        } else if metadata.is_dir() {
366            let dir_entries = match fs::read_dir(path_to_visit) {
367                Ok(dir_entries) => dir_entries,
368                Err(_) => continue,
369            };
370
371            for dir_entry in dir_entries {
372                if let Ok(dir_entry) = dir_entry {
373                    let entry_path = dir_entry.path();
374
375                    // `fs::metadata` traverses symbolic links
376                    let metadata = match fs::metadata(entry_path.as_path()) {
377                        Ok(metadata) => metadata,
378                        Err(_) => continue,
379                    };
380
381                    if metadata.is_file() {
382                        if let Some(file_name) = entry_path.file_name() {
383                            let file_name_str = file_name.to_string_lossy();
384                            if file_name_str.starts_with(|c: char| c.is_ascii_digit())
385                                && file_name_str.ends_with(".conf")
386                            {
387                                paths_to_visit.push((None, entry_path));
388                            }
389                        }
390                    }
391                } else {
392                    return None;
393                }
394            }
395        }
396    }
397
398    if font_paths.is_empty() {
399        return None;
400    }
401
402    Some(FcScanDirectoriesInner(font_paths.as_slice()))
403}
404
405// Parses the fonts.conf file
406#[cfg(all(feature = "std", feature = "parsing"))]
407fn ParseFontsConf(
408    input: &str,
409    paths_to_visit: &mut Vec<(Option<String>, PathBuf)>,
410    font_paths: &mut Vec<(Option<String>, String)>,
411) -> Option<()> {
412    use xmlparser::Token::*;
413    use xmlparser::Tokenizer;
414
415    const TAG_INCLUDE: &str = "include";
416    const TAG_DIR: &str = "dir";
417    const ATTRIBUTE_PREFIX: &str = "prefix";
418
419    let mut current_prefix: Option<&str> = None;
420    let mut current_path: Option<&str> = None;
421    let mut is_in_include = false;
422    let mut is_in_dir = false;
423
424    for token in Tokenizer::from(input) {
425        let token = token.ok()?;
426        match token {
427            ElementStart { local, .. } => {
428                if is_in_include || is_in_dir {
429                    return None; /* error: nested tags */
430                }
431
432                match local.as_str() {
433                    TAG_INCLUDE => {
434                        is_in_include = true;
435                    }
436                    TAG_DIR => {
437                        is_in_dir = true;
438                    }
439                    _ => continue,
440                }
441
442                current_path = None;
443            }
444            Text { text, .. } => {
445                let text = text.as_str().trim();
446                if text.is_empty() {
447                    continue;
448                }
449                if is_in_include || is_in_dir {
450                    current_path = Some(text);
451                }
452            }
453            Attribute { local, value, .. } => {
454                if !is_in_include && !is_in_dir {
455                    continue;
456                }
457                // attribute on <include> or <dir> node
458                if local.as_str() == ATTRIBUTE_PREFIX {
459                    current_prefix = Some(value.as_str());
460                }
461            }
462            ElementEnd { end, .. } => {
463                let end_tag = match end {
464                    xmlparser::ElementEnd::Close(_, a) => a,
465                    _ => continue,
466                };
467
468                match end_tag.as_str() {
469                    TAG_INCLUDE => {
470                        if !is_in_include {
471                            continue;
472                        }
473
474                        if let Some(current_path) = current_path.as_ref() {
475                            paths_to_visit.push((
476                                current_prefix.map(ToOwned::to_owned),
477                                PathBuf::from(*current_path),
478                            ));
479                        }
480                    }
481                    TAG_DIR => {
482                        if !is_in_dir {
483                            continue;
484                        }
485
486                        if let Some(current_path) = current_path.as_ref() {
487                            font_paths.push((
488                                current_prefix.map(ToOwned::to_owned),
489                                (*current_path).to_owned(),
490                            ));
491                        }
492                    }
493                    _ => continue,
494                }
495
496                is_in_include = false;
497                is_in_dir = false;
498                current_path = None;
499                current_prefix = None;
500            }
501            _ => {}
502        }
503    }
504
505    Some(())
506}
507
508#[cfg(all(feature = "std", feature = "parsing"))]
509fn FcScanDirectoriesInner(paths: &[(Option<String>, String)]) -> Vec<(FcPattern, FcFontPath)> {
510    #[cfg(feature = "multithreading")]
511    {
512        use rayon::prelude::*;
513
514        // scan directories in parallel
515        paths
516            .par_iter()
517            .filter_map(|(prefix, p)| {
518                if let Some(path) = process_path(prefix, PathBuf::from(p), false) {
519                    Some(FcScanSingleDirectoryRecursive(path))
520                } else {
521                    None
522                }
523            })
524            .flatten()
525            .collect()
526    }
527    #[cfg(not(feature = "multithreading"))]
528    {
529        paths
530            .iter()
531            .filter_map(|(prefix, p)| {
532                if let Some(path) = process_path(prefix, PathBuf::from(p), false) {
533                    Some(FcScanSingleDirectoryRecursive(path))
534                } else {
535                    None
536                }
537            })
538            .flatten()
539            .collect()
540    }
541}
542
543#[cfg(all(feature = "std", feature = "parsing"))]
544fn FcScanSingleDirectoryRecursive(dir: PathBuf) -> Vec<(FcPattern, FcFontPath)> {
545    let mut files_to_parse = Vec::new();
546    let mut dirs_to_parse = vec![dir];
547
548    'outer: loop {
549        let mut new_dirs_to_parse = Vec::new();
550
551        'inner: for dir in dirs_to_parse.clone() {
552            let dir = match std::fs::read_dir(dir) {
553                Ok(o) => o,
554                Err(_) => continue 'inner,
555            };
556
557            for (path, pathbuf) in dir.filter_map(|entry| {
558                let entry = entry.ok()?;
559                let path = entry.path();
560                let pathbuf = path.to_path_buf();
561                Some((path, pathbuf))
562            }) {
563                if path.is_dir() {
564                    new_dirs_to_parse.push(pathbuf);
565                } else {
566                    files_to_parse.push(pathbuf);
567                }
568            }
569        }
570
571        if new_dirs_to_parse.is_empty() {
572            break 'outer;
573        } else {
574            dirs_to_parse = new_dirs_to_parse;
575        }
576    }
577
578    FcParseFontFiles(&files_to_parse)
579}
580
581#[cfg(all(feature = "std", feature = "parsing"))]
582fn FcParseFontFiles(files_to_parse: &[PathBuf]) -> Vec<(FcPattern, FcFontPath)> {
583    let result = {
584        #[cfg(feature = "multithreading")]
585        {
586            use rayon::prelude::*;
587
588            files_to_parse
589                .par_iter()
590                .filter_map(|file| FcParseFont(file))
591                .collect::<Vec<Vec<_>>>()
592        }
593        #[cfg(not(feature = "multithreading"))]
594        {
595            files_to_parse
596                .iter()
597                .filter_map(|file| FcParseFont(file))
598                .collect::<Vec<Vec<_>>>()
599        }
600    };
601
602    result.into_iter().flat_map(|f| f.into_iter()).collect()
603}
604
605#[cfg(all(feature = "std", feature = "parsing"))]
606fn FcParseFont(filepath: &PathBuf) -> Option<Vec<(FcPattern, FcFontPath)>> {
607    use allsorts::{
608        binary::read::ReadScope,
609        font_data::FontData,
610        get_name::fontcode_get_name,
611        post::PostTable,
612        tables::{
613            os2::Os2, FontTableProvider, HeadTable, HheaTable, HmtxTable, MaxpTable, NameTable,
614        },
615        tag,
616    };
617    #[cfg(all(not(target_family = "wasm"), feature = "std"))]
618    use mmapio::MmapOptions;
619    use std::collections::BTreeSet;
620    use std::fs::File;
621
622    const FONT_SPECIFIER_NAME_ID: u16 = 4;
623    const FONT_SPECIFIER_FAMILY_ID: u16 = 1;
624
625    // font_index = 0 - TODO: iterate through fonts in font file properly!
626    let font_index = 0;
627
628    // try parsing the font file and see if the postscript name matches
629    let file = File::open(filepath).ok()?;
630    #[cfg(all(not(target_family = "wasm"), feature = "std"))]
631    let font_bytes = unsafe { MmapOptions::new().map(&file).ok()? };
632    #[cfg(not(all(not(target_family = "wasm"), feature = "std")))]
633    let font_bytes = std::fs::read(filepath).ok()?;
634    let scope = ReadScope::new(&font_bytes[..]);
635    let font_file = scope.read::<FontData<'_>>().ok()?;
636    let provider = font_file.table_provider(font_index).ok()?;
637
638    let head_data = provider.table_data(tag::HEAD).ok()??.into_owned();
639    let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
640
641    let is_bold = head_table.is_bold();
642    let is_italic = head_table.is_italic();
643    let mut detected_monospace = None;
644
645    let post_data = provider.table_data(tag::POST).ok()??;
646    if let Ok(post_table) = ReadScope::new(&post_data).read::<PostTable>() {
647        // isFixedPitch here - https://learn.microsoft.com/en-us/typography/opentype/spec/post#header
648        detected_monospace = Some(post_table.header.is_fixed_pitch != 0);
649    }
650
651    if detected_monospace.is_none() {
652        // https://learn.microsoft.com/en-us/typography/opentype/spec/os2#panose
653        // Table 20 here - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6OS2.html
654        let os2_data = provider.table_data(tag::OS_2).ok()??;
655        let os2_table = ReadScope::new(&os2_data)
656            .read_dep::<Os2>(os2_data.len())
657            .ok()?;
658        let monospace = os2_table.panose[0] == 2;
659        detected_monospace = Some(monospace);
660    }
661
662    if detected_monospace.is_none() {
663        let hhea_data = provider.table_data(tag::HHEA).ok()??;
664        let hhea_table = ReadScope::new(&hhea_data).read::<HheaTable>().ok()?;
665        let maxp_data = provider.table_data(tag::MAXP).ok()??;
666        let maxp_table = ReadScope::new(&maxp_data).read::<MaxpTable>().ok()?;
667        let hmtx_data = provider.table_data(tag::HMTX).ok()??;
668        let hmtx_table = ReadScope::new(&hmtx_data)
669            .read_dep::<HmtxTable<'_>>((
670                usize::from(maxp_table.num_glyphs),
671                usize::from(hhea_table.num_h_metrics),
672            ))
673            .ok()?;
674
675        let mut monospace = true;
676        let mut last_advance = 0;
677        for i in 0..hhea_table.num_h_metrics as usize {
678            let advance = hmtx_table.h_metrics.read_item(i).ok()?.advance_width;
679            if i > 0 && advance != last_advance {
680                monospace = false;
681                break;
682            }
683            last_advance = advance;
684        }
685
686        detected_monospace = Some(monospace);
687    }
688
689    let is_monospace = detected_monospace.unwrap_or(false);
690
691    let name_data = provider.table_data(tag::NAME).ok()??.into_owned();
692    let name_table = ReadScope::new(&name_data).read::<NameTable>().ok()?;
693
694    // one font can support multiple patterns
695    let mut f_family = None;
696
697    let patterns = name_table
698        .name_records
699        .iter() // TODO: par_iter
700        .filter_map(|name_record| {
701            let name_id = name_record.name_id;
702            if name_id == FONT_SPECIFIER_FAMILY_ID {
703                let family = fontcode_get_name(&name_data, FONT_SPECIFIER_FAMILY_ID).ok()??;
704                f_family = Some(family);
705                None
706            } else if name_id == FONT_SPECIFIER_NAME_ID {
707                let family = f_family.as_ref()?;
708                let name = fontcode_get_name(&name_data, FONT_SPECIFIER_NAME_ID).ok()??;
709                if name.to_bytes().is_empty() {
710                    None
711                } else {
712                    Some((
713                        FcPattern {
714                            name: Some(String::from_utf8_lossy(name.to_bytes()).to_string()),
715                            family: Some(String::from_utf8_lossy(family.as_bytes()).to_string()),
716                            bold: if is_bold {
717                                PatternMatch::True
718                            } else {
719                                PatternMatch::False
720                            },
721                            italic: if is_italic {
722                                PatternMatch::True
723                            } else {
724                                PatternMatch::False
725                            },
726                            monospace: if is_monospace {
727                                PatternMatch::True
728                            } else {
729                                PatternMatch::False
730                            },
731                            ..Default::default() // TODO!
732                        },
733                        font_index,
734                    ))
735                }
736            } else {
737                None
738            }
739        })
740        .collect::<BTreeSet<_>>();
741
742    Some(
743        patterns
744            .into_iter()
745            .map(|(pat, index)| {
746                (
747                    pat,
748                    FcFontPath {
749                        path: filepath.to_string_lossy().to_string(),
750                        font_index: index,
751                    },
752                )
753            })
754            .collect(),
755    )
756}
757
758#[cfg(all(feature = "std", feature = "parsing"))]
759pub fn get_font_name(font_path: &FcFontPath) -> Option<(String, String)> {
760    use allsorts::{
761        binary::read::ReadScope,
762        font_data::FontData,
763        get_name::fontcode_get_name,
764        tables::{FontTableProvider, NameTable},
765        tag,
766    };
767
768    const FONT_SPECIFIER_NAME_ID: u16 = 4;
769    const FONT_SPECIFIER_FAMILY_ID: u16 = 1;
770
771    let font_bytes = std::fs::read(&font_path.path).ok()?;
772    let scope = ReadScope::new(&font_bytes[..]);
773    let font_file = scope.read::<FontData<'_>>().ok()?;
774    let provider = font_file.table_provider(font_path.font_index).ok()?;
775
776    let name_data = provider.table_data(tag::NAME).ok()??.into_owned();
777    let name_table = ReadScope::new(&name_data).read::<NameTable>().ok()?;
778
779    let mut font_family = None;
780    let mut font_name = None;
781
782    for name_record in name_table.name_records.iter() {
783        match name_record.name_id {
784            FONT_SPECIFIER_FAMILY_ID => {
785                if let Ok(Some(family)) = fontcode_get_name(&name_data, FONT_SPECIFIER_FAMILY_ID) {
786                    font_family = Some(String::from_utf8_lossy(family.as_bytes()).to_string());
787                }
788            }
789            FONT_SPECIFIER_NAME_ID => {
790                if let Ok(Some(name)) = fontcode_get_name(&name_data, FONT_SPECIFIER_NAME_ID) {
791                    font_name = Some(String::from_utf8_lossy(name.to_bytes()).to_string());
792                }
793            }
794            _ => continue,
795        }
796
797        if font_family.is_some() && font_name.is_some() {
798            break;
799        }
800    }
801
802    if let (Some(family), Some(name)) = (font_family, font_name) {
803        Some((family, name))
804    } else {
805        None
806    }
807}