use std::collections::HashMap;
const CAMBRIA: &[(u16, char, Option<u16>)] = &[
(3, ' ', Some(220)),
(4, 'A', Some(623)),
(5, 'B', Some(611)),
(6, 'C', Some(563)),
(7, 'D', Some(662)),
(8, 'E', Some(575)),
(9, 'F', Some(537)),
(10, 'G', Some(611)),
(11, 'H', Some(687)),
(12, 'I', Some(324)),
(13, 'J', Some(307)),
(14, 'K', Some(629)),
(15, 'L', Some(537)),
(16, 'M', Some(815)),
(17, 'N', Some(681)),
(18, 'O', Some(653)),
(19, 'P', Some(568)),
(20, 'Q', Some(653)),
(21, 'R', Some(621)),
(22, 'S', Some(496)),
(23, 'T', Some(593)),
(24, 'U', Some(648)),
(25, 'V', Some(604)),
(26, 'W', Some(921)),
(27, 'X', Some(571)),
(28, 'Y', Some(570)),
(29, 'Z', None),
(131, 'a', Some(488)),
(132, 'b', Some(547)),
(133, 'c', Some(441)),
(134, 'd', Some(555)),
(135, 'e', Some(488)),
(136, 'f', Some(303)),
(137, 'g', Some(494)),
(138, 'h', Some(552)),
(139, 'i', Some(278)),
(140, 'j', Some(266)),
(141, 'k', Some(524)),
(142, 'l', Some(271)),
(143, 'm', Some(832)),
(144, 'n', Some(558)),
(145, 'o', Some(531)),
(146, 'p', Some(556)),
(147, 'q', Some(547)),
(148, 'r', Some(414)),
(149, 's', Some(430)),
(150, 't', Some(338)),
(151, 'u', Some(552)),
(152, 'v', Some(504)),
(153, 'w', Some(774)),
(154, 'x', Some(483)),
(155, 'y', Some(504)),
(156, 'z', Some(455)),
(428, '&', Some(688)), (481, ',', Some(205)),
(482, ';', Some(264)), (483, ':', Some(264)), (484, '.', Some(205)),
(486, '-', Some(332)), (491, '?', Some(422)), (495, '\u{2019}', Some(221)), (498, '\u{201C}', Some(375)), (499, '\u{201D}', Some(375)),
(512, '/', Some(490)), (514, '\u{2013}', Some(500)), (523, '(', Some(382)),
(524, ')', Some(382)),
(882, '0', Some(554)),
(883, '1', Some(554)),
(884, '2', Some(554)),
(885, '3', Some(554)),
(886, '4', Some(554)),
(887, '5', Some(554)),
(888, '6', Some(554)),
(889, '7', Some(554)),
(890, '8', Some(554)),
(891, '9', Some(554)),
(938, '+', Some(554)), (945, '=', Some(554)),
];
const WIDTH_TOLERANCE: i32 = 2;
const MIN_SAMPLE: usize = 8;
pub(super) fn resolve(
base_font: &str,
bare: &[(usize, u16)],
declared: &HashMap<u32, f32>,
) -> Option<Vec<(usize, char)>> {
if !is_regular_face(base_font) || bare.is_empty() {
return None;
}
let mut resolved = Vec::with_capacity(bare.len());
let mut checked = 0usize;
for &(code, gid) in bare {
let Some((ch, reference)) = lookup(gid) else { continue };
if let (Some(reference), Some(width)) = (reference, declared.get(&(code as u32))) {
if (thousandths(*width) - i32::from(reference)).abs() > WIDTH_TOLERANCE {
return None;
}
checked += 1;
}
resolved.push((code, ch));
}
(checked >= MIN_SAMPLE).then_some(resolved)
}
fn is_regular_face(base_font: &str) -> bool {
let name = base_font.split_once('+').map_or(base_font, |(_, rest)| rest);
name.eq_ignore_ascii_case("Cambria")
}
fn lookup(gid: u16) -> Option<(char, Option<u16>)> {
CAMBRIA
.binary_search_by(|(g, _, _)| g.cmp(&gid))
.ok()
.map(|i| (CAMBRIA[i].1, CAMBRIA[i].2))
}
fn thousandths(em: f32) -> i32 {
(em * 1000.0).round() as i32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_table_is_sorted_and_its_alphabet_runs_are_complete() {
assert!(CAMBRIA.windows(2).all(|w| w[0].0 < w[1].0), "table must stay sorted for lookup");
for (base, first, last) in [(4u16, 'A', 'Z'), (131, 'a', 'z'), (882, '0', '9')] {
let span = u16::from(u8::try_from(last as u32 - first as u32).unwrap());
for offset in 0..=span {
let want = char::from_u32(first as u32 + u32::from(offset)).unwrap();
assert_eq!(lookup(base + offset).map(|(c, _)| c), Some(want));
}
}
}
fn tam_widths() -> HashMap<u32, f32> {
CAMBRIA
.iter()
.enumerate()
.filter_map(|(i, (_, _, w))| w.map(|w| (i as u32, f32::from(w) / 1000.0)))
.collect()
}
fn tam_bare() -> Vec<(usize, u16)> {
CAMBRIA.iter().enumerate().map(|(i, (gid, _, _))| (i, *gid)).collect()
}
#[test]
fn a_matching_font_resolves_every_glyph_in_the_table() {
let bare = tam_bare();
let resolved = resolve("EMMOLK+Cambria", &bare, &tam_widths()).expect("should resolve");
assert_eq!(resolved.len(), CAMBRIA.len());
for (code, gid) in &bare {
let want = lookup(*gid).map(|(c, _)| c);
assert_eq!(resolved.iter().find(|(c, _)| c == code).map(|(_, c)| *c), want);
}
}
#[test]
fn one_disagreeing_width_refuses_the_whole_font() {
let mut widths = tam_widths();
let a = tam_bare().iter().position(|(_, gid)| *gid == 4).unwrap() as u32;
widths.insert(a, (623.0 + 3.0) / 1000.0);
assert_eq!(resolve("EMMOLK+Cambria", &tam_bare(), &widths), None);
widths.insert(a, (623.0 + 2.0) / 1000.0);
assert!(resolve("EMMOLK+Cambria", &tam_bare(), &widths).is_some());
}
#[test]
fn another_face_and_too_small_a_sample_are_both_refused() {
assert_eq!(resolve("EMMONL+Cambria-Bold", &tam_bare(), &tam_widths()), None);
assert_eq!(resolve("EMMPCK+Cambria-Italic", &tam_bare(), &tam_widths()), None);
assert_eq!(resolve("ABCDEF+Calibri", &tam_bare(), &tam_widths()), None);
let two = vec![(2usize, 3u16), (3, 486)];
assert_eq!(resolve("EMMOLK+Cambria", &two, &tam_widths()), None);
}
#[test]
fn the_glyphs_context_could_not_identify_stay_unmapped() {
assert_eq!(lookup(820), None);
assert_eq!(lookup(821), None);
}
}