From 00c8ee340796bce6ddfe69f7f2aad80788a07e20 Mon Sep 17 00:00:00 2001 From: ".CooCoo" Date: Wed, 14 Oct 2026 15:01:13 -0700 Subject: [PATCH] Fonts: the interface CJK fallback skips faces without outlines (#1250) * Fonts: the interface CJK fallback skips faces without outlines The interface CJK fallback has listed PingFangUI.ttc from FontServices.framework as its last macOS candidate since #495. The skrifa crate reads glyph outlines only from the glyf, CFF, CFF2 and VARC tables. On macOS 27.0.1 the 32 faces in PingFangUI.ttc have none of them and keep their outlines in Apple's hvgl table. pick_face counts coverage from each face's cmap alone. The file's first face maps all 1,114 CJK characters of the zh-hans and zh-hant catalogs. When no earlier candidate maps all of them, find_ui_font returns that face for a Chinese interface built without craft-fonts, and the interface would draw the characters it takes from that face as blank space. pick_face now skips a face with none of those four tables, and find_ui_font goes on to the next candidate. VectorCraft's font scan has the same check (storytold/vectorcraft#781). * Contributors: add dotCooCoo (Robert Lee) --------- Co-authored-by: Brandon Thomas --- contributors/people.toml | 3 +++ crates/fonts/src/ui_fallback.rs | 36 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/contributors/people.toml b/contributors/people.toml index ee37d2c10..b1fe21397 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -103,3 +103,6 @@ display_name = "Gert Pellin" [people.tlomts-dev] display_name = "The Legend Of MTS Dev" + +[people.dotCooCoo] +real_name = "Robert Lee" diff --git a/crates/fonts/src/ui_fallback.rs b/crates/fonts/src/ui_fallback.rs index 6cbe8f794..19ea62ae3 100644 --- a/crates/fonts/src/ui_fallback.rs +++ b/crates/fonts/src/ui_fallback.rs @@ -65,6 +65,10 @@ const MAX_FACES: u32 = 64; const MAX_ASSETS: usize = 8192; /// Characters a face's coverage is measured on. const MAX_NEEDED: usize = 4096; +/// The tables skrifa reads glyph outlines from: TrueType, CFF, CFF2 and VARC. A face with +/// outlines only in Apple's `hvgl` table has none of them, and the interface draws its characters +/// as blank space. +const OUTLINE_TABLES: [&[u8; 4]; 4] = [b"glyf", b"CFF ", b"CFF2", b"VARC"]; /// A character only a font for the language has: a simplified-only hanzi, or kana. fn probe(hans: bool) -> char { @@ -169,8 +173,9 @@ pub(crate) fn find_ui_font(dirs: &[std::path::PathBuf], names: &[&str], needed: /// The face of a font file (or collection) to use and how many of `needed` it maps: the one /// mapping the most, then preferring a Regular style and, for Chinese, a Simplified Chinese -/// family (`… SC`, `… GB`, as in a pan-CJK collection). `None` if the header isn't a font's or -/// no face maps any of `needed`. +/// family (`… SC`, `… GB`, as in a pan-CJK collection). Faces with none of the +/// [`OUTLINE_TABLES`] are skipped. `None` if the header isn't a font's or no face with outlines +/// maps any of `needed`. pub(crate) fn pick_face(data: &[u8], needed: &[char], hans: bool) -> Option<(u32, String, usize)> { let magic = data.get(..4)?; if ![&[0, 1, 0, 0][..], b"OTTO", b"ttcf"].contains(&magic) { @@ -183,6 +188,9 @@ pub(crate) fn pick_face(data: &[u8], needed: &[char], hans: bool) -> Option<(u32 let mut best: Option<((usize, bool, bool), u32, String)> = None; for i in 0..count { let Ok(f) = skrifa::FontRef::from_index(data, i) else { continue }; + if !OUTLINE_TABLES.iter().any(|t| f.table_data(skrifa::Tag::new(t)).is_some()) { + continue; + } let charmap = f.charmap(); let covered = needed.iter().filter(|c| charmap.map(**c).is_some()).count(); if covered == 0 { @@ -254,4 +262,28 @@ mod tests { assert_eq!(found.family, "Source Sans 3"); let _ = std::fs::remove_dir_all(&dir); } + + #[test] + fn skips_faces_without_outlines() { + // Renaming the `glyf` table to `hvgl` gives a face like those of `PingFangUI.ttc` on + // macOS 27.0.1. Its cmap maps characters, and skrifa reads no outlines from it. + let sans = crate::bundled()[0]; + let mut hvgl = sans.to_vec(); + let tables = u16::from_be_bytes([hvgl[4], hvgl[5]]) as usize; + let glyf = (0..tables).map(|r| 12 + r * 16).find(|&at| &hvgl[at..at + 4] == b"glyf").unwrap(); + hvgl[glyf..glyf + 4].copy_from_slice(b"hvgl"); + assert!(skrifa::FontRef::new(&hvgl).unwrap().charmap().map('A').is_some()); + assert!(pick_face(&hvgl, &['A'], true).is_none()); + assert_eq!(pick_face(sans, &['A'], true), Some((0, "Source Sans 3".to_string(), 1))); + // `find_ui_font` uses the next file, and returns `None` when no other file is listed. + let dir = std::env::temp_dir().join(format!("wc-uifallback-outlines-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("hvgl.ttf"), &hvgl).unwrap(); + std::fs::write(dir.join("glyf.ttf"), sans).unwrap(); + let found = find_ui_font(std::slice::from_ref(&dir), &["hvgl.ttf", "glyf.ttf"], &['A'], true).unwrap(); + assert!(found.bytes == sans); + assert!(find_ui_font(std::slice::from_ref(&dir), &["hvgl.ttf"], &['A'], true).is_none()); + let _ = std::fs::remove_dir_all(&dir); + } }