pub fn vmtx_advance_for_glyph(face_data: &[u8], glyph_id: u16, em_size: f32) -> f32 {
if face_data.is_empty() || em_size <= 0.0 {
return em_size;
}
let face = match ttf_parser::Face::parse(face_data, 0) {
Ok(f) => f,
Err(_) => return em_size,
};
let units_per_em = face.units_per_em();
if units_per_em == 0 {
return em_size;
}
match face.glyph_ver_advance(ttf_parser::GlyphId(glyph_id)) {
Some(adv) => adv as f32 * em_size / units_per_em as f32,
None => em_size,
}
}
pub fn is_upright_in_vertical(c: char) -> bool {
let cp = c as u32;
(0x4E00..=0x9FFF).contains(&cp)
|| (0x3400..=0x4DBF).contains(&cp)
|| (0x2_0000..=0x2_A6DF).contains(&cp)
|| (0x3040..=0x309F).contains(&cp)
|| (0x30A0..=0x30FF).contains(&cp)
|| (0x3000..=0x303F).contains(&cp)
|| (0x3200..=0x32FF).contains(&cp)
|| (0xFF01..=0xFF60).contains(&cp)
|| (0xAC00..=0xD7A3).contains(&cp)
|| (0x1100..=0x11FF).contains(&cp)
|| (0x3100..=0x312F).contains(&cp)
|| (0x2F00..=0x2FDF).contains(&cp)
}
pub(crate) struct ParsedFaceCache<'a> {
faces: std::collections::HashMap<usize, Option<(ttf_parser::Face<'a>, u16)>>,
}
impl<'a> ParsedFaceCache<'a> {
pub(crate) fn new() -> Self {
Self {
faces: std::collections::HashMap::new(),
}
}
pub(crate) fn vmtx_advance_or_default(
&mut self,
face_data: &'a [u8],
glyph_id: u16,
em_size: f32,
) -> f32 {
if face_data.is_empty() || em_size <= 0.0 {
return em_size;
}
let key = face_data.as_ptr() as usize;
let entry = self.faces.entry(key).or_insert_with(|| {
let face = ttf_parser::Face::parse(face_data, 0).ok()?;
let upem = face.units_per_em();
if upem == 0 {
return None;
}
Some((face, upem))
});
match entry {
Some((face, upem)) => match face.glyph_ver_advance(ttf_parser::GlyphId(glyph_id)) {
Some(adv) => adv as f32 * em_size / (*upem as f32),
None => em_size,
},
None => em_size,
}
}
}
pub struct VerticalMetrics {
pub advance: f32,
pub upright: bool,
}
impl VerticalMetrics {
pub fn for_char(c: char, em_size: f32) -> Self {
Self {
advance: em_size,
upright: is_upright_in_vertical(c),
}
}
pub fn for_glyph(face_data: &[u8], glyph_id: u16, c: char, em_size: f32) -> Self {
Self {
advance: vmtx_advance_for_glyph(face_data, glyph_id, em_size),
upright: is_upright_in_vertical(c),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use std::sync::Arc;
#[test]
fn parsed_face_cache_returns_em_size_for_empty_face() {
let mut cache = ParsedFaceCache::new();
let font: Arc<[u8]> = Arc::from(&[][..]);
assert_eq!(cache.vmtx_advance_or_default(font.as_ref(), 0, 16.0), 16.0);
}
#[test]
fn parsed_face_cache_returns_em_size_for_garbage_face() {
let mut cache = ParsedFaceCache::new();
let font: Arc<[u8]> = Arc::from(b"not a font".as_slice());
assert_eq!(cache.vmtx_advance_or_default(font.as_ref(), 0, 16.0), 16.0);
}
#[test]
fn parsed_face_cache_returns_em_size_for_zero_em() {
let mut cache = ParsedFaceCache::new();
let font: Arc<[u8]> = Arc::from(b"not a font".as_slice());
assert_eq!(cache.vmtx_advance_or_default(font.as_ref(), 0, 0.0), 0.0);
}
#[test]
fn parsed_face_cache_single_parse_per_face() {
let mut cache = ParsedFaceCache::new();
let font: Arc<[u8]> = Arc::from(b"garbage bytes".as_slice());
for _ in 0..100 {
let _ = cache.vmtx_advance_or_default(font.as_ref(), 1, 16.0);
}
assert_eq!(
cache.faces.len(),
1,
"only one cache entry per unique data pointer"
);
}
#[test]
fn parsed_face_cache_two_fonts_two_entries() {
let mut cache = ParsedFaceCache::new();
let font_a: Arc<[u8]> = Arc::from(b"garbage_a".as_slice());
let font_b: Arc<[u8]> = Arc::from(b"garbage_b".as_slice());
let _ = cache.vmtx_advance_or_default(font_a.as_ref(), 0, 16.0);
let _ = cache.vmtx_advance_or_default(font_b.as_ref(), 0, 16.0);
assert_eq!(
cache.faces.len(),
2,
"two distinct fonts → two cache entries"
);
}
#[test]
fn parsed_face_cache_matches_uncached_for_invalid_font() {
let font: Arc<[u8]> = Arc::from(b"not a valid font".as_slice());
let uncached = vmtx_advance_for_glyph(font.as_ref(), 5, 20.0);
let mut cache = ParsedFaceCache::new();
let cached = cache.vmtx_advance_or_default(font.as_ref(), 5, 20.0);
assert_eq!(
cached, uncached,
"cached and uncached paths must agree for invalid font data"
);
}
#[test]
fn cjk_ideograph_is_upright() {
assert!(is_upright_in_vertical('日'));
assert!(is_upright_in_vertical('語'));
}
#[test]
fn latin_letter_is_rotated() {
assert!(!is_upright_in_vertical('A'));
assert!(!is_upright_in_vertical('z'));
}
#[test]
fn vertical_metrics_advance_equals_em() {
let vm = VerticalMetrics::for_char('日', 16.0);
assert!((vm.advance - 16.0).abs() < f32::EPSILON);
assert!(vm.upright);
}
#[test]
fn vmtx_advance_empty_face_returns_em_size() {
assert_eq!(vmtx_advance_for_glyph(&[], 0, 16.0), 16.0);
}
#[test]
fn vmtx_advance_invalid_face_returns_em_size() {
assert_eq!(vmtx_advance_for_glyph(b"not a font", 0, 16.0), 16.0);
}
#[test]
fn vmtx_advance_zero_em_size() {
assert_eq!(vmtx_advance_for_glyph(&[], 0, 0.0), 0.0);
}
#[test]
fn vmtx_advance_scales_linearly_with_em() {
let candidates = [
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures/test-font.ttf")
.to_path_buf(),
Path::new("/Library/Fonts/Arial Unicode.ttf").to_path_buf(),
Path::new("/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf").to_path_buf(),
];
let font_bytes = candidates
.iter()
.filter(|p| p.exists())
.find_map(|p| std::fs::read(p).ok());
let bytes = match font_bytes {
Some(b) => b,
None => return, };
let face = match ttf_parser::Face::parse(&bytes, 0) {
Ok(f) => f,
Err(_) => return,
};
let gid = (1u16..=100).find(|&g| face.glyph_ver_advance(ttf_parser::GlyphId(g)).is_some());
let gid = match gid {
Some(g) => g,
None => return, };
let adv16 = vmtx_advance_for_glyph(&bytes, gid, 16.0);
let adv32 = vmtx_advance_for_glyph(&bytes, gid, 32.0);
assert!(
(adv32 - 2.0 * adv16).abs() < 1e-3,
"adv at 32px should be 2× adv at 16px: adv16={adv16}, adv32={adv32}"
);
}
#[test]
fn for_glyph_upright_cjk() {
let vm = VerticalMetrics::for_glyph(&[], 0, '日', 16.0);
assert!(vm.upright);
assert!((vm.advance - 16.0).abs() < f32::EPSILON);
}
#[test]
fn for_glyph_rotated_latin() {
let vm = VerticalMetrics::for_glyph(&[], 0, 'A', 16.0);
assert!(!vm.upright);
assert!((vm.advance - 16.0).abs() < f32::EPSILON);
}
}