From 180cb4f36f81e959d640d0f9f024cc905b0ef0f8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 12 Oct 2026 21:34:36 -0700 Subject: [PATCH] Fonts: the font scan leaves out faces with no outline tables The font scan also listed faces with none of the tables skrifa reads glyph outlines from (glyf, CFF, CFF2, VARC): faces whose glyphs are bitmaps only or are stored as outlines in Apple's hvgl table. FontDb::outline, which the renderer reads glyph outlines from, returns an empty path for every glyph of such a face. On macOS 27.0.1 the scan lists "GB18030 Bitmap" (/System/Library/Fonts/Supplemental/NISC18030.ttf), which stores its glyphs as bitmaps (bdat) and has no color glyph tables, so the renderer draws none of the 27,544 characters it maps. file_face_names now leaves out a face whose table directory has none of those tables, as vectorcraft's font scan does (storytold/vectorcraft#781). --- crates/fonts/src/fontdb.rs | 10 ++++++++++ crates/fonts/src/lib.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) 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.