use std::collections::BTreeSet;
use std::sync::LazyLock;
const UI_FACE: &[u8] = include_bytes!("../../../assets/fonts/DejaVuSansMono.ttf");
const JP_FACE: &[u8] = include_bytes!("../../../assets/fonts/NotoSansMonoCJKjp-Subset.otf");
fn be16(font: &[u8], at: usize) -> u16 {
u16::from_be_bytes([font[at], font[at + 1]])
}
fn be32(font: &[u8], at: usize) -> u32 {
u32::from_be_bytes([font[at], font[at + 1], font[at + 2], font[at + 3]])
}
fn table(font: &[u8], tag: &[u8; 4]) -> usize {
(0..be16(font, 4) as usize)
.map(|index| 12 + index * 16)
.find(|&record| &font[record..record + 4] == tag)
.map(|record| be32(font, record + 8) as usize)
.unwrap_or_else(|| panic!("no {} table", String::from_utf8_lossy(tag)))
}
pub fn characters_in_font(font: &[u8]) -> BTreeSet<char> {
let cmap = table(font, b"cmap");
let subtable = (0..be16(font, cmap + 2) as usize)
.map(|index| cmap + 4 + index * 8)
.map(|record| cmap + be32(font, record + 4) as usize)
.find(|&at| be16(font, at) == 4)
.expect("the face has no format 4 cmap");
let segments = be16(font, subtable + 6) as usize / 2;
let ends = subtable + 14;
let starts = ends + segments * 2 + 2;
let deltas = starts + segments * 2;
let ranges = deltas + segments * 2;
let mut out = BTreeSet::new();
for segment in 0..segments {
let at = segment * 2;
let (first, last) = (be16(font, starts + at), be16(font, ends + at));
for code in first..=last.min(0xfffe) {
let range_offset = be16(font, ranges + at);
let glyph = if range_offset == 0 {
code.wrapping_add(be16(font, deltas + at))
} else {
let index = ranges + at + range_offset as usize + (code - first) as usize * 2;
match be16(font, index) {
0 => 0,
glyph => glyph.wrapping_add(be16(font, deltas + at)),
}
};
if glyph != 0
&& let Some(ch) = char::from_u32(u32::from(code))
{
out.insert(ch);
}
}
}
out
}
fn em_advance(font: &[u8]) -> f32 {
let hhea = table(font, b"hhea");
let widest = be16(font, hhea + 10);
let metrics = be16(font, hhea + 34) as usize;
let last = be16(font, table(font, b"hmtx") + (metrics - 1) * 4);
assert_eq!(last, widest, "the face is not monospace");
f32::from(widest) / f32::from(be16(font, table(font, b"head") + 18))
}
struct Face {
covers: BTreeSet<char>,
advance: f32,
}
static UI: LazyLock<Face> = LazyLock::new(|| Face {
covers: characters_in_font(UI_FACE),
advance: em_advance(UI_FACE),
});
static JP: LazyLock<Face> = LazyLock::new(|| Face {
advance: em_advance(JP_FACE),
covers: characters_in_font(JP_FACE),
});
pub fn text_px(line: &str, font_px: f32) -> f32 {
line.chars()
.map(|ch| {
if UI.covers.contains(&ch) || !JP.covers.contains(&ch) {
UI.advance
} else {
JP.advance
}
})
.sum::<f32>()
* font_px
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_shipped_faces_measure_what_they_always_have() {
assert!(
(UI.advance - 1233.0 / 2048.0).abs() < 0.0001,
"DejaVu now advances {}",
UI.advance
);
assert!(
(JP.advance - 1.0).abs() < 0.0001,
"the Japanese subset now advances {}",
JP.advance
);
let ratio = JP.advance / UI.advance;
assert!((1.66..1.67).contains(&ratio), "ratio is {ratio}");
}
#[test]
fn each_half_of_a_mixed_line_is_measured_in_its_own_face() {
let px = |s| text_px(s, 100.0);
assert!((px("ab") - 2.0 * 100.0 * 1233.0 / 2048.0).abs() < 0.01);
assert!((px("かな") - 200.0).abs() < 0.01);
assert!((px("あa") - (100.0 + 100.0 * 1233.0 / 2048.0)).abs() < 0.01);
assert_eq!(px(""), 0.0);
}
#[test]
fn a_curly_quote_is_measured_in_the_face_that_draws_it() {
for quote in ['\u{201c}', '\u{201d}', '\u{201e}'] {
assert!(UI.covers.contains("e), "{quote:?}");
assert_eq!(text_px("e.to_string(), 100.0), text_px("x", 100.0));
}
}
}