1#![forbid(unsafe_code)]
13
14pub use fontdb;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
22pub struct Variant {
23 pub bold: bool,
25 pub italic: bool,
27}
28
29impl Variant {
30 pub const ALL: [Self; 4] = [
32 Self {
33 bold: false,
34 italic: false,
35 },
36 Self {
37 bold: true,
38 italic: false,
39 },
40 Self {
41 bold: false,
42 italic: true,
43 },
44 Self {
45 bold: true,
46 italic: true,
47 },
48 ];
49}
50
51pub fn database() -> fontdb::Database {
53 let mut database = fontdb::Database::new();
54 database.load_system_fonts();
55 set_generics(&mut database);
56 database
57}
58
59pub fn find(database: &fontdb::Database, family: &str, variant: Variant) -> Option<fontdb::ID> {
62 let mut wanted = vec![fontdb::Family::Name(family)];
63 wanted.extend(substitute_families(family));
64 query(database, &wanted, variant)
65}
66
67pub fn find_substitute(
70 database: &fontdb::Database,
71 family: &str,
72 variant: Variant,
73) -> Option<fontdb::ID> {
74 query(database, &substitute_families(family), variant)
75}
76
77fn substitute_families(family: &str) -> Vec<fontdb::Family<'static>> {
78 let mut wanted: Vec<fontdb::Family<'static>> = metric_substitutes(family)
84 .iter()
85 .map(|name| fontdb::Family::Name(name))
86 .collect();
87 wanted.push(generic(family));
88 wanted
89}
90
91fn query(
92 database: &fontdb::Database,
93 families: &[fontdb::Family<'_>],
94 variant: Variant,
95) -> Option<fontdb::ID> {
96 database.query(&fontdb::Query {
97 families,
98 weight: if variant.bold {
99 fontdb::Weight::BOLD
100 } else {
101 fontdb::Weight::NORMAL
102 },
103 stretch: fontdb::Stretch::Normal,
104 style: if variant.italic {
105 fontdb::Style::Italic
106 } else {
107 fontdb::Style::Normal
108 },
109 })
110}
111
112pub fn covers(data: &[u8], index: u32, character: char) -> bool {
114 ttf_parser::Face::parse(data, index)
115 .ok()
116 .and_then(|face| face.glyph_index(character))
117 .is_some_and(|glyph| glyph.0 != 0)
118}
119
120pub fn faces_with(
125 database: &fontdb::Database,
126 character: char,
127 variant: Variant,
128) -> impl Iterator<Item = fontdb::ID> + '_ {
129 [
130 fontdb::Family::SansSerif,
131 fontdb::Family::Serif,
132 fontdb::Family::Monospace,
133 ]
134 .into_iter()
135 .filter_map(move |generic| query(database, &[generic], variant))
136 .chain(database.faces().map(|info| info.id))
137 .filter(move |&id| {
138 database
139 .with_face_data(id, |data, index| covers(data, index, character))
140 .unwrap_or(false)
141 })
142}
143
144pub fn metric_substitutes(family: &str) -> &'static [&'static str] {
152 match family.to_ascii_lowercase().as_str() {
153 "times new roman" | "times" | "timesnewroman" => &["Liberation Serif", "DejaVu Serif"],
154 "arial" | "helvetica" | "arialmt" => &["Liberation Sans", "DejaVu Sans"],
155 "courier new" | "courier" => &["Liberation Mono", "DejaVu Sans Mono"],
156 "calibri" => &["Carlito"],
157 "cambria" => &["Caladea"],
158 "liberation serif" => &["DejaVu Serif"],
159 "liberation sans" => &["DejaVu Sans"],
160 "liberation mono" => &["DejaVu Sans Mono"],
161 _ => &[],
162 }
163}
164
165fn set_generics(database: &mut fontdb::Database) {
172 let present = |database: &fontdb::Database, name: &str| {
173 query(database, &[fontdb::Family::Name(name)], Variant::default()).is_some()
174 };
175 let first = |database: &fontdb::Database, names: &[&str]| {
176 names
177 .iter()
178 .find(|name| present(database, name))
179 .map(|name| (*name).to_owned())
180 };
181
182 if let Some(name) = first(
183 database,
184 &[
185 "Liberation Serif",
186 "DejaVu Serif",
187 "Noto Serif",
188 "Times New Roman",
189 "Georgia",
190 ],
191 ) {
192 database.set_serif_family(name);
193 }
194 if let Some(name) = first(
195 database,
196 &[
197 "Liberation Sans",
198 "DejaVu Sans",
199 "Noto Sans",
200 "Arial",
201 "Helvetica",
202 ],
203 ) {
204 database.set_sans_serif_family(name);
205 }
206 if let Some(name) = first(
207 database,
208 &[
209 "Liberation Mono",
210 "DejaVu Sans Mono",
211 "Noto Sans Mono",
212 "Courier New",
213 ],
214 ) {
215 database.set_monospace_family(name);
216 }
217}
218
219pub fn generic(family: &str) -> fontdb::Family<'static> {
226 let lower = family.to_ascii_lowercase();
227 if lower.contains("mono") || lower.contains("courier") || lower.contains("consol") {
228 return fontdb::Family::Monospace;
229 }
230 if lower.contains("times") || lower.contains("serif") || lower.contains("georgia") {
231 return fontdb::Family::Serif;
234 }
235 fontdb::Family::SansSerif
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn metric_substitutes_matches_regardless_of_case_or_spaces() {
244 assert_eq!(
245 metric_substitutes("Times New Roman"),
246 ["Liberation Serif", "DejaVu Serif"]
247 );
248 assert_eq!(
249 metric_substitutes("TIMES NEW ROMAN"),
250 ["Liberation Serif", "DejaVu Serif"]
251 );
252 assert_eq!(
253 metric_substitutes("TimesNewRoman"),
254 ["Liberation Serif", "DejaVu Serif"]
255 );
256 assert_eq!(metric_substitutes("Calibri"), ["Carlito"]);
257 }
258
259 #[test]
260 fn metric_substitutes_is_empty_for_a_family_with_no_known_substitute() {
261 assert_eq!(metric_substitutes("Comic Sans MS"), &[] as &[&str]);
262 }
263
264 #[test]
268 fn generic_recognises_monospace_families() {
269 assert_eq!(generic("Liberation Mono"), fontdb::Family::Monospace);
270 assert_eq!(generic("Courier New"), fontdb::Family::Monospace);
271 assert_eq!(generic("Consolas"), fontdb::Family::Monospace);
272 }
273
274 #[test]
275 fn generic_recognises_serif_families() {
276 assert_eq!(generic("Times New Roman"), fontdb::Family::Serif);
277 assert_eq!(generic("Liberation Serif"), fontdb::Family::Serif);
278 assert_eq!(generic("Georgia"), fontdb::Family::Serif);
279 }
280
281 #[test]
285 fn generic_defaults_to_sans_serif_rather_than_matching_serif_by_accident() {
286 assert_eq!(generic("Liberation Sans"), fontdb::Family::SansSerif);
287 assert_eq!(generic("Wingdings"), fontdb::Family::SansSerif);
288 }
289}