use super::features::PageWord;
pub const STYLE_FEATURES: [&str; 5] = [
"weight_vs_body",
"colour_vs_body",
"face_vs_body",
"family_vs_body",
"style_share",
];
const WEIGHTS: [(&str, i32); 18] = [
("extralight", 200),
("ultralight", 200),
("semibold", 600),
("demibold", 600),
("extrabold", 800),
("ultrabold", 800),
("hairline", 100),
("thin", 100),
("light", 300),
("medium", 500),
("demi", 600),
("heavy", 800),
("black", 900),
("bold", 700),
("book", 400),
("regular", 400),
("roman", 400),
("normal", 400),
];
fn face(word: &PageWord) -> &str {
let name = word.font.as_deref().unwrap_or("");
let bytes = name.as_bytes();
if bytes.len() >= 7 && bytes[6] == b'+' && bytes[..6].iter().all(u8::is_ascii_uppercase) {
&name[7..]
} else {
name
}
}
fn family(name: &str) -> &str {
name.split(['-', ',']).next().unwrap_or("")
}
fn weight(word: &PageWord) -> i32 {
let name = face(word).to_lowercase();
WEIGHTS
.iter()
.find(|(token, _)| name.contains(token))
.map_or(if word.bold { 700 } else { 400 }, |&(_, w)| w)
}
fn tenths(size: f64) -> i64 {
(size * 10.0).round() as i64
}
struct Tally<K: PartialEq> {
keys: Vec<(K, usize)>,
}
impl<K: PartialEq> Tally<K> {
fn new() -> Self {
Self { keys: Vec::new() }
}
fn add(&mut self, key: K, n: usize) {
match self.keys.iter_mut().find(|(k, _)| *k == key) {
Some((_, count)) => *count += n,
None => self.keys.push((key, n)),
}
}
fn get(&self, key: &K) -> usize {
self.keys
.iter()
.find(|(k, _)| k == key)
.map_or(0, |(_, n)| *n)
}
fn most(&self) -> Option<&K> {
let best = self.keys.iter().map(|(_, n)| *n).max()?;
self.keys.iter().find(|(_, n)| *n == best).map(|(k, _)| k)
}
}
fn chars(word: &PageWord) -> usize {
word.text.chars().count()
}
#[doc(hidden)]
pub fn page_style(n_blocks: usize, words: &[PageWord]) -> Vec<Vec<[f64; 5]>> {
page_style_on_grouped(&super::lines::grouped_lines(words, n_blocks), words)
}
#[doc(hidden)]
pub fn page_style_on_grouped(
grouped: &[Vec<Vec<&PageWord>>],
words: &[PageWord],
) -> Vec<Vec<[f64; 5]>> {
type Style<'a> = (&'a str, Option<[u8; 3]>, i64);
let mut body: Tally<(&str, Option<[u8; 3]>)> = Tally::new();
let mut styles: Tally<Style> = Tally::new();
for w in words {
body.add((face(w), w.color), chars(w));
styles.add((face(w), w.color, tenths(w.size)), chars(w));
}
let total: usize = body.keys.iter().map(|(_, n)| n).sum();
let (body_face, body_colour) = body
.most()
.copied()
.filter(|_| total > 0)
.unwrap_or(("", None));
let body_weight = words
.iter()
.find(|w| face(w) == body_face)
.map_or(400, weight);
let mut out = Vec::with_capacity(grouped.len());
for lines in grouped {
let mut rows = Vec::new();
for line in lines {
let n = line.iter().map(|w| chars(w)).sum::<usize>().max(1) as f64;
let share = |pred: &dyn Fn(&PageWord) -> bool| {
line.iter()
.filter(|w| pred(w))
.map(|w| chars(w))
.sum::<usize>() as f64
/ n
};
let mean_weight = line
.iter()
.map(|w| f64::from(weight(w)) * chars(w) as f64)
.sum::<f64>()
/ n;
let mut own: Tally<Style> = Tally::new();
for w in line {
own.add((face(w), w.color, tenths(w.size)), chars(w));
}
let rarity = match own.most() {
Some(mine) if total > 0 => styles.get(mine) as f64 / total as f64,
_ => 0.0,
};
rows.push([
(mean_weight - f64::from(body_weight)) / 100.0,
share(&|w| w.color != body_colour),
share(&|w| face(w) != body_face),
share(&|w| family(face(w)) != family(body_face)),
rarity,
]);
}
out.push(rows);
}
out
}