use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use eframe::egui::{FontData, FontDefinitions, FontFamily};
use odox_core::{Element, Ns};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Variant {
pub bold: bool,
pub italic: bool,
}
pub fn family_of(family: &str, variant: Variant) -> FontFamily {
let suffix = match (variant.bold, variant.italic) {
(false, false) => "",
(true, false) => ":bold",
(false, true) => ":italic",
(true, true) => ":bolditalic",
};
FontFamily::Name(format!("{family}{suffix}").into())
}
pub fn definitions(families: &BTreeSet<String>) -> FontDefinitions {
let mut definitions = FontDefinitions::default();
let fallback = definitions
.families
.get(&FontFamily::Proportional)
.cloned()
.unwrap_or_default();
let mut database = fontdb::Database::new();
database.load_system_fonts();
set_generics(&mut database);
for family in families {
for variant in [
Variant {
bold: false,
italic: false,
},
Variant {
bold: true,
italic: false,
},
Variant {
bold: false,
italic: true,
},
Variant {
bold: true,
italic: true,
},
] {
let key = match family_of(family, variant) {
FontFamily::Name(name) => name.to_string(),
other => format!("{other:?}"),
};
let mut chain = Vec::new();
if let Some(face) = load(&database, family, variant) {
definitions.font_data.insert(key.clone(), Arc::new(face));
chain.push(key.clone());
}
chain.extend(fallback.iter().cloned());
definitions
.families
.insert(FontFamily::Name(key.into()), chain);
}
}
definitions
}
fn load(database: &fontdb::Database, family: &str, variant: Variant) -> Option<FontData> {
let mut wanted = vec![fontdb::Family::Name(family)];
wanted.extend(
metric_substitutes(family)
.iter()
.map(|name| fontdb::Family::Name(name)),
);
wanted.push(generic(family));
let query = fontdb::Query {
families: &wanted,
weight: if variant.bold {
fontdb::Weight::BOLD
} else {
fontdb::Weight::NORMAL
},
stretch: fontdb::Stretch::Normal,
style: if variant.italic {
fontdb::Style::Italic
} else {
fontdb::Style::Normal
},
};
let id = database.query(&query)?;
let index = database.face(id)?.index;
database.with_face_data(id, |data, face_index| FontData {
font: data.to_vec().into(),
index: face_index.max(index),
tweak: eframe::egui::FontTweak::default(),
})
}
fn metric_substitutes(family: &str) -> &'static [&'static str] {
match family.to_ascii_lowercase().as_str() {
"times new roman" | "times" | "timesnewroman" => &["Liberation Serif", "DejaVu Serif"],
"arial" | "helvetica" | "arialmt" => &["Liberation Sans", "DejaVu Sans"],
"courier new" | "courier" => &["Liberation Mono", "DejaVu Sans Mono"],
"calibri" => &["Carlito"],
"cambria" => &["Caladea"],
"liberation serif" => &["DejaVu Serif"],
"liberation sans" => &["DejaVu Sans"],
"liberation mono" => &["DejaVu Sans Mono"],
_ => &[],
}
}
fn set_generics(database: &mut fontdb::Database) {
let present = |database: &fontdb::Database, name: &str| {
database
.query(&fontdb::Query {
families: &[fontdb::Family::Name(name)],
weight: fontdb::Weight::NORMAL,
stretch: fontdb::Stretch::Normal,
style: fontdb::Style::Normal,
})
.is_some()
};
let first = |database: &fontdb::Database, names: &[&str]| {
names
.iter()
.find(|name| present(database, name))
.map(|name| (*name).to_owned())
};
if let Some(name) = first(
database,
&[
"Liberation Serif",
"DejaVu Serif",
"Noto Serif",
"Times New Roman",
"Georgia",
],
) {
database.set_serif_family(name);
}
if let Some(name) = first(
database,
&[
"Liberation Sans",
"DejaVu Sans",
"Noto Sans",
"Arial",
"Helvetica",
],
) {
database.set_sans_serif_family(name);
}
if let Some(name) = first(
database,
&[
"Liberation Mono",
"DejaVu Sans Mono",
"Noto Sans Mono",
"Courier New",
],
) {
database.set_monospace_family(name);
}
}
fn generic(family: &str) -> fontdb::Family<'static> {
let lower = family.to_ascii_lowercase();
if lower.contains("mono") || lower.contains("courier") || lower.contains("consol") {
return fontdb::Family::Monospace;
}
if lower.contains("times") || lower.contains("serif") || lower.contains("georgia") {
return fontdb::Family::Serif;
}
fontdb::Family::SansSerif
}
pub fn families_used(parts: &[&Element]) -> BTreeSet<String> {
let mut families = BTreeSet::new();
let mut faces: BTreeMap<String, String> = BTreeMap::new();
for root in parts {
collect(root, &mut families, &mut faces);
}
let resolved: BTreeSet<String> = families
.iter()
.map(|name| faces.get(name).cloned().unwrap_or_else(|| name.clone()))
.collect();
resolved
}
fn collect(
element: &Element,
families: &mut BTreeSet<String>,
faces: &mut BTreeMap<String, String>,
) {
if element.is(&Ns::Style, "font-face")
&& let Some(name) = element.attr(&Ns::Style, "name")
{
let family = element
.attr(&Ns::Svg, "font-family")
.unwrap_or(name)
.trim_matches('\'')
.to_owned();
faces.insert(name.to_owned(), family);
}
for (ns, local) in [(Ns::Style, "font-name"), (Ns::Fo, "font-family")] {
if let Some(name) = element.attr(&ns, local) {
let name = name.trim_matches('\'');
if !name.is_empty() {
families.insert(name.to_owned());
}
}
}
for child in element.elements() {
collect(child, families, faces);
}
}