refactor(fonts): move fontdb tests to fontdb/tests.rs

This commit is contained in:
aiongg
2026-10-26 20:54:02 +00:00
parent aedf03549f
commit c5b1efb97d
2 changed files with 376 additions and 378 deletions
+1 -378
View File
@@ -2063,381 +2063,4 @@ impl OutlinePen for FlipPen {
mod tests_sysfonts;
#[cfg(test)]
mod tests {
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn occasional_slow_hinted_glyphs_do_not_turn_hinting_off() {
let off = |pattern: &[bool]| {
let face =
super::make_face(super::FontBytes::Static(super::BUNDLED[0]), 0, "T".into(), "Regular".into(), "T".into(), Vec::new()).unwrap();
pattern.iter().position(|&slow| face.count_hinted(slow))
};
// A face slow throughout stops at the third strike.
assert_eq!(off(&[true; 5]), Some(2));
// Three draws slowed by a busy machine among many: hinting stays on.
let mut busy = vec![false; 100];
busy.extend([true; 3]);
assert_eq!(off(&busy), None);
// Slow glyphs spread among fast ones still stop it once they are one in 16.
let spread: Vec<bool> = (0..64).map(|i| i % 8 == 7).collect();
assert_eq!(off(&spread), Some(23));
}
#[test]
#[ignore = "requires a locally licensed kaiu.ttf; set WORDCRAFT_TEST_KAI_FONT"]
fn kai_contours_include_complete_character_body() {
use kurbo::Shape;
let path = std::env::var_os("WORDCRAFT_TEST_KAI_FONT").expect("set WORDCRAFT_TEST_KAI_FONT");
let db = super::FontDb::with_font_dirs(vec![]);
db.set_system_fallback(false);
assert!(db.add_font(std::fs::read(path).unwrap()) > 0);
let face = db.face("DFKai-SB", "Regular");
assert_eq!(face.family, "DFKai-SB");
for ch in ['中', '文', '契', '約'] {
let outline = face.glyph_outline(face.glyph_for(ch));
let bounds = outline.bounding_box();
eprintln!("{ch}: {bounds:?}, upem={}", face.upem);
assert!(bounds.width() > face.upem * 0.5, "{ch}: missing horizontal strokes");
assert!(bounds.height() > face.upem * 0.5, "{ch}: missing vertical strokes");
assert!(std::sync::Arc::ptr_eq(&db.outline(&face, face.glyph_for(ch)), &db.outline(&face, face.glyph_for(ch))));
}
}
use super::*;
#[test]
#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
fn system_font_dirs_include_the_flatpak_host_folders() {
// Inside a Flatpak, /usr/share/fonts is the runtime's: the host's fonts are under /run/host (#1937).
let dirs = system_font_dirs();
for d in ["/usr/share/fonts", "/run/host/fonts", "/run/host/local-fonts", "/run/host/user-fonts", "/run/host/user-fonts-legacy"] {
assert!(dirs.contains(&std::path::PathBuf::from(d)), "{d} missing from {dirs:?}");
}
}
#[test]
fn a_capped_outline_is_refused_while_it_is_drawn() {
// #3716 review: a colour glyph's clip outline stops growing at the payload left.
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
let face = db.face(FALLBACK_FAMILY, "Regular");
let gid = face.glyph_for('g');
let whole = face.glyph_outline(gid);
let n = whole.elements().len();
assert!(n > 4, "{n}");
assert_eq!(face.glyph_outline_capped(gid, n).as_ref().map(BezPath::elements), Some(whole.elements()));
assert!(face.glyph_outline_capped(gid, n - 1).is_none());
assert!(face.glyph_outline_capped(gid, 0).is_none());
}
#[test]
fn run_fallback_is_cached_per_char_set() {
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
let latin = db.face(FALLBACK_FAMILY, "Regular");
let a = db.fallback_for_run(&['分', '类', '分'], latin.id(), false, false).map(|f| f.id());
// The same set in another order (and without the repeat) is the same entry.
assert_eq!(db.fallback_for_run(&['类', '分'], latin.id(), false, false).map(|f| f.id()), a);
{
let cache = db.run_fallbacks.read().unwrap();
assert_eq!(cache.faces, db.read_faces().len());
assert_eq!(cache.map.len(), 1);
assert!(cache.map.contains_key(&(vec!['分', '类'], latin.id(), false, false)));
}
// Long hostile runs keep the cache bounded.
let many: Vec<char> = (0x4E00u32..0x4E00 + 50_000).filter_map(char::from_u32).collect();
for i in 0..30 {
let _ = db.fallback_for_run(many.get(i..).unwrap_or(&[]), latin.id(), false, false);
}
let cache = db.run_fallbacks.read().unwrap();
assert!(cache.chars <= RUN_FALLBACK_CACHE_CHARS, "{}", cache.chars);
// Turning the system fallback on or off forgets remembered misses.
drop(cache);
db.set_system_fallback(false);
assert!(db.run_fallbacks.read().unwrap().map.is_empty());
}
/// The issue's run (#1549): Thai letters and digits, Latin letters, digits and the baht sign.
const THAI_RUN: &str = "[D] น้ำ ค่ำ ปั้น ฝั่ง ปู่ ๑๒๓๔๕๖๗๘๙๐ ฿1,250.50";
#[test]
fn a_thai_run_with_latin_falls_back_to_one_face() {
let db = FontDb::global();
if db.fallback_for('น', 0).is_none() {
eprintln!("skipped: no Thai font installed");
return;
}
let latin = db.face(FALLBACK_FAMILY, "Regular");
let chars: Vec<char> = THAI_RUN.chars().filter(|c| !c.is_whitespace()).collect();
let f = db.fallback_for_run(&chars, latin.id(), false, false).expect("one face covers the run");
assert_ne!(f.id, latin.id);
for c in &chars {
assert!(f.covers(*c), "{} covers {c}", f.family);
}
// A Thai font with OpenType tables, as the per-character fallback prefers.
assert!(f.has_layout_tables(), "{} shapes Thai", f.family);
// Without system fonts and without craft-fonts' Thai face (built in since craft-fonts
// 3d9ab3a1) no bundled face covers Thai: the caller keeps the per-character path.
let bundled = FontDb::with_font_dirs(Vec::new());
bundled.set_system_fallback(false);
let latin = bundled.face(FALLBACK_FAMILY, "Regular");
// Expected from the embedded font list, not from the database under test.
let thai_bundled = crate::CRAFT_FONTS.iter().any(|f| f.scripts.contains(&"Thai"));
assert_eq!(bundled.fallback_for('น', 0).is_some(), thai_bundled);
assert_eq!(bundled.fallback_for_run(&chars, latin.id(), false, false).is_some(), thai_bundled);
}
/// With only bundled Latin faces and no system fallback, a Thai run has no single covering face,
/// so run-level fallback declines. This test asserts that `fallback_for_run` returns `None`; the
/// caller's per-character path is not exercised here.
#[test]
fn a_run_without_a_covering_face_keeps_the_per_character_path() {
// Use only this repository's Latin faces. Optional craft-fonts inputs can contain a
// complete Thai face even when system font loading is disabled.
let bundled = FontDb::with_font_dirs(Vec::new());
bundled.faces.write().unwrap().retain(|face| BUNDLED.iter().any(|bytes| std::ptr::eq(face.data(), *bytes)));
bundled.set_system_fallback(false);
let latin = bundled.face(FALLBACK_FAMILY, "Regular");
let chars: Vec<char> = THAI_RUN.chars().filter(|c| !c.is_whitespace()).collect();
assert!(!bundled.faces.read().unwrap().is_empty(), "the negative fixture retains real Latin faces");
assert!(bundled.faces.read().unwrap().iter().all(|face| !face.covers('น')));
assert!(bundled.fallback_for_run(&chars, latin.id(), false, false).is_none());
}
#[test]
fn a_family_named_for_a_heavy_weight_is_that_heavy() {
let face = |bytes: &'static [u8], family: &str| {
make_face(FontBytes::Static(bytes), 0, family.into(), "Regular".into(), family.into(), Vec::new()).unwrap()
};
// The file says 400, as the core-fonts Arial Black does: bold must not embolden it again.
assert_eq!(face(BUNDLED[0], "Arial Black").weight, 900.0);
assert_eq!(face(BUNDLED[0], "Franklin Gothic Heavy ").weight, 900.0);
assert_eq!(face(BUNDLED[0], "Segoe UI ExtraBold").weight, 800.0);
assert_eq!(face(BUNDLED[0], "Blackadder ITC").weight, 400.0);
assert_eq!(face(BUNDLED[0], "Arial").weight, 400.0);
// A file that already says it is heavy keeps its own weight (Lato Heavy is 800, not 900).
assert_eq!(face(BUNDLED[1], "Source Sans 3 Heavy").weight, 600.0);
}
#[test]
fn legacy_family_keeps_the_style_words_that_are_not_regular_bold_or_italic() {
for (family, style, legacy) in [
("Roboto", "Regular", "Roboto"),
("Roboto", "Bold Italic", "Roboto"),
("Roboto", "italic", "Roboto"),
("Roboto", "Light", "Roboto Light"),
("Roboto", "Light Italic", "Roboto Light"),
("Roboto", "Condensed Bold", "Roboto Condensed"),
("Inter", "SemiBold", "Inter SemiBold"),
("Inter", "ExtraBold Italic", "Inter ExtraBold"),
("Inter", "Medium {wght:520}", "Inter Medium"),
("Inter", "", "Inter"),
] {
assert_eq!(legacy_family(family, style), legacy, "{family} {style}");
}
}
#[test]
fn faces_are_found_by_their_legacy_family_name() {
// Bundled static fonts whose name ID 1 differs from the typographic family (#219).
let db = FontDb::with_font_dirs(vec![]);
let families = db.families();
for name in ["Inter", "Inter Medium", "Inter SemiBold", "Source Sans 3 Semibold", "Source Serif 4 Semibold"] {
assert!(families.iter().any(|f| f == name), "{name} in {families:?}");
assert!(db.has_family(name));
}
let semibold = db.face("Source Serif 4 Semibold", "Regular");
assert_eq!((semibold.family.as_str(), semibold.style.as_str()), ("Source Serif 4", "Semibold"));
assert_eq!(db.face("inter medium", "Regular").style, "Medium");
assert_eq!(db.face("Inter SemiBold", "Regular").style, "SemiBold");
// Bold and italic choose within the faces the legacy name covers; the family's own Bold
// isn't one of them.
assert_eq!(db.face("Source Serif 4 Semibold", "Bold").style, "Semibold");
assert_eq!(db.face("Source Serif 4 Semibold", "Italic").style, "Semibold");
assert_eq!(db.styles("Source Sans 3 Semibold"), ["Semibold"]);
// The typographic family is unchanged.
assert_eq!(db.face("Source Serif 4", "Bold").style, "Bold");
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
assert_eq!(db.face("Source Serif 4", "Regular").legacy_family, "Source Serif 4");
// Not a name any face has.
assert!(!db.has_family("Source Serif 4 Light"));
assert_eq!(db.face("Source Serif 4 Light", "Regular").family, FALLBACK_FAMILY);
}
#[test]
fn a_legacy_named_instance_with_axis_settings_is_made_once() {
let db = FontDb::with_font_dirs(vec![]);
let a = db.face("Inter Medium", "Medium {wght:520}");
let b = db.face("Inter Medium", "Medium {wght:520}");
assert_eq!(a.id(), b.id());
assert_eq!(a.legacy_family, "Inter Medium");
}
#[test]
fn full_names_select_exact_faces() {
let db = FontDb::with_font_dirs(vec![]);
for style in ["Regular", "Bold", "Italic", "Bold Italic"] {
let expected = db.face("Source Serif 4", style);
let f = expected.skrifa().unwrap();
let full = name(&f, &[StringId::FULL_NAME]).unwrap();
assert!(db.has_family(&full), "{full}");
assert_eq!(db.face(&full, "Regular").id(), expected.id());
let bold = if full == expected.family { db.face(&full, "Bold") } else { expected.clone() };
assert_eq!(db.face(&full.to_ascii_lowercase(), "Bold").id(), bold.id());
}
assert!(!db.has_family("NotAFont Bold"));
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
}
#[test]
fn postscript_names_select_faces() {
let db = FontDb::with_font_dirs(vec![]);
for style in ["Regular", "Bold", "Italic", "Bold Italic"] {
let expected = db.face("Source Serif 4", style);
let f = expected.skrifa().unwrap();
let ps = name(&f, &[StringId::POSTSCRIPT_NAME]).unwrap();
assert!(db.has_family(&ps), "{ps}");
let actual = db.face(&ps, "Regular");
assert_eq!(actual.id(), expected.id(), "{ps}");
assert_eq!(db.face(&ps.to_ascii_lowercase(), "Bold").id(), expected.id());
}
assert!(!db.has_family("NotAFont-Bold"));
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
}
/// A variable font holding just `name` and `fvar` tables: family "Var Test", one `wght` axis
/// and `instances` named instances, each with its own style name.
fn variable_font(instances: u16) -> Vec<u8> {
let mut names: Vec<(u16, String)> = vec![(1, "Var Test".into()), (2, "Regular".into())];
names.extend((0..instances).map(|i| (256 + i, format!("Style {i}"))));
let mut name = vec![0, 0];
name.extend_from_slice(&(names.len() as u16).to_be_bytes());
name.extend_from_slice(&(6 + 12 * names.len() as u16).to_be_bytes());
let mut strings = Vec::new();
for (id, text) in &names {
let utf16: Vec<u8> = text.encode_utf16().flat_map(u16::to_be_bytes).collect();
for v in [3, 1, 0x409, *id, utf16.len() as u16, strings.len() as u16] {
name.extend_from_slice(&v.to_be_bytes());
}
strings.extend(utf16);
}
name.extend(strings);
let mut fvar = Vec::new();
for v in [1u16, 0, 16, 2, 1, 20, instances, 8] {
fvar.extend_from_slice(&v.to_be_bytes());
}
fvar.extend_from_slice(b"wght");
for v in [100u32 << 16, 400 << 16, 900 << 16] {
fvar.extend_from_slice(&v.to_be_bytes());
}
fvar.extend_from_slice(&[0, 0, 0, 2]); // flags, axisNameId
for i in 0..instances {
fvar.extend_from_slice(&(256 + i).to_be_bytes());
fvar.extend_from_slice(&[0, 0]);
fvar.extend_from_slice(&(400u32 << 16).to_be_bytes());
}
mini_font(&[(*b"fvar", fvar), (*b"name", name)])
}
/// A collection listing `font` `members` times.
fn collection_of(font: &[u8], members: u32) -> Vec<u8> {
let start = 12 + 4 * members;
let mut out = b"ttcf\0\x01\0\0".to_vec();
out.extend_from_slice(&members.to_be_bytes());
for _ in 0..members {
out.extend_from_slice(&start.to_be_bytes());
}
let mut font = font.to_vec();
let tables = u16::from_be_bytes([font[4], font[5]]) as usize;
for i in 0..tables {
let at = 12 + 16 * i + 8;
let offset = u32::from_be_bytes(font[at..at + 4].try_into().unwrap()) + start;
font[at..at + 4].copy_from_slice(&offset.to_be_bytes());
}
out.extend(font);
out
}
/// JetBrains Mono renamed "MingLiU JBMono" (a name the interpreter is required for), with its
/// `maxp` twilight-zone size set to `twilight`.
fn hint_reliant_font(twilight: u16) -> Vec<u8> {
let mut data = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/JetBrainsMono-Regular.ttf")).unwrap();
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
for (from, to) in [(utf16("JetBrains Mono"), utf16("MingLiU JBMono")), (b"JetBrains Mono".to_vec(), b"MingLiU JBMono".to_vec())] {
while let Some(at) = data.windows(from.len()).position(|w| w == from) {
data[at..at + to.len()].copy_from_slice(&to);
}
}
let tables = u16::from_be_bytes([data[4], data[5]]) as usize;
let entry = (0..tables).map(|i| 12 + 16 * i).find(|e| &data[*e..e + 4] == b"maxp").unwrap();
let maxp = u32::from_be_bytes(data[entry + 8..entry + 12].try_into().unwrap()) as usize;
data[maxp + 16..maxp + 18].copy_from_slice(&twilight.to_be_bytes());
data
}
#[test]
fn a_font_asking_for_huge_hinting_zones_is_drawn_unhinted() {
// #3692: skrifa's budget counts instructions, not the points each one moves.
let db = FontDb::with_font_dirs(vec![]);
db.add_font(hint_reliant_font(660));
let face = db.face("MingLiU JBMono", "Regular");
assert_eq!(face.family, "MingLiU JBMono");
assert!(face.hinter().is_some(), "a real font's zones are hinted");
assert!(!face.glyph_outline(face.glyph_for('a')).elements().is_empty());
let db = FontDb::with_font_dirs(vec![]);
db.add_font(hint_reliant_font(u16::MAX));
let face = db.face("MingLiU JBMono", "Regular");
assert!(face.hinter().is_none(), "a huge twilight zone is not hinted");
assert!(!face.glyph_outline(face.glyph_for('a')).elements().is_empty(), "drawn unhinted");
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn the_draw_that_turns_hinting_off_is_redrawn_unhinted_for_the_outline_cache() {
// #3692: the glyph whose slow hinted draw trips the breaker may be misassembled; it must not be
// what the outline cache keeps.
let data = hint_reliant_font(660);
let mut face = super::make_face(
super::FontBytes::Owned(std::sync::Arc::new(data)),
0,
"MingLiU JBMono".into(),
"Regular".into(),
"MingLiU JBMono".into(),
Vec::new(),
)
.unwrap();
assert!(face.hinter().is_some());
face.hint_strike = std::time::Duration::ZERO; // every hinted draw is slow
let gid = face.glyph_for('a');
assert!(face.draw_outline(gid, usize::MAX).hinted, "hinted while the breaker is closed");
assert!(face.draw_outline(gid, usize::MAX).hinted);
let tripping = face.draw_outline(gid, usize::MAX);
assert!(face.hinting_off.load(std::sync::atomic::Ordering::Relaxed), "the third slow draw trips the breaker");
assert!(!tripping.hinted, "the tripping draw is returned unhinted");
assert!(!tripping.path.elements().is_empty());
// And what the cache keeps is that outline.
let db = FontDb::with_font_dirs(vec![]);
assert_eq!(db.outline(&face, gid).elements(), tripping.path.elements());
}
#[test]
fn named_instances_and_collection_members_are_capped() {
// #3716: a file's faces are capped before they are built, however the file multiplies them.
let font = variable_font(300);
let faces = font_faces(&skrifa::FontRef::new(&font).unwrap(), 0);
assert_eq!(faces.len(), MAX_FILE_FACES + 1, "one more than the cap, so a caller can tell");
assert_eq!(faces[0].style, "Style 0");
assert_eq!(enumerate_faces(&font, MAX_FILE_FACES).len(), MAX_FILE_FACES);
// A million members, all the same font: read once, and no more than the cap.
let started = std::time::Instant::now();
let ttc = collection_of(BUNDLED[0], 1_000_000);
let found = enumerate_faces(&ttc, MAX_FILE_FACES);
assert_eq!(found.iter().map(|f| (f.family.as_str(), f.style.as_str())).collect::<Vec<_>>(), [(FALLBACK_FAMILY, "Regular")]);
let db = FontDb::with_font_dirs(vec![]);
assert_eq!(db.add_font(collection_of(&font, 1_000_000)), MAX_FILE_FACES);
// A font the user adds is refused instead.
assert!(matches!(db.install_font("v.ttf", &variable_font(300)), Err(user::UserFontError::Unusable(e)) if e.contains("more than 256")));
// The face counts above are what tell the old code (a face per member) apart; the time
// (under 0.1 s on a loaded machine) only backs up "read once", with room for a busy CI.
assert!(started.elapsed() < std::time::Duration::from_secs(10), "{:?}", started.elapsed());
}
}
mod tests;
+375
View File
@@ -0,0 +1,375 @@
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn occasional_slow_hinted_glyphs_do_not_turn_hinting_off() {
let off = |pattern: &[bool]| {
let face = super::make_face(super::FontBytes::Static(super::BUNDLED[0]), 0, "T".into(), "Regular".into(), "T".into(), Vec::new()).unwrap();
pattern.iter().position(|&slow| face.count_hinted(slow))
};
// A face slow throughout stops at the third strike.
assert_eq!(off(&[true; 5]), Some(2));
// Three draws slowed by a busy machine among many: hinting stays on.
let mut busy = vec![false; 100];
busy.extend([true; 3]);
assert_eq!(off(&busy), None);
// Slow glyphs spread among fast ones still stop it once they are one in 16.
let spread: Vec<bool> = (0..64).map(|i| i % 8 == 7).collect();
assert_eq!(off(&spread), Some(23));
}
#[test]
#[ignore = "requires a locally licensed kaiu.ttf; set WORDCRAFT_TEST_KAI_FONT"]
fn kai_contours_include_complete_character_body() {
use kurbo::Shape;
let path = std::env::var_os("WORDCRAFT_TEST_KAI_FONT").expect("set WORDCRAFT_TEST_KAI_FONT");
let db = super::FontDb::with_font_dirs(vec![]);
db.set_system_fallback(false);
assert!(db.add_font(std::fs::read(path).unwrap()) > 0);
let face = db.face("DFKai-SB", "Regular");
assert_eq!(face.family, "DFKai-SB");
for ch in ['中', '文', '契', '約'] {
let outline = face.glyph_outline(face.glyph_for(ch));
let bounds = outline.bounding_box();
eprintln!("{ch}: {bounds:?}, upem={}", face.upem);
assert!(bounds.width() > face.upem * 0.5, "{ch}: missing horizontal strokes");
assert!(bounds.height() > face.upem * 0.5, "{ch}: missing vertical strokes");
assert!(std::sync::Arc::ptr_eq(&db.outline(&face, face.glyph_for(ch)), &db.outline(&face, face.glyph_for(ch))));
}
}
use super::*;
#[test]
#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
fn system_font_dirs_include_the_flatpak_host_folders() {
// Inside a Flatpak, /usr/share/fonts is the runtime's: the host's fonts are under /run/host (#1937).
let dirs = system_font_dirs();
for d in ["/usr/share/fonts", "/run/host/fonts", "/run/host/local-fonts", "/run/host/user-fonts", "/run/host/user-fonts-legacy"] {
assert!(dirs.contains(&std::path::PathBuf::from(d)), "{d} missing from {dirs:?}");
}
}
#[test]
fn a_capped_outline_is_refused_while_it_is_drawn() {
// #3716 review: a colour glyph's clip outline stops growing at the payload left.
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
let face = db.face(FALLBACK_FAMILY, "Regular");
let gid = face.glyph_for('g');
let whole = face.glyph_outline(gid);
let n = whole.elements().len();
assert!(n > 4, "{n}");
assert_eq!(face.glyph_outline_capped(gid, n).as_ref().map(BezPath::elements), Some(whole.elements()));
assert!(face.glyph_outline_capped(gid, n - 1).is_none());
assert!(face.glyph_outline_capped(gid, 0).is_none());
}
#[test]
fn run_fallback_is_cached_per_char_set() {
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
let latin = db.face(FALLBACK_FAMILY, "Regular");
let a = db.fallback_for_run(&['分', '类', '分'], latin.id(), false, false).map(|f| f.id());
// The same set in another order (and without the repeat) is the same entry.
assert_eq!(db.fallback_for_run(&['类', '分'], latin.id(), false, false).map(|f| f.id()), a);
{
let cache = db.run_fallbacks.read().unwrap();
assert_eq!(cache.faces, db.read_faces().len());
assert_eq!(cache.map.len(), 1);
assert!(cache.map.contains_key(&(vec!['分', '类'], latin.id(), false, false)));
}
// Long hostile runs keep the cache bounded.
let many: Vec<char> = (0x4E00u32..0x4E00 + 50_000).filter_map(char::from_u32).collect();
for i in 0..30 {
let _ = db.fallback_for_run(many.get(i..).unwrap_or(&[]), latin.id(), false, false);
}
let cache = db.run_fallbacks.read().unwrap();
assert!(cache.chars <= RUN_FALLBACK_CACHE_CHARS, "{}", cache.chars);
// Turning the system fallback on or off forgets remembered misses.
drop(cache);
db.set_system_fallback(false);
assert!(db.run_fallbacks.read().unwrap().map.is_empty());
}
/// The issue's run (#1549): Thai letters and digits, Latin letters, digits and the baht sign.
const THAI_RUN: &str = "[D] น้ำ ค่ำ ปั้น ฝั่ง ปู่ ๑๒๓๔๕๖๗๘๙๐ ฿1,250.50";
#[test]
fn a_thai_run_with_latin_falls_back_to_one_face() {
let db = FontDb::global();
if db.fallback_for('น', 0).is_none() {
eprintln!("skipped: no Thai font installed");
return;
}
let latin = db.face(FALLBACK_FAMILY, "Regular");
let chars: Vec<char> = THAI_RUN.chars().filter(|c| !c.is_whitespace()).collect();
let f = db.fallback_for_run(&chars, latin.id(), false, false).expect("one face covers the run");
assert_ne!(f.id, latin.id);
for c in &chars {
assert!(f.covers(*c), "{} covers {c}", f.family);
}
// A Thai font with OpenType tables, as the per-character fallback prefers.
assert!(f.has_layout_tables(), "{} shapes Thai", f.family);
// Without system fonts and without craft-fonts' Thai face (built in since craft-fonts
// 3d9ab3a1) no bundled face covers Thai: the caller keeps the per-character path.
let bundled = FontDb::with_font_dirs(Vec::new());
bundled.set_system_fallback(false);
let latin = bundled.face(FALLBACK_FAMILY, "Regular");
// Expected from the embedded font list, not from the database under test.
let thai_bundled = crate::CRAFT_FONTS.iter().any(|f| f.scripts.contains(&"Thai"));
assert_eq!(bundled.fallback_for('น', 0).is_some(), thai_bundled);
assert_eq!(bundled.fallback_for_run(&chars, latin.id(), false, false).is_some(), thai_bundled);
}
/// With only bundled Latin faces and no system fallback, a Thai run has no single covering face,
/// so run-level fallback declines. This test asserts that `fallback_for_run` returns `None`; the
/// caller's per-character path is not exercised here.
#[test]
fn a_run_without_a_covering_face_keeps_the_per_character_path() {
// Use only this repository's Latin faces. Optional craft-fonts inputs can contain a
// complete Thai face even when system font loading is disabled.
let bundled = FontDb::with_font_dirs(Vec::new());
bundled.faces.write().unwrap().retain(|face| BUNDLED.iter().any(|bytes| std::ptr::eq(face.data(), *bytes)));
bundled.set_system_fallback(false);
let latin = bundled.face(FALLBACK_FAMILY, "Regular");
let chars: Vec<char> = THAI_RUN.chars().filter(|c| !c.is_whitespace()).collect();
assert!(!bundled.faces.read().unwrap().is_empty(), "the negative fixture retains real Latin faces");
assert!(bundled.faces.read().unwrap().iter().all(|face| !face.covers('น')));
assert!(bundled.fallback_for_run(&chars, latin.id(), false, false).is_none());
}
#[test]
fn a_family_named_for_a_heavy_weight_is_that_heavy() {
let face = |bytes: &'static [u8], family: &str| {
make_face(FontBytes::Static(bytes), 0, family.into(), "Regular".into(), family.into(), Vec::new()).unwrap()
};
// The file says 400, as the core-fonts Arial Black does: bold must not embolden it again.
assert_eq!(face(BUNDLED[0], "Arial Black").weight, 900.0);
assert_eq!(face(BUNDLED[0], "Franklin Gothic Heavy ").weight, 900.0);
assert_eq!(face(BUNDLED[0], "Segoe UI ExtraBold").weight, 800.0);
assert_eq!(face(BUNDLED[0], "Blackadder ITC").weight, 400.0);
assert_eq!(face(BUNDLED[0], "Arial").weight, 400.0);
// A file that already says it is heavy keeps its own weight (Lato Heavy is 800, not 900).
assert_eq!(face(BUNDLED[1], "Source Sans 3 Heavy").weight, 600.0);
}
#[test]
fn legacy_family_keeps_the_style_words_that_are_not_regular_bold_or_italic() {
for (family, style, legacy) in [
("Roboto", "Regular", "Roboto"),
("Roboto", "Bold Italic", "Roboto"),
("Roboto", "italic", "Roboto"),
("Roboto", "Light", "Roboto Light"),
("Roboto", "Light Italic", "Roboto Light"),
("Roboto", "Condensed Bold", "Roboto Condensed"),
("Inter", "SemiBold", "Inter SemiBold"),
("Inter", "ExtraBold Italic", "Inter ExtraBold"),
("Inter", "Medium {wght:520}", "Inter Medium"),
("Inter", "", "Inter"),
] {
assert_eq!(legacy_family(family, style), legacy, "{family} {style}");
}
}
#[test]
fn faces_are_found_by_their_legacy_family_name() {
// Bundled static fonts whose name ID 1 differs from the typographic family (#219).
let db = FontDb::with_font_dirs(vec![]);
let families = db.families();
for name in ["Inter", "Inter Medium", "Inter SemiBold", "Source Sans 3 Semibold", "Source Serif 4 Semibold"] {
assert!(families.iter().any(|f| f == name), "{name} in {families:?}");
assert!(db.has_family(name));
}
let semibold = db.face("Source Serif 4 Semibold", "Regular");
assert_eq!((semibold.family.as_str(), semibold.style.as_str()), ("Source Serif 4", "Semibold"));
assert_eq!(db.face("inter medium", "Regular").style, "Medium");
assert_eq!(db.face("Inter SemiBold", "Regular").style, "SemiBold");
// Bold and italic choose within the faces the legacy name covers; the family's own Bold
// isn't one of them.
assert_eq!(db.face("Source Serif 4 Semibold", "Bold").style, "Semibold");
assert_eq!(db.face("Source Serif 4 Semibold", "Italic").style, "Semibold");
assert_eq!(db.styles("Source Sans 3 Semibold"), ["Semibold"]);
// The typographic family is unchanged.
assert_eq!(db.face("Source Serif 4", "Bold").style, "Bold");
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
assert_eq!(db.face("Source Serif 4", "Regular").legacy_family, "Source Serif 4");
// Not a name any face has.
assert!(!db.has_family("Source Serif 4 Light"));
assert_eq!(db.face("Source Serif 4 Light", "Regular").family, FALLBACK_FAMILY);
}
#[test]
fn a_legacy_named_instance_with_axis_settings_is_made_once() {
let db = FontDb::with_font_dirs(vec![]);
let a = db.face("Inter Medium", "Medium {wght:520}");
let b = db.face("Inter Medium", "Medium {wght:520}");
assert_eq!(a.id(), b.id());
assert_eq!(a.legacy_family, "Inter Medium");
}
#[test]
fn full_names_select_exact_faces() {
let db = FontDb::with_font_dirs(vec![]);
for style in ["Regular", "Bold", "Italic", "Bold Italic"] {
let expected = db.face("Source Serif 4", style);
let f = expected.skrifa().unwrap();
let full = name(&f, &[StringId::FULL_NAME]).unwrap();
assert!(db.has_family(&full), "{full}");
assert_eq!(db.face(&full, "Regular").id(), expected.id());
let bold = if full == expected.family { db.face(&full, "Bold") } else { expected.clone() };
assert_eq!(db.face(&full.to_ascii_lowercase(), "Bold").id(), bold.id());
}
assert!(!db.has_family("NotAFont Bold"));
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
}
#[test]
fn postscript_names_select_faces() {
let db = FontDb::with_font_dirs(vec![]);
for style in ["Regular", "Bold", "Italic", "Bold Italic"] {
let expected = db.face("Source Serif 4", style);
let f = expected.skrifa().unwrap();
let ps = name(&f, &[StringId::POSTSCRIPT_NAME]).unwrap();
assert!(db.has_family(&ps), "{ps}");
let actual = db.face(&ps, "Regular");
assert_eq!(actual.id(), expected.id(), "{ps}");
assert_eq!(db.face(&ps.to_ascii_lowercase(), "Bold").id(), expected.id());
}
assert!(!db.has_family("NotAFont-Bold"));
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
}
/// A variable font holding just `name` and `fvar` tables: family "Var Test", one `wght` axis
/// and `instances` named instances, each with its own style name.
fn variable_font(instances: u16) -> Vec<u8> {
let mut names: Vec<(u16, String)> = vec![(1, "Var Test".into()), (2, "Regular".into())];
names.extend((0..instances).map(|i| (256 + i, format!("Style {i}"))));
let mut name = vec![0, 0];
name.extend_from_slice(&(names.len() as u16).to_be_bytes());
name.extend_from_slice(&(6 + 12 * names.len() as u16).to_be_bytes());
let mut strings = Vec::new();
for (id, text) in &names {
let utf16: Vec<u8> = text.encode_utf16().flat_map(u16::to_be_bytes).collect();
for v in [3, 1, 0x409, *id, utf16.len() as u16, strings.len() as u16] {
name.extend_from_slice(&v.to_be_bytes());
}
strings.extend(utf16);
}
name.extend(strings);
let mut fvar = Vec::new();
for v in [1u16, 0, 16, 2, 1, 20, instances, 8] {
fvar.extend_from_slice(&v.to_be_bytes());
}
fvar.extend_from_slice(b"wght");
for v in [100u32 << 16, 400 << 16, 900 << 16] {
fvar.extend_from_slice(&v.to_be_bytes());
}
fvar.extend_from_slice(&[0, 0, 0, 2]); // flags, axisNameId
for i in 0..instances {
fvar.extend_from_slice(&(256 + i).to_be_bytes());
fvar.extend_from_slice(&[0, 0]);
fvar.extend_from_slice(&(400u32 << 16).to_be_bytes());
}
mini_font(&[(*b"fvar", fvar), (*b"name", name)])
}
/// A collection listing `font` `members` times.
fn collection_of(font: &[u8], members: u32) -> Vec<u8> {
let start = 12 + 4 * members;
let mut out = b"ttcf\0\x01\0\0".to_vec();
out.extend_from_slice(&members.to_be_bytes());
for _ in 0..members {
out.extend_from_slice(&start.to_be_bytes());
}
let mut font = font.to_vec();
let tables = u16::from_be_bytes([font[4], font[5]]) as usize;
for i in 0..tables {
let at = 12 + 16 * i + 8;
let offset = u32::from_be_bytes(font[at..at + 4].try_into().unwrap()) + start;
font[at..at + 4].copy_from_slice(&offset.to_be_bytes());
}
out.extend(font);
out
}
/// JetBrains Mono renamed "MingLiU JBMono" (a name the interpreter is required for), with its
/// `maxp` twilight-zone size set to `twilight`.
fn hint_reliant_font(twilight: u16) -> Vec<u8> {
let mut data = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/JetBrainsMono-Regular.ttf")).unwrap();
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
for (from, to) in [(utf16("JetBrains Mono"), utf16("MingLiU JBMono")), (b"JetBrains Mono".to_vec(), b"MingLiU JBMono".to_vec())] {
while let Some(at) = data.windows(from.len()).position(|w| w == from) {
data[at..at + to.len()].copy_from_slice(&to);
}
}
let tables = u16::from_be_bytes([data[4], data[5]]) as usize;
let entry = (0..tables).map(|i| 12 + 16 * i).find(|e| &data[*e..e + 4] == b"maxp").unwrap();
let maxp = u32::from_be_bytes(data[entry + 8..entry + 12].try_into().unwrap()) as usize;
data[maxp + 16..maxp + 18].copy_from_slice(&twilight.to_be_bytes());
data
}
#[test]
fn a_font_asking_for_huge_hinting_zones_is_drawn_unhinted() {
// #3692: skrifa's budget counts instructions, not the points each one moves.
let db = FontDb::with_font_dirs(vec![]);
db.add_font(hint_reliant_font(660));
let face = db.face("MingLiU JBMono", "Regular");
assert_eq!(face.family, "MingLiU JBMono");
assert!(face.hinter().is_some(), "a real font's zones are hinted");
assert!(!face.glyph_outline(face.glyph_for('a')).elements().is_empty());
let db = FontDb::with_font_dirs(vec![]);
db.add_font(hint_reliant_font(u16::MAX));
let face = db.face("MingLiU JBMono", "Regular");
assert!(face.hinter().is_none(), "a huge twilight zone is not hinted");
assert!(!face.glyph_outline(face.glyph_for('a')).elements().is_empty(), "drawn unhinted");
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn the_draw_that_turns_hinting_off_is_redrawn_unhinted_for_the_outline_cache() {
// #3692: the glyph whose slow hinted draw trips the breaker may be misassembled; it must not be
// what the outline cache keeps.
let data = hint_reliant_font(660);
let mut face = super::make_face(
super::FontBytes::Owned(std::sync::Arc::new(data)),
0,
"MingLiU JBMono".into(),
"Regular".into(),
"MingLiU JBMono".into(),
Vec::new(),
)
.unwrap();
assert!(face.hinter().is_some());
face.hint_strike = std::time::Duration::ZERO; // every hinted draw is slow
let gid = face.glyph_for('a');
assert!(face.draw_outline(gid, usize::MAX).hinted, "hinted while the breaker is closed");
assert!(face.draw_outline(gid, usize::MAX).hinted);
let tripping = face.draw_outline(gid, usize::MAX);
assert!(face.hinting_off.load(std::sync::atomic::Ordering::Relaxed), "the third slow draw trips the breaker");
assert!(!tripping.hinted, "the tripping draw is returned unhinted");
assert!(!tripping.path.elements().is_empty());
// And what the cache keeps is that outline.
let db = FontDb::with_font_dirs(vec![]);
assert_eq!(db.outline(&face, gid).elements(), tripping.path.elements());
}
#[test]
fn named_instances_and_collection_members_are_capped() {
// #3716: a file's faces are capped before they are built, however the file multiplies them.
let font = variable_font(300);
let faces = font_faces(&skrifa::FontRef::new(&font).unwrap(), 0);
assert_eq!(faces.len(), MAX_FILE_FACES + 1, "one more than the cap, so a caller can tell");
assert_eq!(faces[0].style, "Style 0");
assert_eq!(enumerate_faces(&font, MAX_FILE_FACES).len(), MAX_FILE_FACES);
// A million members, all the same font: read once, and no more than the cap.
let started = std::time::Instant::now();
let ttc = collection_of(BUNDLED[0], 1_000_000);
let found = enumerate_faces(&ttc, MAX_FILE_FACES);
assert_eq!(found.iter().map(|f| (f.family.as_str(), f.style.as_str())).collect::<Vec<_>>(), [(FALLBACK_FAMILY, "Regular")]);
let db = FontDb::with_font_dirs(vec![]);
assert_eq!(db.add_font(collection_of(&font, 1_000_000)), MAX_FILE_FACES);
// A font the user adds is refused instead.
assert!(matches!(db.install_font("v.ttf", &variable_font(300)), Err(user::UserFontError::Unusable(e)) if e.contains("more than 256")));
// The face counts above are what tell the old code (a face per member) apart; the time
// (under 0.1 s on a loaded machine) only backs up "read once", with room for a busy CI.
assert!(started.elapsed() < std::time::Duration::from_secs(10), "{:?}", started.elapsed());
}