#![forbid(unsafe_code)]
pub use fontdb;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Variant {
pub bold: bool,
pub italic: bool,
}
impl Variant {
pub const ALL: [Self; 4] = [
Self {
bold: false,
italic: false,
},
Self {
bold: true,
italic: false,
},
Self {
bold: false,
italic: true,
},
Self {
bold: true,
italic: true,
},
];
}
pub fn database() -> fontdb::Database {
let mut database = fontdb::Database::new();
database.load_system_fonts();
set_generics(&mut database);
database
}
pub fn find(database: &fontdb::Database, family: &str, variant: Variant) -> Option<fontdb::ID> {
let mut wanted = vec![fontdb::Family::Name(family)];
wanted.extend(substitute_families(family));
query(database, &wanted, variant)
}
pub fn find_substitute(
database: &fontdb::Database,
family: &str,
variant: Variant,
) -> Option<fontdb::ID> {
query(database, &substitute_families(family), variant)
}
fn substitute_families(family: &str) -> Vec<fontdb::Family<'static>> {
let mut wanted: Vec<fontdb::Family<'static>> = metric_substitutes(family)
.iter()
.map(|name| fontdb::Family::Name(name))
.collect();
wanted.push(generic(family));
wanted
}
fn query(
database: &fontdb::Database,
families: &[fontdb::Family<'_>],
variant: Variant,
) -> Option<fontdb::ID> {
database.query(&fontdb::Query {
families,
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
},
})
}
pub fn covers(data: &[u8], index: u32, character: char) -> bool {
ttf_parser::Face::parse(data, index)
.ok()
.and_then(|face| face.glyph_index(character))
.is_some_and(|glyph| glyph.0 != 0)
}
pub fn faces_with(
database: &fontdb::Database,
character: char,
variant: Variant,
) -> impl Iterator<Item = fontdb::ID> + '_ {
[
fontdb::Family::SansSerif,
fontdb::Family::Serif,
fontdb::Family::Monospace,
]
.into_iter()
.filter_map(move |generic| query(database, &[generic], variant))
.chain(database.faces().map(|info| info.id))
.filter(move |&id| {
database
.with_face_data(id, |data, index| covers(data, index, character))
.unwrap_or(false)
})
}
pub 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| {
query(database, &[fontdb::Family::Name(name)], Variant::default()).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);
}
}
pub 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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metric_substitutes_matches_regardless_of_case_or_spaces() {
assert_eq!(
metric_substitutes("Times New Roman"),
["Liberation Serif", "DejaVu Serif"]
);
assert_eq!(
metric_substitutes("TIMES NEW ROMAN"),
["Liberation Serif", "DejaVu Serif"]
);
assert_eq!(
metric_substitutes("TimesNewRoman"),
["Liberation Serif", "DejaVu Serif"]
);
assert_eq!(metric_substitutes("Calibri"), ["Carlito"]);
}
#[test]
fn metric_substitutes_is_empty_for_a_family_with_no_known_substitute() {
assert_eq!(metric_substitutes("Comic Sans MS"), &[] as &[&str]);
}
#[test]
fn generic_recognises_monospace_families() {
assert_eq!(generic("Liberation Mono"), fontdb::Family::Monospace);
assert_eq!(generic("Courier New"), fontdb::Family::Monospace);
assert_eq!(generic("Consolas"), fontdb::Family::Monospace);
}
#[test]
fn generic_recognises_serif_families() {
assert_eq!(generic("Times New Roman"), fontdb::Family::Serif);
assert_eq!(generic("Liberation Serif"), fontdb::Family::Serif);
assert_eq!(generic("Georgia"), fontdb::Family::Serif);
}
#[test]
fn generic_defaults_to_sans_serif_rather_than_matching_serif_by_accident() {
assert_eq!(generic("Liberation Sans"), fontdb::Family::SansSerif);
assert_eq!(generic("Wingdings"), fontdb::Family::SansSerif);
}
}