1#![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 pub name: Option<String>,
65 pub family: Option<String>,
67 pub italic: PatternMatch,
69 pub oblique: PatternMatch,
71 pub bold: PatternMatch,
73 pub monospace: PatternMatch,
75 pub condensed: PatternMatch,
77 pub weight: usize,
79 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#[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 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 #[cfg(not(all(feature = "std", feature = "parsing")))]
125 pub fn build() -> Self {
126 Self::default()
127 }
128
129 #[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 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 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 pub fn query_all(&self, pattern: &FcPattern) -> Vec<&FcFontPath> {
235 self.map
236 .iter() .filter(|(k, _)| Self::query_matches_internal(k, pattern))
238 .map(|(_, v)| v)
239 .collect()
240 }
241
242 pub fn query(&self, pattern: &FcPattern) -> Option<&FcFontPath> {
244 self.map
245 .iter() .find(|(k, _)| Self::query_matches_internal(k, pattern))
247 .map(|(_, v)| v)
248 }
249}
250
251#[cfg(feature = "std")]
252fn 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 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 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 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, },
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 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#[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; }
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 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 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 let font_index = 0;
627
628 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 detected_monospace = Some(post_table.header.is_fixed_pitch != 0);
649 }
650
651 if detected_monospace.is_none() {
652 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 let mut f_family = None;
696
697 let patterns = name_table
698 .name_records
699 .iter() .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() },
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}