use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use eframe::egui::{FontData, FontDefinitions, FontFamily};
use odox_core::{Element, Ns};
pub use odox_fonts::Variant;
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 for_document(parts: &[&Element]) -> FontDefinitions {
definitions(&families_used(parts), &characters_used(parts))
}
pub fn definitions(families: &BTreeSet<String>, characters: &BTreeSet<char>) -> FontDefinitions {
let mut definitions = FontDefinitions::default();
let fallback = definitions
.families
.get(&FontFamily::Proportional)
.cloned()
.unwrap_or_default();
let database = odox_fonts::database();
for family in families {
for variant in Variant::ALL {
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) =
odox_fonts::find(&database, family, variant).and_then(|id| load(&database, id))
{
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);
}
}
let mut added = Vec::new();
for &character in characters {
if character.is_whitespace() || character.is_control() {
continue;
}
let drawn = definitions
.font_data
.values()
.any(|face| odox_fonts::covers(&face.font, face.index, character));
if drawn {
continue;
}
let Some(face) = odox_fonts::faces_with(&database, character, Variant::default())
.next()
.and_then(|id| load(&database, id))
else {
continue;
};
let key = format!("machine fallback {}", added.len());
definitions.font_data.insert(key.clone(), Arc::new(face));
added.push(key);
}
for chain in definitions.families.values_mut() {
chain.extend(added.iter().cloned());
}
definitions
}
fn load(database: &odox_fonts::fontdb::Database, id: odox_fonts::fontdb::ID) -> Option<FontData> {
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(),
})
}
pub fn characters_used(parts: &[&Element]) -> BTreeSet<char> {
parts
.iter()
.flat_map(|part| part.plain_text().chars().collect::<Vec<_>>())
.collect()
}
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);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn family_of_names_a_variant_with_a_suffix() {
let plain = Variant {
bold: false,
italic: false,
};
let bold = Variant {
bold: true,
italic: false,
};
let italic = Variant {
bold: false,
italic: true,
};
let both = Variant {
bold: true,
italic: true,
};
assert_eq!(family_of("Arial", plain), FontFamily::Name("Arial".into()));
assert_eq!(
family_of("Arial", bold),
FontFamily::Name("Arial:bold".into())
);
assert_eq!(
family_of("Arial", italic),
FontFamily::Name("Arial:italic".into())
);
assert_eq!(
family_of("Arial", both),
FontFamily::Name("Arial:bolditalic".into())
);
}
#[test]
fn a_character_no_loaded_face_has_is_drawn_from_one_of_the_machines() {
let database = odox_fonts::database();
if odox_fonts::faces_with(&database, '日', Variant::default())
.next()
.is_none()
{
eprintln!("no face on this machine has 日: nothing to check");
return;
}
let families = BTreeSet::from(["Liberation Serif".to_owned()]);
let defs = definitions(&families, &BTreeSet::from(['日', 'a']));
let chain = &defs.families[&family_of("Liberation Serif", Variant::default())];
let added: Vec<&String> = chain
.iter()
.filter(|key| key.starts_with("machine fallback"))
.collect();
assert_eq!(added.len(), 1, "one face for the one character missing");
assert_eq!(chain.last(), Some(added[0]), "it comes last");
let face = &defs.font_data[added[0]];
assert!(odox_fonts::covers(&face.font, face.index, '日'));
}
}