Fonts: faces with no outline tables are no longer listed

read_face (crates/text/src/sfnt.rs) also listed faces with none of the
tables skrifa reads glyph outlines from (glyf, CFF, CFF2, VARC), such
as faces whose glyphs are bitmaps only or whose outlines are in Apple's
hvgl table. The renderer draws glyphs only from those outlines, so text
set in such a face is blank. On macOS 27.0.1 the system scan listed
"GB18030 Bitmap" (NISC18030.ttf, bdat bitmaps), so it appeared in the
Font menus.

read_face now returns no face when the table directory has none of
these tables, before it reads the name table. In a collection, the
faces with outlines keep their index. VectorCraft's font scan has the
same check (storytold/vectorcraft#781).
This commit is contained in:
dotCooCoo committed 2026-10-13 07:51:29 -07:00
1 parent cee1feb185
commit eafc6208d9
1 file changed
+49 -1
+49 -1
View File
@@ -4,6 +4,11 @@
use std::io::{Read, Seek, SeekFrom};
/// The tables skrifa reads glyph outlines from: TrueType, CFF, CFF2 and VARC. The renderer draws
/// no glyph of a face that has none of them, such as a face with bitmaps only or with its outlines
/// in Apple's `hvgl` table.
const OUTLINE_TABLES: [&[u8; 4]; 4] = [b"glyf", b"CFF ", b"CFF2", b"VARC"];
/// Names and style attributes of one face in a font file.
#[derive(Clone, Debug, PartialEq)]
pub struct FaceNames {
@@ -32,7 +37,8 @@ fn read_at<R: Read + Seek>(r: &mut R, off: u64, len: usize) -> Option<Vec<u8>> {
Some(v)
}
/// Read the names of every face in a font file (a single font or a collection).
/// Read the names of every face in a font file (a single font or a collection). Faces with none
/// of the `OUTLINE_TABLES` are left out.
pub fn read_faces<R: Read + Seek>(r: &mut R) -> Vec<FaceNames> {
let Some(head) = read_at(r, 0, 12) else { return Vec::new() };
let offsets: Vec<u32> = if &head[0..4] == b"ttcf" {
@@ -58,6 +64,9 @@ fn read_face<R: Read + Seek>(r: &mut R, off: u64, index: u32) -> Option<FaceName
}
let n = be16(&h, 4)? as usize;
let dir = read_at(r, off + 12, n * 16)?;
if !dir.as_chunks::<16>().0.iter().any(|rec| OUTLINE_TABLES.iter().any(|t| rec.starts_with(*t))) {
return None;
}
let mut name = None;
let mut os2 = None;
for i in 0..n {
@@ -137,4 +146,43 @@ mod tests {
assert_eq!(n[0].family, "Noto Serif");
assert!(read_faces_bytes(b"not a font").is_empty());
}
#[test]
fn faces_without_outline_tables_are_left_out() {
use skrifa::MetadataProvider;
// Renaming the `glyf` record of a copy of Inter Regular gives a face with no outline
// tables, like a face whose glyphs are bitmaps only or are kept in Apple's `hvgl` table.
// The new tag keeps the table directory sorted.
let font = crate::fonts::INTER_REGULAR;
let n = usize::from(be16(font, 4).unwrap());
let glyf = (0..n).map(|i| 12 + 16 * i).find(|&r| &font[r..r + 4] == b"glyf").unwrap();
let mut renamed = font.to_vec();
renamed[glyf..glyf + 4].copy_from_slice(b"gxyz");
// skrifa maps 'A' to a glyph of the copy and reads no outline for it.
let copy = skrifa::FontRef::new(&renamed).unwrap();
assert!(copy.outline_glyphs().get(copy.charmap().map('A').unwrap()).is_none());
assert!(read_faces_bytes(&renamed).is_empty());
assert_eq!(read_faces_bytes(font).len(), 1);
// A collection holds both faces, which share one copy of the tables. Table offsets count
// from the start of the collection. Only the face with outlines is listed, with its index 1.
let dir = 12 + 16 * n;
let tables_at = 20 + 2 * dir;
let shifted = |face: &[u8]| {
let mut d = face[..dir].to_vec();
for r in (12..dir).step_by(16) {
let o = be32(&d, r + 8).unwrap() + tables_at as u32;
d[r + 8..r + 12].copy_from_slice(&o.to_be_bytes());
}
d
};
// The header holds the tag, version 1.0, the number of faces and where each face's table
// directory starts.
let mut ttc = [*b"ttcf", [0, 1, 0, 0], 2u32.to_be_bytes(), 20u32.to_be_bytes(), (20 + dir as u32).to_be_bytes()].concat();
ttc.extend(shifted(&renamed));
ttc.extend(shifted(font));
ttc.extend_from_slice(font);
let faces = read_faces_bytes(&ttc);
assert_eq!(faces.len(), 1);
assert_eq!((faces[0].index, faces[0].family.as_str(), faces[0].style.as_str()), (1, "Inter", "Regular"));
}
}