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 <echelon@gmail.com>
This commit is contained in:
.CooCoo
2026-10-14 18:01:13 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent d129fa2daa
commit 00c8ee3407
2 changed files with 37 additions and 2 deletions
+3
View File
@@ -103,3 +103,6 @@ display_name = "Gert Pellin"
[people.tlomts-dev]
display_name = "The Legend Of MTS Dev"
[people.dotCooCoo]
real_name = "Robert Lee"
+34 -2
View File
@@ -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);
}
}