1use std::collections::{BTreeMap, BTreeSet};
19use std::sync::Arc;
20
21use eframe::egui::{FontData, FontDefinitions, FontFamily};
22use odox_core::{Element, Ns};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct Variant {
31 pub bold: bool,
33 pub italic: bool,
35}
36
37pub fn family_of(family: &str, variant: Variant) -> FontFamily {
43 let suffix = match (variant.bold, variant.italic) {
44 (false, false) => "",
45 (true, false) => ":bold",
46 (false, true) => ":italic",
47 (true, true) => ":bolditalic",
48 };
49 FontFamily::Name(format!("{family}{suffix}").into())
50}
51
52pub fn definitions(families: &BTreeSet<String>) -> FontDefinitions {
59 let mut definitions = FontDefinitions::default();
60 let fallback = definitions
61 .families
62 .get(&FontFamily::Proportional)
63 .cloned()
64 .unwrap_or_default();
65
66 let mut database = fontdb::Database::new();
67 database.load_system_fonts();
68 set_generics(&mut database);
69
70 for family in families {
71 for variant in [
72 Variant {
73 bold: false,
74 italic: false,
75 },
76 Variant {
77 bold: true,
78 italic: false,
79 },
80 Variant {
81 bold: false,
82 italic: true,
83 },
84 Variant {
85 bold: true,
86 italic: true,
87 },
88 ] {
89 let key = match family_of(family, variant) {
90 FontFamily::Name(name) => name.to_string(),
91 other => format!("{other:?}"),
94 };
95 let mut chain = Vec::new();
96 if let Some(face) = load(&database, family, variant) {
97 definitions.font_data.insert(key.clone(), Arc::new(face));
98 chain.push(key.clone());
99 }
100 chain.extend(fallback.iter().cloned());
101 definitions
102 .families
103 .insert(FontFamily::Name(key.into()), chain);
104 }
105 }
106 definitions
107}
108
109fn load(database: &fontdb::Database, family: &str, variant: Variant) -> Option<FontData> {
111 let mut wanted = vec![fontdb::Family::Name(family)];
112 wanted.extend(
117 metric_substitutes(family)
118 .iter()
119 .map(|name| fontdb::Family::Name(name)),
120 );
121 wanted.push(generic(family));
125 let query = fontdb::Query {
126 families: &wanted,
127 weight: if variant.bold {
128 fontdb::Weight::BOLD
129 } else {
130 fontdb::Weight::NORMAL
131 },
132 stretch: fontdb::Stretch::Normal,
133 style: if variant.italic {
134 fontdb::Style::Italic
135 } else {
136 fontdb::Style::Normal
137 },
138 };
139 let id = database.query(&query)?;
140 let index = database.face(id)?.index;
141 database.with_face_data(id, |data, face_index| FontData {
142 font: data.to_vec().into(),
143 index: face_index.max(index),
147 tweak: eframe::egui::FontTweak::default(),
148 })
149}
150
151fn metric_substitutes(family: &str) -> &'static [&'static str] {
159 match family.to_ascii_lowercase().as_str() {
160 "times new roman" | "times" | "timesnewroman" => &["Liberation Serif", "DejaVu Serif"],
161 "arial" | "helvetica" | "arialmt" => &["Liberation Sans", "DejaVu Sans"],
162 "courier new" | "courier" => &["Liberation Mono", "DejaVu Sans Mono"],
163 "calibri" => &["Carlito"],
164 "cambria" => &["Caladea"],
165 "liberation serif" => &["DejaVu Serif"],
166 "liberation sans" => &["DejaVu Sans"],
167 "liberation mono" => &["DejaVu Sans Mono"],
168 _ => &[],
169 }
170}
171
172fn set_generics(database: &mut fontdb::Database) {
180 let present = |database: &fontdb::Database, name: &str| {
181 database
182 .query(&fontdb::Query {
183 families: &[fontdb::Family::Name(name)],
184 weight: fontdb::Weight::NORMAL,
185 stretch: fontdb::Stretch::Normal,
186 style: fontdb::Style::Normal,
187 })
188 .is_some()
189 };
190 let first = |database: &fontdb::Database, names: &[&str]| {
191 names
192 .iter()
193 .find(|name| present(database, name))
194 .map(|name| (*name).to_owned())
195 };
196
197 if let Some(name) = first(
198 database,
199 &[
200 "Liberation Serif",
201 "DejaVu Serif",
202 "Noto Serif",
203 "Times New Roman",
204 "Georgia",
205 ],
206 ) {
207 database.set_serif_family(name);
208 }
209 if let Some(name) = first(
210 database,
211 &[
212 "Liberation Sans",
213 "DejaVu Sans",
214 "Noto Sans",
215 "Arial",
216 "Helvetica",
217 ],
218 ) {
219 database.set_sans_serif_family(name);
220 }
221 if let Some(name) = first(
222 database,
223 &[
224 "Liberation Mono",
225 "DejaVu Sans Mono",
226 "Noto Sans Mono",
227 "Courier New",
228 ],
229 ) {
230 database.set_monospace_family(name);
231 }
232}
233
234fn generic(family: &str) -> fontdb::Family<'static> {
241 let lower = family.to_ascii_lowercase();
242 if lower.contains("mono") || lower.contains("courier") || lower.contains("consol") {
243 return fontdb::Family::Monospace;
244 }
245 if lower.contains("times") || lower.contains("serif") || lower.contains("georgia") {
246 return fontdb::Family::Serif;
249 }
250 fontdb::Family::SansSerif
251}
252
253pub fn families_used(parts: &[&Element]) -> BTreeSet<String> {
259 let mut families = BTreeSet::new();
260 let mut faces: BTreeMap<String, String> = BTreeMap::new();
261 for root in parts {
262 collect(root, &mut families, &mut faces);
263 }
264 let resolved: BTreeSet<String> = families
268 .iter()
269 .map(|name| faces.get(name).cloned().unwrap_or_else(|| name.clone()))
270 .collect();
271 resolved
272}
273
274fn collect(
275 element: &Element,
276 families: &mut BTreeSet<String>,
277 faces: &mut BTreeMap<String, String>,
278) {
279 if element.is(&Ns::Style, "font-face")
280 && let Some(name) = element.attr(&Ns::Style, "name")
281 {
282 let family = element
283 .attr(&Ns::Svg, "font-family")
284 .unwrap_or(name)
285 .trim_matches('\'')
286 .to_owned();
287 faces.insert(name.to_owned(), family);
288 }
289 for (ns, local) in [(Ns::Style, "font-name"), (Ns::Fo, "font-family")] {
290 if let Some(name) = element.attr(&ns, local) {
291 let name = name.trim_matches('\'');
292 if !name.is_empty() {
293 families.insert(name.to_owned());
294 }
295 }
296 }
297 for child in element.elements() {
298 collect(child, families, faces);
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn family_of_names_a_variant_with_a_suffix() {
308 let plain = Variant {
309 bold: false,
310 italic: false,
311 };
312 let bold = Variant {
313 bold: true,
314 italic: false,
315 };
316 let italic = Variant {
317 bold: false,
318 italic: true,
319 };
320 let both = Variant {
321 bold: true,
322 italic: true,
323 };
324 assert_eq!(family_of("Arial", plain), FontFamily::Name("Arial".into()));
325 assert_eq!(
326 family_of("Arial", bold),
327 FontFamily::Name("Arial:bold".into())
328 );
329 assert_eq!(
330 family_of("Arial", italic),
331 FontFamily::Name("Arial:italic".into())
332 );
333 assert_eq!(
334 family_of("Arial", both),
335 FontFamily::Name("Arial:bolditalic".into())
336 );
337 }
338
339 #[test]
340 fn metric_substitutes_matches_regardless_of_case_or_spaces() {
341 assert_eq!(
342 metric_substitutes("Times New Roman"),
343 ["Liberation Serif", "DejaVu Serif"]
344 );
345 assert_eq!(
346 metric_substitutes("TIMES NEW ROMAN"),
347 ["Liberation Serif", "DejaVu Serif"]
348 );
349 assert_eq!(
350 metric_substitutes("TimesNewRoman"),
351 ["Liberation Serif", "DejaVu Serif"]
352 );
353 assert_eq!(metric_substitutes("Calibri"), ["Carlito"]);
354 }
355
356 #[test]
357 fn metric_substitutes_is_empty_for_a_family_with_no_known_substitute() {
358 assert_eq!(metric_substitutes("Comic Sans MS"), &[] as &[&str]);
359 }
360
361 #[test]
365 fn generic_recognises_monospace_families() {
366 assert_eq!(generic("Liberation Mono"), fontdb::Family::Monospace);
367 assert_eq!(generic("Courier New"), fontdb::Family::Monospace);
368 assert_eq!(generic("Consolas"), fontdb::Family::Monospace);
369 }
370
371 #[test]
372 fn generic_recognises_serif_families() {
373 assert_eq!(generic("Times New Roman"), fontdb::Family::Serif);
374 assert_eq!(generic("Liberation Serif"), fontdb::Family::Serif);
375 assert_eq!(generic("Georgia"), fontdb::Family::Serif);
376 }
377
378 #[test]
382 fn generic_defaults_to_sans_serif_rather_than_matching_serif_by_accident() {
383 assert_eq!(generic("Liberation Sans"), fontdb::Family::SansSerif);
384 assert_eq!(generic("Wingdings"), fontdb::Family::SansSerif);
385 }
386}