use nox_gfx::geom::{Rect, IDENTITY}; use nox_gfx::path::{Path, Verb}; use nox_text::Font; const FIXTURE: &str = include_str!("fixture.txt"); #[derive(Default)] struct Expect { file: String, upem: u16, num_glyphs: u16, vmetrics: (i16, i16, i16), bbox: (i16, i16, i16, i16), has_cmap: bool, gids: Vec<(u32, u16)>, advances: Vec<(u16, u16)>, glyphs: Vec<(u16, usize, usize, bool, (i32, i32, i32, i32))>, kerning: Vec<(u16, u16, i16)>, } fn fixtures() -> Vec { let mut out = Vec::new(); let mut cur = Expect::default(); for line in FIXTURE.lines() { let mut it = line.split_whitespace(); let Some(key) = it.next() else { continue }; let mut num = |d: &mut dyn Iterator| -> i64 { d.next().and_then(|v| v.parse::().ok()).unwrap_or(0) }; match key { "FONT" => { cur = Expect::default(); cur.file = it.next().unwrap_or("").to_string(); } "UPEM" => cur.upem = num(&mut it) as u16, "NGLYPHS" => cur.num_glyphs = num(&mut it) as u16, "VMETRICS" => { cur.vmetrics = (num(&mut it) as i16, num(&mut it) as i16, num(&mut it) as i16) } "BBOX" => { cur.bbox = ( num(&mut it) as i16, num(&mut it) as i16, num(&mut it) as i16, num(&mut it) as i16, ) } "CMAP" => cur.has_cmap = num(&mut it) == 1, "GID" => { let cp = num(&mut it) as u32; cur.gids.push((cp, num(&mut it) as u16)); } "ADV" => { let g = num(&mut it) as u16; cur.advances.push((g, num(&mut it) as u16)); } "GLYPH" => { let g = num(&mut it) as u16; let nc = num(&mut it) as usize; let npts = num(&mut it) as usize; let _non = num(&mut it); let simple = num(&mut it) == 1; let bb = ( num(&mut it) as i32, num(&mut it) as i32, num(&mut it) as i32, num(&mut it) as i32, ); cur.glyphs.push((g, nc, npts, simple, bb)); } "KERN" => { let l = num(&mut it) as u16; let r = num(&mut it) as u16; cur.kerning.push((l, r, num(&mut it) as i16)); } "END" => out.push(std::mem::take(&mut cur)), _ => {} } } out } fn contours_and_bbox(font: &Font, gid: u16) -> (usize, Option) { let mut p = Path::new(); nox_text::outline(font, gid, &IDENTITY, &mut p); let closes = p.verbs.iter().filter(|v| **v == Verb::Close).count(); let bb = if p.points.is_empty() { None } else { Some(p.bounds()) }; (closes, bb) } #[test] fn matches_independent_reference_parser() { let fx = fixtures(); assert!(fx.len() > 50, "fixture looks empty ({} fonts)", fx.len()); let mut checked = 0usize; let mut skipped = 0usize; for e in &fx { let Ok(bytes) = std::fs::read(&e.file) else { skipped += 1; continue; }; let font = match Font::parse(&bytes) { Ok(f) => f, Err(err) => panic!("{}: reference parsed it but we failed: {err:?}", e.file), }; let f = &e.file; assert_eq!(font.units_per_em, e.upem, "{f}: units_per_em"); assert_eq!(font.num_glyphs, e.num_glyphs, "{f}: num_glyphs"); let upem = font.units_per_em as f32; let lm = font.line_metrics(); assert_eq!((lm.ascent * upem).round() as i16, e.vmetrics.0, "{f}: ascent"); assert_eq!((lm.descent * upem).round() as i16, e.vmetrics.1, "{f}: descent"); assert_eq!((lm.line_gap * upem).round() as i16, e.vmetrics.2, "{f}: line_gap"); assert_eq!(font.bbox(), e.bbox, "{f}: bbox"); assert_eq!(font.has_cmap(), e.has_cmap, "{f}: has_cmap"); for (cp, gid) in &e.gids { let ch = char::from_u32(*cp).unwrap(); assert_eq!(font.glyph_index(ch), *gid, "{f}: glyph_index({ch:?})"); } for (gid, adv) in &e.advances { assert_eq!(font.advance(*gid), *adv, "{f}: advance({gid})"); } for (gid, nc, npts, simple, bb) in &e.glyphs { let (got_nc, got_bb) = contours_and_bbox(&font, *gid); assert_eq!(got_nc, *nc, "{f}: contour count for glyph {gid}"); if *simple && *npts > 0 { let bb2 = got_bb.unwrap_or_else(|| panic!("{f}: glyph {gid} produced no points")); assert_eq!( ( bb2.x0.round() as i32, bb2.y0.round() as i32, bb2.x1.round() as i32, bb2.y1.round() as i32 ), *bb, "{f}: decoded point bbox for glyph {gid}" ); } } for (l, r, v) in &e.kerning { assert_eq!(font.kerning(*l, *r), *v, "{f}: kerning({l},{r})"); } checked += 1; } assert!(checked > 50, "only {checked} fonts checked, {skipped} skipped"); eprintln!("cross-checked {checked} fonts against the reference parser"); } #[test] fn every_installed_font_parses_and_outlines_without_panicking() { let mut ok = 0usize; let mut rejected = 0usize; for e in fixtures() { let Ok(bytes) = std::fs::read(&e.file) else { continue }; match Font::parse(&bytes) { Ok(font) => { let mut p = Path::new(); for gid in 0..font.num_glyphs.min(400) { p.clear(); nox_text::outline(&font, gid, &IDENTITY, &mut p); for q in &p.points { assert!(q.x.is_finite() && q.y.is_finite(), "{}: non-finite point", e.file); } } ok += 1; } Err(_) => rejected += 1, } } assert!(ok > 50, "only {ok} fonts parsed ({rejected} rejected)"); eprintln!("outlined glyphs from {ok} fonts, {rejected} rejected"); } fn lcg(s: &mut u64) -> u64 { *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); *s >> 17 } #[test] fn corrupted_fonts_are_rejected_or_survive_without_panicking() { let sources: Vec = fixtures().into_iter().map(|e| e.file).collect(); let mut seed = 0xabcd_1234_5678_9911u64; let mut cases = 0usize; for file in sources.iter().take(24) { let Ok(orig) = std::fs::read(file) else { continue }; if orig.len() < 1024 { continue; } for k in 1..=24 { let cut = orig.len() * k / 25; let data = &orig[..cut]; if let Ok(font) = Font::parse(data) { let mut p = Path::new(); for gid in [0u16, 1, 2, 40, 500, font.num_glyphs.saturating_sub(1), 65535] { p.clear(); nox_text::outline(&font, gid, &IDENTITY, &mut p); } for ch in ['A', 'z', '\u{5b57}', '\u{10FFFF}'] { let _ = font.glyph_index(ch); } let _ = font.advance(60000); let _ = font.kerning(3, 9); } cases += 1; } for _ in 0..64 { let mut data = orig.clone(); let hits = 1 + (lcg(&mut seed) % 16) as usize; for _ in 0..hits { let i = (lcg(&mut seed) as usize) % data.len(); data[i] = (lcg(&mut seed) & 0xff) as u8; } if let Ok(font) = Font::parse(&data) { let mut p = Path::new(); for gid in [0u16, 3, 36, 200, 5000, 65535] { p.clear(); nox_text::outline(&font, gid, &IDENTITY, &mut p); assert!(p.points.len() < 4_000_000, "runaway outline"); } for ch in ['A', ' ', '\u{5b57}'] { let _ = font.glyph_index(ch); } } cases += 1; } } assert!(cases > 500, "only {cases} corruption cases exercised"); eprintln!("survived {cases} truncated/corrupted font cases"); } #[test] fn junk_input_is_rejected_cleanly() { assert!(Font::parse(&[]).is_err()); assert!(Font::parse(&[0; 4]).is_err()); assert!(Font::parse(&[0xff; 4096]).is_err()); assert!(Font::parse(b"OTTO____________").is_err()); let mut fake = vec![0u8; 64]; fake[0..4].copy_from_slice(&0x0001_0000u32.to_be_bytes()); fake[4..6].copy_from_slice(&9999u16.to_be_bytes()); assert!(Font::parse(&fake).is_err()); } #[test] fn glyph_cache_does_not_confuse_faces() { use nox_text::GlyphCache; let files = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", ]; let blobs: Vec> = files.iter().filter_map(|f| std::fs::read(f).ok()).collect(); if blobs.len() < 2 { return; } let fonts: Vec = blobs.iter().map(|b| Font::parse(b).unwrap()).collect(); for (i, a) in fonts.iter().enumerate() { for (j, b) in fonts.iter().enumerate() { if i < j { assert_ne!(a.id(), b.id(), "{} and {} share a face id", files[i], files[j]); } } } let mut shared = GlyphCache::new(); for (i, font) in fonts.iter().enumerate() { let mut solo = GlyphCache::new(); for ch in "AaBbGgQqWw@#8".chars() { let gid = font.glyph_index(ch); for px in [11.0f32, 17.0, 31.0] { for subx in 0..4u8 { let want = solo.get(font, gid, px, subx).cloned().unwrap(); let got = shared.get(font, gid, px, subx).cloned().unwrap(); assert_eq!( (got.w, got.h, got.left, got.top), (want.w, want.h, want.left, want.top), "{}: metrics differ for {ch:?} at {px}px via shared cache", files[i] ); assert_eq!(got.a, want.a, "{}: pixels differ for {ch:?} at {px}px", files[i]); } } } } }