refactor(fonts): move lib.rs tests to tests.rs

This commit is contained in:
aiongg
2026-10-26 22:08:44 +00:00
parent 50feadc965
commit 00eb5caad6
2 changed files with 327 additions and 329 deletions
+1 -329
View File
@@ -168,332 +168,4 @@ pub fn first_glyph(face: &FontFace, chars: &[char]) -> u32 {
}
#[cfg(test)]
mod tests {
use super::*;
fn any_face() -> std::sync::Arc<FontFace> {
FontDb::global().face(DEFAULT_FAMILY, "Regular")
}
#[test]
fn emoji_clusters_are_recognised() {
let c = |s: &str| s.chars().collect::<Vec<char>>();
// As PowerPoint draws them: emoji-presentation characters (⭐ ✅ ☔ ⌚ ⏩) in colour even
// without a selector, text-presentation ones (♥ ✔ ☎ ⬆ ▶) as text unless U+FE0F asks, and
// keycaps always as text.
for yes in ["😀", "👍🏽", "❤\u{FE0F}", "👨\u{200D}👩\u{200D}👧", "🇯🇵", "🏳\u{FE0F}\u{200D}🌈", "⭐", "✅", "☔", "⌚", "⏩", "🀄"]
{
assert!(is_emoji_cluster(&c(yes)), "{yes:?}");
}
for no in ["a", "1", "❤", "☺\u{FE0E}", "😀\u{FE0E}", "字", "→", "♥", "✔", "☎", "⬆", "▶", "1\u{FE0F}\u{20E3}", "#\u{20E3}"]
{
assert!(!is_emoji_cluster(&c(no)), "{no:?}");
}
}
#[test]
fn colour_fonts_are_recognised() {
let db = FontDb::with_font_dirs(Vec::new());
assert!(db.add_font(include_bytes!("../../render/tests/fixtures/colr-squares.ttf").to_vec()) > 0);
assert!(db.face("DeckCraft Colr Squares", "Regular").is_color());
assert!(!fontdb::last_resort_face().is_color());
}
#[test]
fn last_resort_face_parses() {
let f = fontdb::last_resort_face();
assert_eq!(f.family, fontdb::LAST_RESORT_FAMILY);
assert!(f.upem > 0.0);
}
#[test]
fn line_split_follows_the_windows_ascent_and_descent() {
use skrifa::raw::TableProvider;
let f = fontdb::last_resort_face();
let font = skrifa::FontRef::new(f.data()).unwrap();
let os2 = font.os2().unwrap();
let (a, d) = (os2.us_win_ascent() as f64, os2.us_win_descent() as f64);
assert!((f.line_split - a / (a + d)).abs() < 1e-9, "{} vs {}", f.line_split, a / (a + d));
assert!(f.line_split > 0.5 && f.line_split < 1.0);
}
#[test]
fn some_face_always_resolves() {
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
let f = db.face("No Such Font Anywhere", "Regular");
assert!(!f.family.is_empty());
assert!(!shape(&f, "Hello", &[], |c| c).is_empty());
}
#[test]
fn filtered_fallback_continues_after_a_rejected_loaded_face() {
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
assert_eq!(db.add_font(include_bytes!("../tests/fixtures/legacy-regular.ttf").to_vec()), 1);
let first = db.fallback_for('A', u32::MAX).unwrap();
let accepted_family = if first.family == LAST_RESORT_FAMILY { "DeckCraft Legacy" } else { LAST_RESORT_FAMILY };
let fallback = db.fallback_for_filtered('A', u32::MAX, |f| f.family == accepted_family).unwrap();
assert_eq!(fallback.family, accepted_family);
assert!(fallback.covers('A'));
assert_ne!(fallback.id(), first.id());
assert_eq!(db.fallback_for('A', u32::MAX).unwrap().id(), first.id(), "document fallback order is unchanged");
assert!(db.fallback_for_filtered('A', fallback.id(), |f| f.family == accepted_family).is_none(), "the excluded face stays excluded");
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn filtered_fallback_loads_an_eligible_system_face_and_does_not_cache_policy_misses() {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let db = FontDb::with_font_dirs(vec![dir]);
let first = db.fallback_for('A', u32::MAX).unwrap();
assert_ne!(first.family, "DeckCraft Legacy");
assert!(db.fallback_for_filtered('A', u32::MAX, |_| false).is_none());
assert!(db.fallback_for('A', u32::MAX).is_some(), "an interface policy miss does not hide document glyphs");
let accepted = db.fallback_for_filtered('A', u32::MAX, |f| f.family == "DeckCraft Legacy").unwrap();
assert_eq!(accepted.family, "DeckCraft Legacy");
assert!(accepted.covers('A'));
}
#[test]
fn concurrent_lookups_all_find_an_installed_family() {
// Threads resolving an installed family nobody had loaded yet used to fall back to
// another face when a different thread finished loading it first, so the same text was
// laid out in different fonts depending on timing.
let dir = std::env::temp_dir().join(format!("deckcraft-fonts-concurrent-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("Hack-Regular.ttf"), epaint_default_fonts::HACK_REGULAR).unwrap();
for _ in 0..20 {
let db = FontDb::with_font_dirs(vec![dir.clone()]);
db.set_system_fallback(false);
let start = std::sync::Barrier::new(8);
let got: Vec<String> = std::thread::scope(|s| {
let threads: Vec<_> = (0..8)
.map(|_| {
s.spawn(|| {
start.wait();
db.face("Hack", "Regular").family.clone()
})
})
.collect();
threads.into_iter().map(|t| t.join().unwrap()).collect()
});
assert!(got.iter().all(|f| f == "Hack"), "{got:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn windows_family_names_find_their_faces() {
// Documents name fonts by their Windows family ("Segoe UI Semibold"); the font's
// typographic family is "Segoe UI" with a "Semibold" style. The fixture is named the same way.
let check = |db: &FontDb| {
assert!(db.has_family("DeckCraft Legacy Semibold"));
for style in ["Regular", "Bold"] {
let f = db.face("DeckCraft Legacy Semibold", style);
assert_eq!((f.family.as_str(), f.style.as_str()), ("DeckCraft Legacy", "Semibold"), "{style}");
}
assert_eq!(db.document_face("DeckCraft Legacy Semibold", "Regular").family, "DeckCraft Legacy", "installed, so not Calibri");
// The typographic name still works.
assert_eq!(db.face("DeckCraft Legacy", "Semibold").style, "Semibold");
};
// Loaded directly…
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
assert_eq!(db.add_font(include_bytes!("../tests/fixtures/legacy-semibold.ttf").to_vec()), 1);
check(&db);
// …and found by the system font scan under its Windows name, then loaded.
#[cfg(not(target_arch = "wasm32"))]
{
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let db = FontDb::with_font_dirs(vec![dir.clone()]);
db.set_system_fallback(false);
check(&db);
// Looking up the Windows name loads the Semibold file only; a later lookup of the
// family's Regular must still load and pick the Regular file, not settle for Semibold.
let db = FontDb::with_font_dirs(vec![dir]);
db.set_system_fallback(false);
assert_eq!(db.face("DeckCraft Legacy Semibold", "Regular").style, "Semibold");
let regular = db.face("DeckCraft Legacy", "Regular");
assert_eq!((regular.style.as_str(), regular.weight), ("Regular", 400.0));
}
}
#[test]
fn faces_without_outlines_are_not_cataloged() {
// Rename the Semibold's `glyf` table, as in fonts whose glyphs are bitmaps or are stored in
// Apple's `hvgl` table. The face still maps "A", but its outline is empty. The new tag
// keeps the table directory sorted.
let mut semibold = include_bytes!("../tests/fixtures/legacy-semibold.ttf").to_vec();
let tables = u16::from_be_bytes([semibold[4], semibold[5]]) as usize;
let glyf = (0..tables).map(|i| 12 + 16 * i).find(|&r| &semibold[r..r + 4] == b"glyf").unwrap();
semibold[glyf..glyf + 4].copy_from_slice(b"gxyz");
let loaded = FontDb::with_font_dirs(Vec::new());
assert_eq!(loaded.add_font(semibold.clone()), 1);
let face = loaded.face("DeckCraft Legacy", "Semibold");
assert!(face.covers('A'));
assert!(loaded.outline(&face, face.glyph_for('A')).elements().is_empty());
// The system font scan lists the Regular, which has outlines, and leaves out the Semibold
// under both of its names.
#[cfg(not(target_arch = "wasm32"))]
{
let dir = std::env::temp_dir().join(format!("deckcraft-fonts-outlines-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("legacy-semibold.ttf"), &semibold).unwrap();
std::fs::write(dir.join("legacy-regular.ttf"), include_bytes!("../tests/fixtures/legacy-regular.ttf")).unwrap();
let db = FontDb::with_font_dirs(vec![dir.clone()]);
db.set_system_fallback(false);
assert_eq!(db.styles("DeckCraft Legacy"), ["Regular"]);
assert!(!db.has_family("DeckCraft Legacy Semibold"));
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn missing_document_fonts_are_drawn_in_calibri() {
// PowerPoint draws a missing family in Calibri, whatever its panose or the theme says.
let db = FontDb::global();
for missing in ["Qqq Nonexistent", "Avenir Next Qqq", "", "Arimo Qqq"] {
let f = db.document_face(missing, "Bold");
assert_eq!(f.family, db.face(MISSING_FONT_FAMILY, "Bold").family, "{missing:?}");
}
// PowerPoint's own twins and Windows' substitutes, when the font itself is missing.
for (missing, twin) in
[("Liberation Sans", "Arial"), ("Liberation Serif", "Times New Roman"), ("Arial CE", "Arial"), ("Times New Roman CYR", "Times New Roman")]
{
if !db.has_family(missing) {
assert_eq!(db.document_face(missing, "Regular").family, db.face(twin, "Regular").family, "{missing}");
}
}
// Metric-compatible stand-ins and DeckCraft's default keep their own fallbacks.
// So do Office's cloud fonts, which PowerPoint downloads and draws as themselves.
for kept in ["Arial", "Times New Roman", "Cambria", "Courier New", FALLBACK_FAMILY, "Aptos", "Aptos Display", "Grandview"] {
assert_eq!(db.document_face(kept, "Regular").family, db.face(kept, "Regular").family, "{kept}");
}
}
#[test]
fn office_fonts_have_substitutes() {
assert!(substitutes("Calibri").contains(&"Carlito"));
assert!(substitutes("Cambria").contains(&"Caladea"));
assert!(substitutes("Times New Roman").contains(&"Liberation Serif"));
assert!(substitutes("Aptos").contains(&"Inter"));
assert!(substitutes("zzz").is_empty());
}
#[test]
fn shaping_produces_clusters_and_advances() {
let face = any_face();
let g = shape(&face, "Hello", &[], |c| c);
assert_eq!(g.len(), 5);
assert_eq!(g.iter().map(|g| g.cluster).collect::<Vec<_>>(), vec![0, 1, 2, 3, 4]);
assert!(g.iter().all(|g| g.x_advance > 0 && g.gid != 0));
}
#[test]
fn mapping_keeps_clusters() {
let face = any_face();
let g = shape(&face, "ab", &[], |c| c.to_ascii_uppercase());
assert_eq!(g[0].gid, face.glyph_for('A'));
assert_eq!(g[1].cluster, 1);
}
#[test]
fn outlines_and_metrics() {
let db = FontDb::global();
let face = any_face();
let o = db.outline(&face, face.glyph_for('O'));
assert!(!o.elements().is_empty());
assert!(face.ascent > 0.0 && face.descent > 0.0);
assert!(feature("abc").is_none());
assert!(feature("liga").is_some());
}
/// When the Latin build input exists, the runtime catalogue carries the expected Latin faces;
/// required builds also assert their presence.
#[test]
fn craft_fonts_latin_when_present() {
let required = option_env!("CRAFT_FONTS_REQUIRED").is_some();
if !CRAFT_FONTS.iter().any(|f| f.family == "Inter") {
assert!(!required, "CRAFT_FONTS_REQUIRED must embed Inter rather than skip the optional Latin manifest");
eprintln!("skipped: built without craft-fonts' Latin manifest (set CRAFT_FONTS_DIR)");
return;
}
let inter = |style: &str| CRAFT_FONTS.iter().find(|f| f.family == "Inter" && f.style == style);
let (regular, bold, variable) = (inter("Regular"), inter("Bold"), inter("Variable"));
if required {
assert!(
variable.is_some() || (regular.is_some() && bold.is_some()),
"CRAFT_FONTS_REQUIRED must embed Inter Regular and Bold (or a Variable face)"
);
}
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
assert_eq!(db.face("Inter", "Regular").family, "Inter");
assert_eq!(db.face("Aptos", "Regular").family, "Inter");
if let (Some(regular), Some(bold)) = (regular, bold) {
// The build input carries one entry per face; real static Bold must not be the Regular
// file relabelled in the manifest.
assert_ne!(regular.bytes, bold.bytes, "Inter Bold must be a distinct face file from Regular");
let (r, b) = (db.face("Inter", "Regular"), db.face("Inter", "Bold"));
assert_eq!((r.family.as_str(), r.style.as_str()), ("Inter", "Regular"));
assert_eq!((b.family.as_str(), b.style.as_str()), ("Inter", "Bold"));
assert_ne!(r.id(), b.id(), "Inter Bold must load as its own face");
assert!(b.weight > r.weight, "Inter Bold weight {} is not above Regular {}", b.weight, r.weight);
}
if CRAFT_FONTS.iter().any(|f| f.family == "Carlito") {
assert_eq!(db.face("Calibri", "Bold").family, "Carlito");
}
}
/// An Embedded OpenType header of `version` around `font`, with the font at the end.
fn wrap_eot(font: &[u8], version: u32) -> Vec<u8> {
let mut h = vec![0u8; 82];
let total = 82 + font.len();
h[0..4].copy_from_slice(&(total as u32).to_le_bytes());
h[4..8].copy_from_slice(&(font.len() as u32).to_le_bytes());
h[8..12].copy_from_slice(&version.to_le_bytes());
h[34..36].copy_from_slice(&0x504Cu16.to_le_bytes());
h.extend_from_slice(font);
h
}
#[test]
fn embedded_open_type_yields_the_font() {
let font = epaint_default_fonts::UBUNTU_LIGHT;
assert_eq!(embedded_sfnt(font), Some(font));
assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0002_0001)), Some(font));
assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0001_0000)), Some(font));
assert!(embedded_sfnt(b"not a font file").is_none());
assert!(embedded_sfnt(&wrap_eot(b"hello world!!", 0x0002_0002)).is_none());
let mut truncated = wrap_eot(font, 0x0002_0001);
truncated.truncate(40);
assert!(embedded_sfnt(&truncated).is_none());
}
/// A FIFO, a folder and a link loop dressed up as font files: the system scan skips them
/// instead of blocking forever on the FIFO (start-up hung when one sat in `~/.fonts`), and
/// still finds the real font beside them.
#[cfg(unix)]
#[test]
fn the_font_scan_skips_fifos_folders_and_link_loops() {
let dir = std::env::temp_dir().join(format!("deckcraft-fontscan-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("sub")).unwrap();
std::fs::copy(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/legacy-semibold.ttf"), dir.join("sub/real.ttf")).unwrap();
let fifo = std::process::Command::new("mkfifo").arg(dir.join("trap.ttf")).status().is_ok_and(|s| s.success());
std::fs::create_dir(dir.join("folder.ttf")).unwrap();
std::fs::write(dir.join("empty.otf"), b"").unwrap();
std::os::unix::fs::symlink(&dir, dir.join("sub/loop")).unwrap();
let (tx, rx) = std::sync::mpsc::channel();
let scan_dir = dir.clone();
std::thread::spawn(move || {
let db = FontDb::with_font_dirs(vec![scan_dir]);
let _ = tx.send(db.load_system_fonts());
});
let n = rx.recv_timeout(std::time::Duration::from_secs(30)).expect("the font scan hung");
assert!(n >= 1, "the real font was found");
assert!(fifo || n >= 1);
let _ = std::fs::remove_dir_all(&dir);
}
}
mod tests;
+326
View File
@@ -0,0 +1,326 @@
use super::*;
fn any_face() -> std::sync::Arc<FontFace> {
FontDb::global().face(DEFAULT_FAMILY, "Regular")
}
#[test]
fn emoji_clusters_are_recognised() {
let c = |s: &str| s.chars().collect::<Vec<char>>();
// As PowerPoint draws them: emoji-presentation characters (⭐ ✅ ☔ ⌚ ⏩) in colour even
// without a selector, text-presentation ones (♥ ✔ ☎ ⬆ ▶) as text unless U+FE0F asks, and
// keycaps always as text.
for yes in ["😀", "👍🏽", "❤\u{FE0F}", "👨\u{200D}👩\u{200D}👧", "🇯🇵", "🏳\u{FE0F}\u{200D}🌈", "⭐", "✅", "☔", "⌚", "⏩", "🀄"]
{
assert!(is_emoji_cluster(&c(yes)), "{yes:?}");
}
for no in ["a", "1", "❤", "☺\u{FE0E}", "😀\u{FE0E}", "字", "→", "♥", "✔", "☎", "⬆", "▶", "1\u{FE0F}\u{20E3}", "#\u{20E3}"] {
assert!(!is_emoji_cluster(&c(no)), "{no:?}");
}
}
#[test]
fn colour_fonts_are_recognised() {
let db = FontDb::with_font_dirs(Vec::new());
assert!(db.add_font(include_bytes!("../../render/tests/fixtures/colr-squares.ttf").to_vec()) > 0);
assert!(db.face("DeckCraft Colr Squares", "Regular").is_color());
assert!(!fontdb::last_resort_face().is_color());
}
#[test]
fn last_resort_face_parses() {
let f = fontdb::last_resort_face();
assert_eq!(f.family, fontdb::LAST_RESORT_FAMILY);
assert!(f.upem > 0.0);
}
#[test]
fn line_split_follows_the_windows_ascent_and_descent() {
use skrifa::raw::TableProvider;
let f = fontdb::last_resort_face();
let font = skrifa::FontRef::new(f.data()).unwrap();
let os2 = font.os2().unwrap();
let (a, d) = (os2.us_win_ascent() as f64, os2.us_win_descent() as f64);
assert!((f.line_split - a / (a + d)).abs() < 1e-9, "{} vs {}", f.line_split, a / (a + d));
assert!(f.line_split > 0.5 && f.line_split < 1.0);
}
#[test]
fn some_face_always_resolves() {
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
let f = db.face("No Such Font Anywhere", "Regular");
assert!(!f.family.is_empty());
assert!(!shape(&f, "Hello", &[], |c| c).is_empty());
}
#[test]
fn filtered_fallback_continues_after_a_rejected_loaded_face() {
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
assert_eq!(db.add_font(include_bytes!("../tests/fixtures/legacy-regular.ttf").to_vec()), 1);
let first = db.fallback_for('A', u32::MAX).unwrap();
let accepted_family = if first.family == LAST_RESORT_FAMILY { "DeckCraft Legacy" } else { LAST_RESORT_FAMILY };
let fallback = db.fallback_for_filtered('A', u32::MAX, |f| f.family == accepted_family).unwrap();
assert_eq!(fallback.family, accepted_family);
assert!(fallback.covers('A'));
assert_ne!(fallback.id(), first.id());
assert_eq!(db.fallback_for('A', u32::MAX).unwrap().id(), first.id(), "document fallback order is unchanged");
assert!(db.fallback_for_filtered('A', fallback.id(), |f| f.family == accepted_family).is_none(), "the excluded face stays excluded");
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn filtered_fallback_loads_an_eligible_system_face_and_does_not_cache_policy_misses() {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let db = FontDb::with_font_dirs(vec![dir]);
let first = db.fallback_for('A', u32::MAX).unwrap();
assert_ne!(first.family, "DeckCraft Legacy");
assert!(db.fallback_for_filtered('A', u32::MAX, |_| false).is_none());
assert!(db.fallback_for('A', u32::MAX).is_some(), "an interface policy miss does not hide document glyphs");
let accepted = db.fallback_for_filtered('A', u32::MAX, |f| f.family == "DeckCraft Legacy").unwrap();
assert_eq!(accepted.family, "DeckCraft Legacy");
assert!(accepted.covers('A'));
}
#[test]
fn concurrent_lookups_all_find_an_installed_family() {
// Threads resolving an installed family nobody had loaded yet used to fall back to
// another face when a different thread finished loading it first, so the same text was
// laid out in different fonts depending on timing.
let dir = std::env::temp_dir().join(format!("deckcraft-fonts-concurrent-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("Hack-Regular.ttf"), epaint_default_fonts::HACK_REGULAR).unwrap();
for _ in 0..20 {
let db = FontDb::with_font_dirs(vec![dir.clone()]);
db.set_system_fallback(false);
let start = std::sync::Barrier::new(8);
let got: Vec<String> = std::thread::scope(|s| {
let threads: Vec<_> = (0..8)
.map(|_| {
s.spawn(|| {
start.wait();
db.face("Hack", "Regular").family.clone()
})
})
.collect();
threads.into_iter().map(|t| t.join().unwrap()).collect()
});
assert!(got.iter().all(|f| f == "Hack"), "{got:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn windows_family_names_find_their_faces() {
// Documents name fonts by their Windows family ("Segoe UI Semibold"); the font's
// typographic family is "Segoe UI" with a "Semibold" style. The fixture is named the same way.
let check = |db: &FontDb| {
assert!(db.has_family("DeckCraft Legacy Semibold"));
for style in ["Regular", "Bold"] {
let f = db.face("DeckCraft Legacy Semibold", style);
assert_eq!((f.family.as_str(), f.style.as_str()), ("DeckCraft Legacy", "Semibold"), "{style}");
}
assert_eq!(db.document_face("DeckCraft Legacy Semibold", "Regular").family, "DeckCraft Legacy", "installed, so not Calibri");
// The typographic name still works.
assert_eq!(db.face("DeckCraft Legacy", "Semibold").style, "Semibold");
};
// Loaded directly…
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
assert_eq!(db.add_font(include_bytes!("../tests/fixtures/legacy-semibold.ttf").to_vec()), 1);
check(&db);
// …and found by the system font scan under its Windows name, then loaded.
#[cfg(not(target_arch = "wasm32"))]
{
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let db = FontDb::with_font_dirs(vec![dir.clone()]);
db.set_system_fallback(false);
check(&db);
// Looking up the Windows name loads the Semibold file only; a later lookup of the
// family's Regular must still load and pick the Regular file, not settle for Semibold.
let db = FontDb::with_font_dirs(vec![dir]);
db.set_system_fallback(false);
assert_eq!(db.face("DeckCraft Legacy Semibold", "Regular").style, "Semibold");
let regular = db.face("DeckCraft Legacy", "Regular");
assert_eq!((regular.style.as_str(), regular.weight), ("Regular", 400.0));
}
}
#[test]
fn faces_without_outlines_are_not_cataloged() {
// Rename the Semibold's `glyf` table, as in fonts whose glyphs are bitmaps or are stored in
// Apple's `hvgl` table. The face still maps "A", but its outline is empty. The new tag
// keeps the table directory sorted.
let mut semibold = include_bytes!("../tests/fixtures/legacy-semibold.ttf").to_vec();
let tables = u16::from_be_bytes([semibold[4], semibold[5]]) as usize;
let glyf = (0..tables).map(|i| 12 + 16 * i).find(|&r| &semibold[r..r + 4] == b"glyf").unwrap();
semibold[glyf..glyf + 4].copy_from_slice(b"gxyz");
let loaded = FontDb::with_font_dirs(Vec::new());
assert_eq!(loaded.add_font(semibold.clone()), 1);
let face = loaded.face("DeckCraft Legacy", "Semibold");
assert!(face.covers('A'));
assert!(loaded.outline(&face, face.glyph_for('A')).elements().is_empty());
// The system font scan lists the Regular, which has outlines, and leaves out the Semibold
// under both of its names.
#[cfg(not(target_arch = "wasm32"))]
{
let dir = std::env::temp_dir().join(format!("deckcraft-fonts-outlines-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("legacy-semibold.ttf"), &semibold).unwrap();
std::fs::write(dir.join("legacy-regular.ttf"), include_bytes!("../tests/fixtures/legacy-regular.ttf")).unwrap();
let db = FontDb::with_font_dirs(vec![dir.clone()]);
db.set_system_fallback(false);
assert_eq!(db.styles("DeckCraft Legacy"), ["Regular"]);
assert!(!db.has_family("DeckCraft Legacy Semibold"));
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn missing_document_fonts_are_drawn_in_calibri() {
// PowerPoint draws a missing family in Calibri, whatever its panose or the theme says.
let db = FontDb::global();
for missing in ["Qqq Nonexistent", "Avenir Next Qqq", "", "Arimo Qqq"] {
let f = db.document_face(missing, "Bold");
assert_eq!(f.family, db.face(MISSING_FONT_FAMILY, "Bold").family, "{missing:?}");
}
// PowerPoint's own twins and Windows' substitutes, when the font itself is missing.
for (missing, twin) in
[("Liberation Sans", "Arial"), ("Liberation Serif", "Times New Roman"), ("Arial CE", "Arial"), ("Times New Roman CYR", "Times New Roman")]
{
if !db.has_family(missing) {
assert_eq!(db.document_face(missing, "Regular").family, db.face(twin, "Regular").family, "{missing}");
}
}
// Metric-compatible stand-ins and DeckCraft's default keep their own fallbacks.
// So do Office's cloud fonts, which PowerPoint downloads and draws as themselves.
for kept in ["Arial", "Times New Roman", "Cambria", "Courier New", FALLBACK_FAMILY, "Aptos", "Aptos Display", "Grandview"] {
assert_eq!(db.document_face(kept, "Regular").family, db.face(kept, "Regular").family, "{kept}");
}
}
#[test]
fn office_fonts_have_substitutes() {
assert!(substitutes("Calibri").contains(&"Carlito"));
assert!(substitutes("Cambria").contains(&"Caladea"));
assert!(substitutes("Times New Roman").contains(&"Liberation Serif"));
assert!(substitutes("Aptos").contains(&"Inter"));
assert!(substitutes("zzz").is_empty());
}
#[test]
fn shaping_produces_clusters_and_advances() {
let face = any_face();
let g = shape(&face, "Hello", &[], |c| c);
assert_eq!(g.len(), 5);
assert_eq!(g.iter().map(|g| g.cluster).collect::<Vec<_>>(), vec![0, 1, 2, 3, 4]);
assert!(g.iter().all(|g| g.x_advance > 0 && g.gid != 0));
}
#[test]
fn mapping_keeps_clusters() {
let face = any_face();
let g = shape(&face, "ab", &[], |c| c.to_ascii_uppercase());
assert_eq!(g[0].gid, face.glyph_for('A'));
assert_eq!(g[1].cluster, 1);
}
#[test]
fn outlines_and_metrics() {
let db = FontDb::global();
let face = any_face();
let o = db.outline(&face, face.glyph_for('O'));
assert!(!o.elements().is_empty());
assert!(face.ascent > 0.0 && face.descent > 0.0);
assert!(feature("abc").is_none());
assert!(feature("liga").is_some());
}
/// When the Latin build input exists, the runtime catalogue carries the expected Latin faces;
/// required builds also assert their presence.
#[test]
fn craft_fonts_latin_when_present() {
let required = option_env!("CRAFT_FONTS_REQUIRED").is_some();
if !CRAFT_FONTS.iter().any(|f| f.family == "Inter") {
assert!(!required, "CRAFT_FONTS_REQUIRED must embed Inter rather than skip the optional Latin manifest");
eprintln!("skipped: built without craft-fonts' Latin manifest (set CRAFT_FONTS_DIR)");
return;
}
let inter = |style: &str| CRAFT_FONTS.iter().find(|f| f.family == "Inter" && f.style == style);
let (regular, bold, variable) = (inter("Regular"), inter("Bold"), inter("Variable"));
if required {
assert!(
variable.is_some() || (regular.is_some() && bold.is_some()),
"CRAFT_FONTS_REQUIRED must embed Inter Regular and Bold (or a Variable face)"
);
}
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
assert_eq!(db.face("Inter", "Regular").family, "Inter");
assert_eq!(db.face("Aptos", "Regular").family, "Inter");
if let (Some(regular), Some(bold)) = (regular, bold) {
// The build input carries one entry per face; real static Bold must not be the Regular
// file relabelled in the manifest.
assert_ne!(regular.bytes, bold.bytes, "Inter Bold must be a distinct face file from Regular");
let (r, b) = (db.face("Inter", "Regular"), db.face("Inter", "Bold"));
assert_eq!((r.family.as_str(), r.style.as_str()), ("Inter", "Regular"));
assert_eq!((b.family.as_str(), b.style.as_str()), ("Inter", "Bold"));
assert_ne!(r.id(), b.id(), "Inter Bold must load as its own face");
assert!(b.weight > r.weight, "Inter Bold weight {} is not above Regular {}", b.weight, r.weight);
}
if CRAFT_FONTS.iter().any(|f| f.family == "Carlito") {
assert_eq!(db.face("Calibri", "Bold").family, "Carlito");
}
}
/// An Embedded OpenType header of `version` around `font`, with the font at the end.
fn wrap_eot(font: &[u8], version: u32) -> Vec<u8> {
let mut h = vec![0u8; 82];
let total = 82 + font.len();
h[0..4].copy_from_slice(&(total as u32).to_le_bytes());
h[4..8].copy_from_slice(&(font.len() as u32).to_le_bytes());
h[8..12].copy_from_slice(&version.to_le_bytes());
h[34..36].copy_from_slice(&0x504Cu16.to_le_bytes());
h.extend_from_slice(font);
h
}
#[test]
fn embedded_open_type_yields_the_font() {
let font = epaint_default_fonts::UBUNTU_LIGHT;
assert_eq!(embedded_sfnt(font), Some(font));
assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0002_0001)), Some(font));
assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0001_0000)), Some(font));
assert!(embedded_sfnt(b"not a font file").is_none());
assert!(embedded_sfnt(&wrap_eot(b"hello world!!", 0x0002_0002)).is_none());
let mut truncated = wrap_eot(font, 0x0002_0001);
truncated.truncate(40);
assert!(embedded_sfnt(&truncated).is_none());
}
/// A FIFO, a folder and a link loop dressed up as font files: the system scan skips them
/// instead of blocking forever on the FIFO (start-up hung when one sat in `~/.fonts`), and
/// still finds the real font beside them.
#[cfg(unix)]
#[test]
fn the_font_scan_skips_fifos_folders_and_link_loops() {
let dir = std::env::temp_dir().join(format!("deckcraft-fontscan-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("sub")).unwrap();
std::fs::copy(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/legacy-semibold.ttf"), dir.join("sub/real.ttf")).unwrap();
let fifo = std::process::Command::new("mkfifo").arg(dir.join("trap.ttf")).status().is_ok_and(|s| s.success());
std::fs::create_dir(dir.join("folder.ttf")).unwrap();
std::fs::write(dir.join("empty.otf"), b"").unwrap();
std::os::unix::fs::symlink(&dir, dir.join("sub/loop")).unwrap();
let (tx, rx) = std::sync::mpsc::channel();
let scan_dir = dir.clone();
std::thread::spawn(move || {
let db = FontDb::with_font_dirs(vec![scan_dir]);
let _ = tx.send(db.load_system_fonts());
});
let n = rx.recv_timeout(std::time::Duration::from_secs(30)).expect("the font scan hung");
assert!(n >= 1, "the real font was found");
assert!(fifo || n >= 1);
let _ = std::fs::remove_dir_all(&dir);
}