diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 31cb9a18..9fe32e46 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -382,9 +382,16 @@ fn legacy_names(f: &skrifa::FontRef<'_>, family: &str) -> Option<(String, String Some((legacy, name(f, &[StringId::SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into()))) } +/// The tables skrifa reads glyph outlines from: `glyf` (TrueType), `CFF `, `CFF2` and `VARC`. For a +/// face with none of them (bitmaps only, or outlines in Apple's `hvgl` table), [`FontDb::outline`] +/// returns an empty path for every glyph. +#[cfg(not(target_arch = "wasm32"))] +const OUTLINE_TABLES: [&[u8; 4]; 4] = [b"glyf", b"CFF ", b"CFF2", b"VARC"]; + /// (family, style) of every face in the font file at `path`, reading only its table directories /// and `name` tables: a scan opens hundreds of font files, many of them megabytes long. A /// variable font is cataloged under its default style; its named instances appear once it loads. +/// Faces with none of the [`OUTLINE_TABLES`] are left out. #[cfg(not(target_arch = "wasm32"))] fn file_face_names(path: &std::path::Path) -> Vec<(String, String)> { use std::io::{Read, Seek, SeekFrom}; @@ -413,6 +420,9 @@ fn file_face_names(path: &std::path::Path) -> Vec<(String, String)> { let dir = read_at(start.into(), 12)?; let tables = u16::from_be_bytes(dir.get(4..6)?.try_into().ok()?) as usize; let records = read_at(u64::from(start) + 12, tables * 16)?; + if !records.as_chunks::<16>().0.iter().any(|r| OUTLINE_TABLES.iter().any(|t| r.starts_with(*t))) { + return None; + } let rec = records.as_chunks::<16>().0.iter().find(|r| r.starts_with(b"name"))?; let (offset, len) = (be32(rec, 8)?, be32(rec, 12)?); if len > MAX_NAME_TABLE { diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 16be2ca5..9b727530 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -220,6 +220,36 @@ mod tests { } } + #[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.