1/*!
2`fontdb` is a simple, in-memory font database with CSS-like queries.
3
4# Features
5
6- The database can load fonts from files, directories and raw data (`Vec<u8>`).
7- The database can match a font using CSS-like queries. See `Database::query`.
8- The database can try to load system fonts.
9 Currently, this is implemented by scanning predefined directories.
10 The library does not interact with the system API.
11- Provides a unique ID for each font face.
12
13# Non-goals
14
15- Advanced font properties querying.<br>
16 The database provides only storage and matching capabilities.
17 For font properties querying you can use [ttf-parser].
18
19- A font fallback mechanism.<br>
20 This library can be used to implement a font fallback mechanism, but it doesn't implement one.
21
22- Application's global database.<br>
23 The database doesn't use `static`, therefore it's up to the caller where it should be stored.
24
25- Font types support other than TrueType.
26
27# Font vs Face
28
29A font is a collection of font faces. Therefore, a font face is a subset of a font.
30A simple font (\*.ttf/\*.otf) usually contains a single font face,
31but a font collection (\*.ttc) can contain multiple font faces.
32
33`fontdb` stores and matches font faces, not fonts.
34Therefore, after loading a font collection with 5 faces (for example), the database will be populated
35with 5 `FaceInfo` objects, all of which will be pointing to the same file or binary data.
36
37# Performance
38
39The database performance is largely limited by the storage itself.
40We are using [ttf-parser], so the parsing should not be a bottleneck.
41
42On my machine with Samsung SSD 860 and Gentoo Linux, it takes ~20ms
43to load 1906 font faces (most of them are from Google Noto collection)
44with a hot disk cache and ~860ms with a cold one.
45
46On Mac Mini M1 it takes just 9ms to load 898 fonts.
47
48# Safety
49
50The library relies on memory-mapped files, which is inherently unsafe.
51But since we do not keep the files open it should be perfectly safe.
52
53If you would like to use a persistent memory mapping of the font files,
54then you can use the unsafe [`Database::make_shared_face_data`] function.
55
56[ttf-parser]: https://github.com/RazrFalcon/ttf-parser
57*/
58
59#![cfg_attr(not(feature = "std"), no_std)]
60#![warn(missing_docs)]
61#![warn(missing_debug_implementations)]
62#![warn(missing_copy_implementations)]
63
64extern crate alloc;
65
66#[cfg(not(feature = "std"))]
67use alloc::{
68 string::{String, ToString},
69 vec::Vec,
70};
71
72pub use ttf_parser::Language;
73pub use ttf_parser::Width as Stretch;
74
75use slotmap::SlotMap;
76use tinyvec::TinyVec;
77
78/// A unique per database face ID.
79///
80/// Since `Database` is not global/unique, we cannot guarantee that a specific ID
81/// is actually from the same db instance. This is up to the caller.
82///
83/// ID overflow will cause a panic, but it's highly unlikely that someone would
84/// load more than 4 billion font faces.
85///
86/// Because the internal representation of ID is private, The `Display` trait
87/// implementation for this type only promise that unequal IDs will be displayed
88/// as different strings, but does not make any guarantees about format or
89/// content of the strings.
90///
91/// [`KeyData`]: https://docs.rs/slotmap/latest/slotmap/struct.KeyData.html
92#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)]
93pub struct ID(InnerId);
94
95slotmap::new_key_type! {
96 /// Internal ID type.
97 struct InnerId;
98}
99
100impl ID {
101 /// Creates a dummy ID.
102 ///
103 /// Should be used in tandem with [`Database::push_face_info`].
104 #[inline]
105 pub fn dummy() -> Self {
106 Self(InnerId::from(slotmap::KeyData::from_ffi(core::u64::MAX)))
107 }
108}
109
110impl core::fmt::Display for ID {
111 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112 write!(f, "{}", (self.0).0.as_ffi())
113 }
114}
115
116/// A list of possible font loading errors.
117#[derive(Debug)]
118enum LoadError {
119 /// A malformed font.
120 ///
121 /// Typically means that [ttf-parser](https://github.com/RazrFalcon/ttf-parser)
122 /// wasn't able to parse it.
123 MalformedFont,
124 /// A valid TrueType font without a valid *Family Name*.
125 UnnamedFont,
126 /// A file IO related error.
127 #[cfg(feature = "std")]
128 IoError(std::io::Error),
129}
130
131#[cfg(feature = "std")]
132impl From<std::io::Error> for LoadError {
133 #[inline]
134 fn from(e: std::io::Error) -> Self {
135 LoadError::IoError(e)
136 }
137}
138
139impl core::fmt::Display for LoadError {
140 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141 match self {
142 LoadError::MalformedFont => write!(f, "malformed font"),
143 LoadError::UnnamedFont => write!(f, "font doesn't have a family name"),
144 #[cfg(feature = "std")]
145 LoadError::IoError(ref e: &Error) => write!(f, "{}", e),
146 }
147 }
148}
149
150/// A font database.
151#[derive(Clone, Debug)]
152pub struct Database {
153 faces: SlotMap<InnerId, FaceInfo>,
154 family_serif: String,
155 family_sans_serif: String,
156 family_cursive: String,
157 family_fantasy: String,
158 family_monospace: String,
159}
160
161impl Default for Database {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl Database {
168 /// Create a new, empty `Database`.
169 ///
170 /// Generic font families would be set to:
171 ///
172 /// - `serif` - Times New Roman
173 /// - `sans-serif` - Arial
174 /// - `cursive` - Comic Sans MS
175 /// - `fantasy` - Impact (Papyrus on macOS)
176 /// - `monospace` - Courier New
177 #[inline]
178 pub fn new() -> Self {
179 Database {
180 faces: SlotMap::with_key(),
181 family_serif: "Times New Roman".to_string(),
182 family_sans_serif: "Arial".to_string(),
183 family_cursive: "Comic Sans MS".to_string(),
184 #[cfg(not(target_os = "macos"))]
185 family_fantasy: "Impact".to_string(),
186 #[cfg(target_os = "macos")]
187 family_fantasy: "Papyrus".to_string(),
188 family_monospace: "Courier New".to_string(),
189 }
190 }
191
192 /// Loads a font data into the `Database`.
193 ///
194 /// Will load all font faces in case of a font collection.
195 pub fn load_font_data(&mut self, data: Vec<u8>) {
196 self.load_font_source(Source::Binary(alloc::sync::Arc::new(data)));
197 }
198
199 /// Loads a font from the given source into the `Database` and returns
200 /// the ID of the loaded font.
201 ///
202 /// Will load all font faces in case of a font collection.
203 pub fn load_font_source(&mut self, source: Source) -> TinyVec<[ID; 8]> {
204 let ids = source.with_data(|data| {
205 let n = ttf_parser::fonts_in_collection(data).unwrap_or(1);
206 let mut ids = TinyVec::with_capacity(n as usize);
207
208 for index in 0..n {
209 match parse_face_info(source.clone(), data, index) {
210 Ok(mut info) => {
211 let id = self.faces.insert_with_key(|k| {
212 info.id = ID(k);
213 info
214 });
215 ids.push(ID(id));
216 }
217 Err(e) => log::warn!(
218 "Failed to load a font face {} from source cause {}.",
219 index,
220 e
221 ),
222 }
223 }
224
225 ids
226 });
227
228 ids.unwrap_or_default()
229 }
230
231 /// Backend function used by load_font_file to load font files.
232 #[cfg(feature = "fs")]
233 fn load_fonts_from_file(&mut self, path: &std::path::Path, data: &[u8]) {
234 let source = Source::File(path.into());
235
236 let n = ttf_parser::fonts_in_collection(data).unwrap_or(1);
237 for index in 0..n {
238 match parse_face_info(source.clone(), data, index) {
239 Ok(info) => self.push_face_info(info),
240 Err(e) => {
241 log::warn!(
242 "Failed to load a font face {} from '{}' cause {}.",
243 index,
244 path.display(),
245 e
246 )
247 }
248 }
249 }
250 }
251
252 /// Loads a font file into the `Database`.
253 ///
254 /// Will load all font faces in case of a font collection.
255 #[cfg(all(feature = "fs", feature = "memmap"))]
256 pub fn load_font_file<P: AsRef<std::path::Path>>(
257 &mut self,
258 path: P,
259 ) -> Result<(), std::io::Error> {
260 self.load_font_file_impl(path.as_ref())
261 }
262
263 // A non-generic version.
264 #[cfg(all(feature = "fs", feature = "memmap"))]
265 fn load_font_file_impl(&mut self, path: &std::path::Path) -> Result<(), std::io::Error> {
266 let file = std::fs::File::open(path)?;
267 let data: &[u8] = unsafe { &memmap2::MmapOptions::new().map(&file)? };
268
269 self.load_fonts_from_file(path, data);
270 Ok(())
271 }
272
273 /// Loads a font file into the `Database`.
274 ///
275 /// Will load all font faces in case of a font collection.
276 #[cfg(all(feature = "fs", not(feature = "memmap")))]
277 pub fn load_font_file<P: AsRef<std::path::Path>>(
278 &mut self,
279 path: P,
280 ) -> Result<(), std::io::Error> {
281 self.load_font_file_impl(path.as_ref())
282 }
283
284 // A non-generic version.
285 #[cfg(all(feature = "fs", not(feature = "memmap")))]
286 fn load_font_file_impl(&mut self, path: &std::path::Path) -> Result<(), std::io::Error> {
287 let data = std::fs::read(path)?;
288
289 self.load_fonts_from_file(path, &data);
290 Ok(())
291 }
292
293 /// Loads font files from the selected directory into the `Database`.
294 ///
295 /// This method will scan directories recursively.
296 ///
297 /// Will load `ttf`, `otf`, `ttc` and `otc` fonts.
298 ///
299 /// Unlike other `load_*` methods, this one doesn't return an error.
300 /// It will simply skip malformed fonts and will print a warning into the log for each of them.
301 #[cfg(feature = "fs")]
302 pub fn load_fonts_dir<P: AsRef<std::path::Path>>(&mut self, dir: P) {
303 self.load_fonts_dir_impl(dir.as_ref())
304 }
305
306 // A non-generic version.
307 #[rustfmt::skip] // keep extensions match as is
308 #[cfg(feature = "fs")]
309 fn load_fonts_dir_impl(&mut self, dir: &std::path::Path) {
310 let fonts_dir = match std::fs::read_dir(dir) {
311 Ok(dir) => dir,
312 Err(_) => return,
313 };
314
315 for entry in fonts_dir.flatten() {
316 let path = entry.path();
317 if path.is_file() {
318 match path.extension().and_then(|e| e.to_str()) {
319 Some("ttf") | Some("ttc") | Some("TTF") | Some("TTC") |
320 Some("otf") | Some("otc") | Some("OTF") | Some("OTC") => {
321 if let Err(e) = self.load_font_file(&path) {
322 log::warn!("Failed to load '{}' cause {}.", path.display(), e);
323 }
324 }
325 _ => {}
326 }
327 } else if path.is_dir() {
328 // TODO: ignore symlinks?
329 self.load_fonts_dir(path);
330 }
331 }
332 }
333
334 /// Attempts to load system fonts.
335 ///
336 /// Supports Windows, Linux and macOS.
337 ///
338 /// System fonts loading is a surprisingly complicated task,
339 /// mostly unsolvable without interacting with system libraries.
340 /// And since `fontdb` tries to be small and portable, this method
341 /// will simply scan some predefined directories.
342 /// Which means that fonts that are not in those directories must
343 /// be added manually.
344 #[cfg(feature = "fs")]
345 pub fn load_system_fonts(&mut self) {
346 #[cfg(target_os = "windows")]
347 {
348 if let Some(ref system_root) = std::env::var_os("SYSTEMROOT") {
349 let system_root_path = std::path::Path::new(system_root);
350 self.load_fonts_dir(system_root_path.join("Fonts"));
351 } else {
352 self.load_fonts_dir("C:\\Windows\\Fonts\\");
353 }
354
355 if let Ok(ref home) = std::env::var("USERPROFILE") {
356 let home_path = std::path::Path::new(home);
357 self.load_fonts_dir(home_path.join("AppData\\Local\\Microsoft\\Windows\\Fonts"));
358 self.load_fonts_dir(home_path.join("AppData\\Roaming\\Microsoft\\Windows\\Fonts"));
359 }
360 }
361
362 #[cfg(target_os = "macos")]
363 {
364 self.load_fonts_dir("/Library/Fonts");
365 self.load_fonts_dir("/System/Library/Fonts");
366 // Downloadable fonts, location varies on major macOS releases
367 if let Ok(dir) = std::fs::read_dir("/System/Library/AssetsV2") {
368 for entry in dir {
369 let entry = match entry {
370 Ok(entry) => entry,
371 Err(_) => continue,
372 };
373 if entry
374 .file_name()
375 .to_string_lossy()
376 .starts_with("com_apple_MobileAsset_Font")
377 {
378 self.load_fonts_dir(entry.path());
379 }
380 }
381 }
382 self.load_fonts_dir("/Network/Library/Fonts");
383
384 if let Ok(ref home) = std::env::var("HOME") {
385 let home_path = std::path::Path::new(home);
386 self.load_fonts_dir(home_path.join("Library/Fonts"));
387 }
388 }
389
390 // Redox OS.
391 #[cfg(target_os = "redox")]
392 {
393 self.load_fonts_dir("/ui/fonts");
394 }
395
396 // Linux.
397 #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
398 {
399 #[cfg(feature = "fontconfig")]
400 {
401 self.load_fontconfig();
402 }
403
404 #[cfg(not(feature = "fontconfig"))]
405 {
406 self.load_fonts_dir("/usr/share/fonts/");
407 self.load_fonts_dir("/usr/local/share/fonts/");
408
409 if let Ok(ref home) = std::env::var("HOME") {
410 let home_path = std::path::Path::new(home);
411 self.load_fonts_dir(home_path.join(".fonts"));
412 self.load_fonts_dir(home_path.join(".local/share/fonts"));
413 }
414 }
415 }
416 }
417
418 // Linux.
419 #[cfg(all(
420 unix,
421 feature = "fontconfig",
422 not(any(target_os = "macos", target_os = "android"))
423 ))]
424 fn load_fontconfig(&mut self) {
425 use std::path::Path;
426
427 let mut fontconfig = fontconfig_parser::FontConfig::default();
428 let home = std::env::var("HOME");
429
430 if let Ok(ref config_file) = std::env::var("FONTCONFIG_FILE") {
431 let _ = fontconfig.merge_config(Path::new(config_file));
432 } else {
433 let xdg_config_home = if let Ok(val) = std::env::var("XDG_CONFIG_HOME") {
434 Some(val.into())
435 } else if let Ok(ref home) = home {
436 // according to https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
437 // $XDG_CONFIG_HOME should default to $HOME/.config if not set
438 Some(Path::new(home).join(".config"))
439 } else {
440 None
441 };
442
443 let read_global = match xdg_config_home {
444 Some(p) => fontconfig
445 .merge_config(&p.join("fontconfig/fonts.conf"))
446 .is_err(),
447 None => true,
448 };
449
450 if read_global {
451 let _ = fontconfig.merge_config(Path::new("/etc/fonts/local.conf"));
452 }
453 let _ = fontconfig.merge_config(Path::new("/etc/fonts/fonts.conf"));
454 }
455
456 for fontconfig_parser::Alias {
457 alias,
458 default,
459 prefer,
460 accept,
461 } in fontconfig.aliases
462 {
463 let name = prefer
464 .get(0)
465 .or_else(|| accept.get(0))
466 .or_else(|| default.get(0));
467
468 if let Some(name) = name {
469 match alias.to_lowercase().as_str() {
470 "serif" => self.set_serif_family(name),
471 "sans-serif" => self.set_sans_serif_family(name),
472 "sans serif" => self.set_sans_serif_family(name),
473 "monospace" => self.set_monospace_family(name),
474 "cursive" => self.set_cursive_family(name),
475 "fantasy" => self.set_fantasy_family(name),
476 _ => {}
477 }
478 }
479 }
480
481 for dir in fontconfig.dirs {
482 let path = if dir.path.starts_with("~") {
483 if let Ok(ref home) = home {
484 Path::new(home).join(dir.path.strip_prefix("~").unwrap())
485 } else {
486 continue;
487 }
488 } else {
489 dir.path
490 };
491 self.load_fonts_dir(path);
492 }
493 }
494
495 /// Pushes a user-provided `FaceInfo` to the database.
496 ///
497 /// In some cases, a caller might want to ignore the font's metadata and provide their own.
498 /// This method doesn't parse the `source` font.
499 ///
500 /// The `id` field should be set to [`ID::dummy()`] and will be then overwritten by this method.
501 pub fn push_face_info(&mut self, mut info: FaceInfo) {
502 self.faces.insert_with_key(|k| {
503 info.id = ID(k);
504 info
505 });
506 }
507
508 /// Removes a font face by `id` from the database.
509 ///
510 /// Returns `false` while attempting to remove a non-existing font face.
511 ///
512 /// Useful when you want to ignore some specific font face(s)
513 /// after loading a large directory with fonts.
514 /// Or a specific face from a font.
515 pub fn remove_face(&mut self, id: ID) {
516 self.faces.remove(id.0);
517 }
518
519 /// Returns `true` if the `Database` contains no font faces.
520 #[inline]
521 pub fn is_empty(&self) -> bool {
522 self.faces.is_empty()
523 }
524
525 /// Returns the number of font faces in the `Database`.
526 ///
527 /// Note that `Database` stores font faces, not fonts.
528 /// For example, if a caller will try to load a font collection (`*.ttc`) that contains 5 faces,
529 /// then the `Database` will load 5 font faces and this method will return 5, not 1.
530 #[inline]
531 pub fn len(&self) -> usize {
532 self.faces.len()
533 }
534
535 /// Sets the family that will be used by `Family::Serif`.
536 pub fn set_serif_family<S: Into<String>>(&mut self, family: S) {
537 self.family_serif = family.into();
538 }
539
540 /// Sets the family that will be used by `Family::SansSerif`.
541 pub fn set_sans_serif_family<S: Into<String>>(&mut self, family: S) {
542 self.family_sans_serif = family.into();
543 }
544
545 /// Sets the family that will be used by `Family::Cursive`.
546 pub fn set_cursive_family<S: Into<String>>(&mut self, family: S) {
547 self.family_cursive = family.into();
548 }
549
550 /// Sets the family that will be used by `Family::Fantasy`.
551 pub fn set_fantasy_family<S: Into<String>>(&mut self, family: S) {
552 self.family_fantasy = family.into();
553 }
554
555 /// Sets the family that will be used by `Family::Monospace`.
556 pub fn set_monospace_family<S: Into<String>>(&mut self, family: S) {
557 self.family_monospace = family.into();
558 }
559
560 /// Returns the generic family name or the `Family::Name` itself.
561 ///
562 /// Generic family names should be set via `Database::set_*_family` methods.
563 pub fn family_name<'a>(&'a self, family: &'a Family) -> &'a str {
564 match family {
565 Family::Name(name) => name,
566 Family::Serif => self.family_serif.as_str(),
567 Family::SansSerif => self.family_sans_serif.as_str(),
568 Family::Cursive => self.family_cursive.as_str(),
569 Family::Fantasy => self.family_fantasy.as_str(),
570 Family::Monospace => self.family_monospace.as_str(),
571 }
572 }
573
574 /// Performs a CSS-like query and returns the best matched font face.
575 pub fn query(&self, query: &Query) -> Option<ID> {
576 for family in query.families {
577 let name = self.family_name(family);
578 let candidates: Vec<_> = self
579 .faces
580 .iter()
581 .filter(|(_, face)| face.families.iter().any(|family| family.0 == name))
582 .map(|(_, info)| info)
583 .collect();
584
585 if !candidates.is_empty() {
586 if let Some(index) = find_best_match(&candidates, query) {
587 return Some(candidates[index].id);
588 }
589 }
590 }
591
592 None
593 }
594
595 /// Returns an iterator over the internal storage.
596 ///
597 /// This can be used for manual font matching.
598 #[inline]
599 pub fn faces(&self) -> impl Iterator<Item = &FaceInfo> + '_ {
600 self.faces.iter().map(|(_, info)| info)
601 }
602
603 /// Selects a `FaceInfo` by `id`.
604 ///
605 /// Returns `None` if a face with such ID was already removed,
606 /// or this ID belong to the other `Database`.
607 pub fn face(&self, id: ID) -> Option<&FaceInfo> {
608 self.faces.get(id.0)
609 }
610
611 /// Returns font face storage and the face index by `ID`.
612 pub fn face_source(&self, id: ID) -> Option<(Source, u32)> {
613 self.face(id).map(|info| (info.source.clone(), info.index))
614 }
615
616 /// Executes a closure with a font's data.
617 ///
618 /// We can't return a reference to a font binary data because of lifetimes.
619 /// So instead, you can use this method to process font's data.
620 ///
621 /// The closure accepts raw font data and font face index.
622 ///
623 /// In case of `Source::File`, the font file will be memory mapped.
624 ///
625 /// Returns `None` when font file loading failed.
626 ///
627 /// # Example
628 ///
629 /// ```ignore
630 /// let is_variable = db.with_face_data(id, |font_data, face_index| {
631 /// let font = ttf_parser::Face::from_slice(font_data, face_index).unwrap();
632 /// font.is_variable()
633 /// })?;
634 /// ```
635 pub fn with_face_data<P, T>(&self, id: ID, p: P) -> Option<T>
636 where
637 P: FnOnce(&[u8], u32) -> T,
638 {
639 let (src, face_index) = self.face_source(id)?;
640 src.with_data(|data| p(data, face_index))
641 }
642
643 /// Makes the font data that backs the specified face id shared so that the application can
644 /// hold a reference to it.
645 ///
646 /// # Safety
647 ///
648 /// If the face originates from a file from disk, then the file is mapped from disk. This is unsafe as
649 /// another process may make changes to the file on disk, which may become visible in this process'
650 /// mapping and possibly cause crashes.
651 ///
652 /// If the underlying font provides multiple faces, then all faces are updated to participate in
653 /// the data sharing. If the face was previously marked for data sharing, then this function will
654 /// return a clone of the existing reference.
655 #[cfg(all(feature = "fs", feature = "memmap"))]
656 pub unsafe fn make_shared_face_data(
657 &mut self,
658 id: ID,
659 ) -> Option<(std::sync::Arc<dyn AsRef<[u8]> + Send + Sync>, u32)> {
660 let face_info = self.faces.get(id.0)?;
661 let face_index = face_info.index;
662
663 let old_source = face_info.source.clone();
664
665 let (path, shared_data) = match &old_source {
666 Source::Binary(data) => {
667 return Some((data.clone(), face_index));
668 }
669 Source::File(ref path) => {
670 let file = std::fs::File::open(path).ok()?;
671 let shared_data = std::sync::Arc::new(memmap2::MmapOptions::new().map(&file).ok()?)
672 as std::sync::Arc<dyn AsRef<[u8]> + Send + Sync>;
673 (path.clone(), shared_data)
674 }
675 Source::SharedFile(_, data) => {
676 return Some((data.clone(), face_index));
677 }
678 };
679
680 let shared_source = Source::SharedFile(path.clone(), shared_data.clone());
681
682 self.faces.iter_mut().for_each(|(_, face)| {
683 if matches!(&face.source, Source::File(old_path) if old_path == &path) {
684 face.source = shared_source.clone();
685 }
686 });
687
688 Some((shared_data, face_index))
689 }
690
691 /// Transfers ownership of shared font data back to the font database. This is the reverse operation
692 /// of [`Self::make_shared_face_data`]. If the font data belonging to the specified face is mapped
693 /// from a file on disk, then that mapping is closed and the data becomes private to the process again.
694 #[cfg(all(feature = "fs", feature = "memmap"))]
695 pub fn make_face_data_unshared(&mut self, id: ID) {
696 let face_info = match self.faces.get(id.0) {
697 Some(face_info) => face_info,
698 None => return,
699 };
700
701 let old_source = face_info.source.clone();
702
703 let shared_path = match old_source {
704 #[cfg(all(feature = "fs", feature = "memmap"))]
705 Source::SharedFile(path, _) => path,
706 _ => return,
707 };
708
709 let new_source = Source::File(shared_path.clone());
710
711 self.faces.iter_mut().for_each(|(_, face)| {
712 if matches!(&face.source, Source::SharedFile(path, ..) if path == &shared_path) {
713 face.source = new_source.clone();
714 }
715 });
716 }
717}
718
719/// A single font face info.
720///
721/// A font can have multiple faces.
722///
723/// A single item of the `Database`.
724#[derive(Clone, Debug)]
725pub struct FaceInfo {
726 /// An unique ID.
727 pub id: ID,
728
729 /// A font source.
730 ///
731 /// Note that multiple `FaceInfo` objects can reference the same data in case of
732 /// font collections, which means that they'll use the same Source.
733 pub source: Source,
734
735 /// A face index in the `source`.
736 pub index: u32,
737
738 /// A list of family names.
739 ///
740 /// Contains pairs of Name + Language. Where the first family is always English US,
741 /// unless it's missing from the font.
742 ///
743 /// Corresponds to a *Typographic Family* (ID 16) or a *Font Family* (ID 1) [name ID]
744 /// in a TrueType font.
745 ///
746 /// This is not an *Extended Typographic Family* or a *Full Name*.
747 /// Meaning it will contain _Arial_ and not _Arial Bold_.
748 ///
749 /// [name ID]: https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
750 pub families: Vec<(String, Language)>,
751
752 /// A PostScript name.
753 ///
754 /// Corresponds to a *PostScript name* (6) [name ID] in a TrueType font.
755 ///
756 /// [name ID]: https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
757 pub post_script_name: String,
758
759 /// A font face style.
760 pub style: Style,
761
762 /// A font face weight.
763 pub weight: Weight,
764
765 /// A font face stretch.
766 pub stretch: Stretch,
767
768 /// Indicates that the font face is monospaced.
769 pub monospaced: bool,
770}
771
772/// A font source.
773///
774/// Either a raw binary data or a file path.
775///
776/// Stores the whole font and not just a single face.
777#[derive(Clone)]
778pub enum Source {
779 /// A font's raw data, typically backed by a Vec<u8>.
780 Binary(alloc::sync::Arc<dyn AsRef<[u8]> + Sync + Send>),
781
782 /// A font's path.
783 #[cfg(feature = "fs")]
784 File(std::path::PathBuf),
785
786 /// A font's raw data originating from a shared file mapping.
787 #[cfg(all(feature = "fs", feature = "memmap"))]
788 SharedFile(
789 std::path::PathBuf,
790 std::sync::Arc<dyn AsRef<[u8]> + Sync + Send>,
791 ),
792}
793
794impl core::fmt::Debug for Source {
795 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
796 match self {
797 Self::Binary(arg0: &Arc + Sync + Send>) => f&mut DebugTuple<'_, '_>
798 .debug_tuple(name:"SharedBinary")
799 .field(&arg0.as_ref().as_ref())
800 .finish(),
801 #[cfg(feature = "fs")]
802 Self::File(arg0: &PathBuf) => f.debug_tuple(name:"File").field(arg0).finish(),
803 #[cfg(all(feature = "fs", feature = "memmap"))]
804 Self::SharedFile(arg0: &PathBuf, arg1: &Arc + Sync + Send>) => f&mut DebugTuple<'_, '_>
805 .debug_tuple(name:"SharedFile")
806 .field(arg0)
807 .field(&arg1.as_ref().as_ref())
808 .finish(),
809 }
810 }
811}
812
813impl Source {
814 fn with_data<P, T>(&self, p: P) -> Option<T>
815 where
816 P: FnOnce(&[u8]) -> T,
817 {
818 match &self {
819 #[cfg(all(feature = "fs", not(feature = "memmap")))]
820 Source::File(ref path) => {
821 let data = std::fs::read(path).ok()?;
822
823 Some(p(&data))
824 }
825 #[cfg(all(feature = "fs", feature = "memmap"))]
826 Source::File(ref path) => {
827 let file = std::fs::File::open(path).ok()?;
828 let data = unsafe { &memmap2::MmapOptions::new().map(&file).ok()? };
829
830 Some(p(data))
831 }
832 Source::Binary(ref data) => Some(p(data.as_ref().as_ref())),
833 #[cfg(all(feature = "fs", feature = "memmap"))]
834 Source::SharedFile(_, ref data) => Some(p(data.as_ref().as_ref())),
835 }
836 }
837}
838
839/// A database query.
840///
841/// Mainly used by `Database::query()`.
842#[derive(Clone, Copy, Default, Debug, Eq, PartialEq, Hash)]
843pub struct Query<'a> {
844 /// A prioritized list of font family names or generic family names.
845 ///
846 /// [font-family](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#propdef-font-family) in CSS.
847 pub families: &'a [Family<'a>],
848
849 /// Specifies the weight of glyphs in the font, their degree of blackness or stroke thickness.
850 ///
851 /// [font-weight](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-weight-prop) in CSS.
852 pub weight: Weight,
853
854 /// Selects a normal, condensed, or expanded face from a font family.
855 ///
856 /// [font-stretch](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-stretch-prop) in CSS.
857 pub stretch: Stretch,
858
859 /// Allows italic or oblique faces to be selected.
860 ///
861 /// [font-style](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-style-prop) in CSS.
862 pub style: Style,
863}
864
865// Enum value descriptions are from the CSS spec.
866/// A [font family](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#propdef-font-family).
867#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
868pub enum Family<'a> {
869 /// The name of a font family of choice.
870 ///
871 /// This must be a *Typographic Family* (ID 16) or a *Family Name* (ID 1) in terms of TrueType.
872 /// Meaning you have to pass a family without any additional suffixes like _Bold_, _Italic_,
873 /// _Regular_, etc.
874 ///
875 /// Localized names are allowed.
876 Name(&'a str),
877
878 /// Serif fonts represent the formal text style for a script.
879 Serif,
880
881 /// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low contrast
882 /// and have stroke endings that are plain — without any flaring, cross stroke,
883 /// or other ornamentation.
884 SansSerif,
885
886 /// Glyphs in cursive fonts generally use a more informal script style,
887 /// and the result looks more like handwritten pen or brush writing than printed letterwork.
888 Cursive,
889
890 /// Fantasy fonts are primarily decorative or expressive fonts that
891 /// contain decorative or expressive representations of characters.
892 Fantasy,
893
894 /// The sole criterion of a monospace font is that all glyphs have the same fixed width.
895 Monospace,
896}
897
898/// Specifies the weight of glyphs in the font, their degree of blackness or stroke thickness.
899#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug, Hash)]
900pub struct Weight(pub u16);
901
902impl Default for Weight {
903 #[inline]
904 fn default() -> Weight {
905 Weight::NORMAL
906 }
907}
908
909impl Weight {
910 /// Thin weight (100), the thinnest value.
911 pub const THIN: Weight = Weight(100);
912 /// Extra light weight (200).
913 pub const EXTRA_LIGHT: Weight = Weight(200);
914 /// Light weight (300).
915 pub const LIGHT: Weight = Weight(300);
916 /// Normal (400).
917 pub const NORMAL: Weight = Weight(400);
918 /// Medium weight (500, higher than normal).
919 pub const MEDIUM: Weight = Weight(500);
920 /// Semibold weight (600).
921 pub const SEMIBOLD: Weight = Weight(600);
922 /// Bold weight (700).
923 pub const BOLD: Weight = Weight(700);
924 /// Extra-bold weight (800).
925 pub const EXTRA_BOLD: Weight = Weight(800);
926 /// Black weight (900), the thickest value.
927 pub const BLACK: Weight = Weight(900);
928}
929
930/// Allows italic or oblique faces to be selected.
931#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
932pub enum Style {
933 /// A face that is neither italic not obliqued.
934 Normal,
935 /// A form that is generally cursive in nature.
936 Italic,
937 /// A typically-sloped version of the regular face.
938 Oblique,
939}
940
941impl Default for Style {
942 #[inline]
943 fn default() -> Style {
944 Style::Normal
945 }
946}
947
948fn parse_face_info(source: Source, data: &[u8], index: u32) -> Result<FaceInfo, LoadError> {
949 let raw_face: RawFace<'_> = ttf_parser::RawFace::parse(data, index).map_err(|_| LoadError::MalformedFont)?;
950 let (families: Vec<(String, Language)>, post_script_name: String) = parse_names(&raw_face).ok_or(err:LoadError::UnnamedFont)?;
951 let (mut style: Style, weight: Weight, stretch: Width) = parse_os2(&raw_face);
952 let (monospaced: bool, italic: bool) = parse_post(&raw_face);
953
954 if style == Style::Normal && italic {
955 style = Style::Italic;
956 }
957
958 Ok(FaceInfo {
959 id: ID::dummy(),
960 source,
961 index,
962 families,
963 post_script_name,
964 style,
965 weight,
966 stretch,
967 monospaced,
968 })
969}
970
971fn parse_names(raw_face: &ttf_parser::RawFace) -> Option<(Vec<(String, Language)>, String)> {
972 const NAME_TAG: ttf_parser::Tag = ttf_parser::Tag::from_bytes(b"name");
973 let name_data = raw_face.table(NAME_TAG)?;
974 let name_table = ttf_parser::name::Table::parse(name_data)?;
975
976 let mut families = collect_families(ttf_parser::name_id::TYPOGRAPHIC_FAMILY, &name_table.names);
977
978 // We have to fallback to Family Name when no Typographic Family Name was set.
979 if families.is_empty() {
980 families = collect_families(ttf_parser::name_id::FAMILY, &name_table.names);
981 }
982
983 // Make English US the first one.
984 if families.len() > 1 {
985 if let Some(index) = families
986 .iter()
987 .position(|f| f.1 == Language::English_UnitedStates)
988 {
989 if index != 0 {
990 families.swap(0, index);
991 }
992 }
993 }
994
995 if families.is_empty() {
996 return None;
997 }
998
999 let post_script_name = name_table
1000 .names
1001 .into_iter()
1002 .find(|name| {
1003 name.name_id == ttf_parser::name_id::POST_SCRIPT_NAME && name.is_supported_encoding()
1004 })
1005 .and_then(|name| name_to_unicode(&name))?;
1006
1007 Some((families, post_script_name))
1008}
1009
1010fn collect_families(name_id: u16, names: &ttf_parser::name::Names) -> Vec<(String, Language)> {
1011 let mut families = Vec::new();
1012 for name in names.into_iter() {
1013 if name.name_id == name_id && name.is_unicode() {
1014 if let Some(family) = name_to_unicode(&name) {
1015 families.push((family, name.language()));
1016 }
1017 }
1018 }
1019
1020 // If no Unicode English US family name was found then look for English MacRoman as well.
1021 if !families
1022 .iter()
1023 .any(|f| f.1 == Language::English_UnitedStates)
1024 {
1025 for name in names.into_iter() {
1026 if name.name_id == name_id && name.is_mac_roman() {
1027 if let Some(family) = name_to_unicode(&name) {
1028 families.push((family, name.language()));
1029 break;
1030 }
1031 }
1032 }
1033 }
1034
1035 families
1036}
1037
1038fn name_to_unicode(name: &ttf_parser::name::Name) -> Option<String> {
1039 if name.is_unicode() {
1040 let mut raw_data: Vec<u16> = Vec::new();
1041 for c: u16 in ttf_parser::LazyArray16::<u16>::new(data:name.name) {
1042 raw_data.push(c);
1043 }
1044
1045 String::from_utf16(&raw_data).ok()
1046 } else if name.is_mac_roman() {
1047 // We support only MacRoman encoding here, which should be enough in most cases.
1048 let mut raw_data: Vec = Vec::with_capacity(name.name.len());
1049 for b: &u8 in name.name {
1050 raw_data.push(MAC_ROMAN[*b as usize]);
1051 }
1052
1053 String::from_utf16(&raw_data).ok()
1054 } else {
1055 None
1056 }
1057}
1058
1059fn parse_os2(raw_face: &ttf_parser::RawFace) -> (Style, Weight, Stretch) {
1060 const OS2_TAG: ttf_parser::Tag = ttf_parser::Tag::from_bytes(b"OS/2");
1061 let table: Table<'_> = match raw_faceOption<&[u8]>
1062 .table(OS2_TAG)
1063 .and_then(ttf_parser::os2::Table::parse)
1064 {
1065 Some(table: Table<'_>) => table,
1066 None => return (Style::Normal, Weight::NORMAL, Stretch::Normal),
1067 };
1068
1069 let style: Style = match table.style() {
1070 ttf_parser::Style::Normal => Style::Normal,
1071 ttf_parser::Style::Italic => Style::Italic,
1072 ttf_parser::Style::Oblique => Style::Oblique,
1073 };
1074
1075 let weight: Weight = table.weight();
1076 let stretch: Width = table.width();
1077
1078 (style, Weight(weight.to_number()), stretch)
1079}
1080
1081fn parse_post(raw_face: &ttf_parser::RawFace) -> (bool, bool) {
1082 // We need just a single value from the `post` table, while ttf-parser will parse all.
1083 // Therefore we have a custom parser.
1084
1085 const POST_TAG: ttf_parser::Tag = ttf_parser::Tag::from_bytes(b"post");
1086 let data: &[u8] = match raw_face.table(POST_TAG) {
1087 Some(v: &[u8]) => v,
1088 None => return (false, false),
1089 };
1090
1091 // All we care about, it that u32 at offset 12 is non-zero.
1092 let monospaced: bool = data.get(index:12..16) != Some(&[0, 0, 0, 0]);
1093
1094 // Italic angle as f16.16.
1095 let italic: bool = data.get(index:4..8) != Some(&[0, 0, 0, 0]);
1096
1097 (monospaced, italic)
1098}
1099
1100trait NameExt {
1101 fn is_mac_roman(&self) -> bool;
1102 fn is_supported_encoding(&self) -> bool;
1103}
1104
1105impl NameExt for ttf_parser::name::Name<'_> {
1106 #[inline]
1107 fn is_mac_roman(&self) -> bool {
1108 use ttf_parser::PlatformId::Macintosh;
1109 // https://docs.microsoft.com/en-us/typography/opentype/spec/name#macintosh-encoding-ids-script-manager-codes
1110 const MACINTOSH_ROMAN_ENCODING_ID: u16 = 0;
1111
1112 self.platform_id == Macintosh && self.encoding_id == MACINTOSH_ROMAN_ENCODING_ID
1113 }
1114
1115 #[inline]
1116 fn is_supported_encoding(&self) -> bool {
1117 self.is_unicode() || self.is_mac_roman()
1118 }
1119}
1120
1121// https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-style-matching
1122// Based on https://github.com/servo/font-kit
1123#[inline(never)]
1124fn find_best_match(candidates: &[&FaceInfo], query: &Query) -> Option<usize> {
1125 debug_assert!(!candidates.is_empty());
1126
1127 // Step 4.
1128 let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
1129
1130 // Step 4a (`font-stretch`).
1131 let matches = matching_set
1132 .iter()
1133 .any(|&index| candidates[index].stretch == query.stretch);
1134 let matching_stretch = if matches {
1135 // Exact match.
1136 query.stretch
1137 } else if query.stretch <= Stretch::Normal {
1138 // Closest stretch, first checking narrower values and then wider values.
1139 let stretch = matching_set
1140 .iter()
1141 .filter(|&&index| candidates[index].stretch < query.stretch)
1142 .min_by_key(|&&index| {
1143 query.stretch.to_number() - candidates[index].stretch.to_number()
1144 });
1145
1146 match stretch {
1147 Some(&matching_index) => candidates[matching_index].stretch,
1148 None => {
1149 let matching_index = *matching_set.iter().min_by_key(|&&index| {
1150 candidates[index].stretch.to_number() - query.stretch.to_number()
1151 })?;
1152
1153 candidates[matching_index].stretch
1154 }
1155 }
1156 } else {
1157 // Closest stretch, first checking wider values and then narrower values.
1158 let stretch = matching_set
1159 .iter()
1160 .filter(|&&index| candidates[index].stretch > query.stretch)
1161 .min_by_key(|&&index| {
1162 candidates[index].stretch.to_number() - query.stretch.to_number()
1163 });
1164
1165 match stretch {
1166 Some(&matching_index) => candidates[matching_index].stretch,
1167 None => {
1168 let matching_index = *matching_set.iter().min_by_key(|&&index| {
1169 query.stretch.to_number() - candidates[index].stretch.to_number()
1170 })?;
1171
1172 candidates[matching_index].stretch
1173 }
1174 }
1175 };
1176 matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
1177
1178 // Step 4b (`font-style`).
1179 let style_preference = match query.style {
1180 Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
1181 Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
1182 Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
1183 };
1184 let matching_style = *style_preference.iter().find(|&query_style| {
1185 matching_set
1186 .iter()
1187 .any(|&index| candidates[index].style == *query_style)
1188 })?;
1189
1190 matching_set.retain(|&index| candidates[index].style == matching_style);
1191
1192 // Step 4c (`font-weight`).
1193 //
1194 // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
1195 // just use 450 as the cutoff.
1196 let weight = query.weight.0;
1197
1198 let matching_weight = if matching_set
1199 .iter()
1200 .any(|&index| candidates[index].weight.0 == weight)
1201 {
1202 Weight(weight)
1203 } else if (400..450).contains(&weight)
1204 && matching_set
1205 .iter()
1206 .any(|&index| candidates[index].weight.0 == 500)
1207 {
1208 // Check 500 first.
1209 Weight::MEDIUM
1210 } else if (450..=500).contains(&weight)
1211 && matching_set
1212 .iter()
1213 .any(|&index| candidates[index].weight.0 == 400)
1214 {
1215 // Check 400 first.
1216 Weight::NORMAL
1217 } else if weight <= 500 {
1218 // Closest weight, first checking thinner values and then fatter ones.
1219 let idx = matching_set
1220 .iter()
1221 .filter(|&&index| candidates[index].weight.0 <= weight)
1222 .min_by_key(|&&index| weight - candidates[index].weight.0);
1223
1224 match idx {
1225 Some(&matching_index) => candidates[matching_index].weight,
1226 None => {
1227 let matching_index = *matching_set
1228 .iter()
1229 .min_by_key(|&&index| candidates[index].weight.0 - weight)?;
1230 candidates[matching_index].weight
1231 }
1232 }
1233 } else {
1234 // Closest weight, first checking fatter values and then thinner ones.
1235 let idx = matching_set
1236 .iter()
1237 .filter(|&&index| candidates[index].weight.0 >= weight)
1238 .min_by_key(|&&index| candidates[index].weight.0 - weight);
1239
1240 match idx {
1241 Some(&matching_index) => candidates[matching_index].weight,
1242 None => {
1243 let matching_index = *matching_set
1244 .iter()
1245 .min_by_key(|&&index| weight - candidates[index].weight.0)?;
1246 candidates[matching_index].weight
1247 }
1248 }
1249 };
1250 matching_set.retain(|&index| candidates[index].weight == matching_weight);
1251
1252 // Ignore step 4d (`font-size`).
1253
1254 // Return the result.
1255 matching_set.into_iter().next()
1256}
1257
1258/// Macintosh Roman to UTF-16 encoding table.
1259///
1260/// https://en.wikipedia.org/wiki/Mac_OS_Roman
1261#[rustfmt::skip]
1262const MAC_ROMAN: &[u16; 256] = &[
1263 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
1264 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
1265 0x0010, 0x2318, 0x21E7, 0x2325, 0x2303, 0x0015, 0x0016, 0x0017,
1266 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
1267 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
1268 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
1269 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
1270 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
1271 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
1272 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
1273 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
1274 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
1275 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
1276 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
1277 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
1278 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
1279 0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1,
1280 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8,
1281 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3,
1282 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC,
1283 0x2020, 0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF,
1284 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8,
1285 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211,
1286 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x03A9, 0x00E6, 0x00F8,
1287 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB,
1288 0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153,
1289 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA,
1290 0x00FF, 0x0178, 0x2044, 0x20AC, 0x2039, 0x203A, 0xFB01, 0xFB02,
1291 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1,
1292 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC, 0x00D3, 0x00D4,
1293 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC,
1294 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7,
1295];
1296