font_enumeration/lib.rs
1//! This is a cross-platform library for enumerating system fonts.
2//!
3//! # Supported platforms:
4//!
5//! - Unix-like (Fontconfig)
6//! - Windows (DirectWrite; **untested**)
7//! - MacOS (Core Text; **untested**)
8//!
9//! # Features and alternatives
10//!
11//! This library is for very simple uses, where you're only interested in listing installed fonts,
12//! perhaps filtering by family name. The listed fonts include family and font name, file path, and
13//! some limited font attributes (style, weight and stretch). It's unlikely this library will grow
14//! much beyond this feature set, and its dependency tree will remain small.
15//!
16//! ```rust
17//! let font_collection = font_enumeration::Collection::new().unwrap();
18//!
19//! for font in font_collection.by_family("DejaVu Sans") {
20//! println!("{font:#?}");
21//! }
22//! ```
23
24use std::path::PathBuf;
25
26use thiserror::Error;
27
28mod utils;
29
30#[cfg(not(any(target_os = "macos", windows)))]
31#[path = "./fontconfig.rs"]
32mod system;
33
34#[cfg(target_os = "macos")]
35#[path = "./core_text.rs"]
36mod system;
37
38#[cfg(windows)]
39#[path = "./direct_write.rs"]
40mod system;
41
42#[derive(Debug, Error)]
43pub enum Error {
44 /// Failed to initialize the system font collection.
45 #[error("Could not initialize system collection")]
46 SystemCollection,
47}
48
49/// A system font collection.
50pub struct Collection {
51 // Using a boxed slice rather than Vec saves [Collection] from having to store a capacity
52 all_fonts: Box<[Font]>,
53}
54
55impl Collection {
56 /// Construct a new font collection. This scans and caches the system fonts.
57 pub fn new() -> Result<Self, Error> {
58 let all_fonts = system::all_fonts()?;
59
60 Ok(Self { all_fonts })
61 }
62
63 /// Iterate over fonts in the collection.
64 pub fn all(&self) -> impl Iterator<Item = &'_ Font> {
65 self.all_fonts.iter()
66 }
67
68 /// Iterate over fonts matching the given family name. The matching is case insensitive.
69 pub fn by_family<'c, 'f>(&'c self, family_name: &'f str) -> impl Iterator<Item = &'c Font> + 'f
70 where
71 'c: 'f,
72 {
73 self.all()
74 .filter(|font| utils::case_insensitive_match(&font.family_name, family_name))
75 }
76
77 /// Consume this collection and get owned font data.
78 pub fn take(self) -> Vec<Font> {
79 self.all_fonts.into_vec()
80 }
81}
82
83/// Style of a font.
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub enum Style {
86 /// Upright. Also known as "Roman".
87 Normal,
88 /// Italic style. Usually visually distinct from the normal style, rather than simply angled.
89 Italic,
90 /// Angle of the font in degrees
91 Oblique(Option<f32>),
92}
93
94/// Weight class of a font, usually from 1 to 1000.
95#[derive(Clone, Copy, Debug, PartialEq)]
96pub struct Weight(f32);
97
98impl Weight {
99 /// Weight corresponding to a CSS value of 100.
100 pub const THIN: Self = Weight(100.);
101
102 /// Weight corresponding to a CSS value of 200.
103 pub const EXTRA_LIGHT: Self = Weight(200.);
104
105 /// Weight corresponding to a CSS value of 300.
106 pub const LIGHT: Self = Weight(300.);
107
108 /// Weight corresponding to a CSS value of 350.
109 pub const SEMI_LIGHT: Self = Weight(350.);
110
111 /// Weight corresponding to a CSS value of 400.
112 pub const NORMAL: Self = Weight(400.);
113
114 /// Weight corresponding to a CSS value of 500.
115 pub const MEDIUM: Self = Weight(500.);
116
117 /// Weight corresponding to a CSS value of 600.
118 pub const SEMI_BOLD: Self = Weight(600.);
119
120 /// Weight corresponding to a CSS value of 700.
121 pub const BOLD: Self = Weight(700.);
122
123 /// Weight corresponding to a CSS value of 800.
124 pub const EXTRA_BOLD: Self = Weight(800.);
125
126 /// Weight corresponding to a CSS value of 900.
127 pub const BLACK: Self = Weight(900.);
128
129 /// Weight corresponding to a CSS value of 950.
130 pub const EXTRA_BLACK: Self = Weight(950.);
131
132 // Create the weight corresponding to the given CSS value.
133 pub const fn new(weight: f32) -> Self {
134 Weight(weight)
135 }
136
137 /// Get the corresponding CSS value of this weight.
138 pub const fn value(self) -> f32 {
139 self.0
140 }
141}
142
143/// Stretch of a font.
144#[derive(Clone, Copy, Debug, PartialEq)]
145pub struct Stretch(f32);
146
147impl Stretch {
148 /// Character width 50% of normal.
149 pub const ULTRA_CONDENSED: Self = Stretch(0.5);
150
151 /// Character width 62.5% of normal.
152 pub const EXTRA_CONDENSED: Self = Stretch(0.625);
153
154 /// Character width 75% of normal.
155 pub const CONDENSED: Self = Stretch(0.75);
156
157 /// Character width 87.5% of normal.
158 pub const SEMI_CONDENSED: Self = Stretch(0.875);
159
160 /// Character width 100% of normal.
161 pub const NORMAL: Self = Stretch(1.0);
162
163 /// Character width 112.5% of normal.
164 pub const SEMI_EXPANDED: Self = Stretch(1.125);
165
166 /// Character width 125% of normal.
167 pub const EXPANDED: Self = Stretch(1.25);
168
169 /// Character width 150% of normal.
170 pub const EXTRA_EXPANDED: Self = Stretch(1.5);
171
172 /// Character width 200% of normal.
173 pub const ULTRA_EXPANDED: Self = Stretch(2.);
174
175 /// Create the specified stretch as a factor of normal. 1.0 is normal, less than 1.0 is
176 /// condensed, more than 1.0 is expanded.
177 pub const fn new(stretch: f32) -> Self {
178 Stretch(stretch)
179 }
180
181 /// Get the stretch value as a factor of normal. 1.0 is normal, less than 1.0 is condensed,
182 /// more than 1.0 is expanded.
183 pub const fn value(self) -> f32 {
184 self.0
185 }
186}
187
188/// A font.
189#[derive(Clone, Debug, PartialEq)]
190pub struct Font {
191 /// Name of the family the font is part of.
192 pub family_name: String,
193
194 /// Name of the font.
195 pub font_name: String,
196
197 /// Path at which the font file is located.
198 pub path: PathBuf,
199
200 /// The font's style.
201 pub style: Style,
202
203 /// The font's weight.
204 pub weight: Weight,
205
206 /// The font's stretch.
207 pub stretch: Stretch,
208}
209
210#[cfg(test)]
211mod test {
212 use super::Collection;
213
214 #[test]
215 fn has_fonts() {
216 let collection = Collection::new().unwrap();
217
218 // is this a reasonable assumption?
219 assert!(!collection.take().is_empty());
220 }
221}