Merge PR #2500: Resolve installed fonts by PostScript face name

Test-font pins: #3389's kept, #2500's dropped (layout tests.rs, tests_pages.rs, render lib.rs). crates/fonts/src/fontdb.rs: #2500's PostScript-name match moved into #3078's family_paths.
This commit is contained in:
aiongg
2026-10-22 01:50:29 +00:00
3 changed files with 79 additions and 8 deletions
+48 -8
View File
@@ -62,6 +62,8 @@ pub struct FontFace {
pub local_names: Vec<LocalName>,
/// Exact OpenType full name (name ID 4), distinct from family and style names.
full_name: Option<String>,
/// Exact OpenType PostScript name (name ID 6).
postscript: Option<String>,
/// usWeightClass-style weight (400 = regular).
pub weight: f32,
pub italic: bool,
@@ -204,11 +206,12 @@ impl FontFace {
}
pen.0
}
/// Is `family` this face's typographic, legacy or full name?
/// Is `family` this face's typographic, legacy, full or PostScript name?
pub fn named(&self, family: &str) -> bool {
self.family.eq_ignore_ascii_case(family)
|| self.legacy_family.eq_ignore_ascii_case(family)
|| self.full_name.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))
|| self.postscript.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))
}
/// Does the face map `c` to a glyph?
pub fn covers(&self, c: char) -> bool {
@@ -318,6 +321,7 @@ struct CatalogEntry {
style: String,
legacy: String,
full_name: Option<String>,
postscript: Option<String>,
local: Vec<LocalName>,
/// A Chinese, Japanese or Korean font ([`FontFace::is_east_asian`]).
east_asian: bool,
@@ -802,6 +806,8 @@ struct Found {
legacy: String,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
full_name: Option<String>,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
postscript: Option<String>,
/// The family's names in other languages ([`FontFace::local_names`]).
local: Vec<LocalName>,
/// A Chinese, Japanese or Korean font ([`FontFace::is_east_asian`]); the scan's catalog keeps it.
@@ -829,13 +835,23 @@ fn font_faces(f: &skrifa::FontRef<'_>, index: u32) -> Vec<Found> {
let legacy = legacy_family(&family, &style);
// Name ID 1 names the default instance only: keep the names of the whole family.
let local = local.iter().filter(|n| n.english.eq_ignore_ascii_case(&family)).cloned().collect();
named.push(Found { index, family: family.clone(), style, legacy, full_name: None, local, east_asian, coords });
named.push(Found { index, family: family.clone(), style, legacy, full_name: None, postscript: None, local, east_asian, coords });
}
if !named.is_empty() {
return named;
}
let legacy = id1.filter(|n| !n.eq_ignore_ascii_case(&family)).unwrap_or_else(|| legacy_family(&family, &style));
vec![Found { index, family, style, legacy, full_name: name(f, &[StringId::FULL_NAME]), local, east_asian, coords: Vec::new() }]
vec![Found {
index,
family,
style,
legacy,
full_name: name(f, &[StringId::FULL_NAME]),
postscript: name(f, &[StringId::POSTSCRIPT_NAME]),
local,
east_asian,
coords: Vec::new(),
}]
}
/// Parse every face in `data` (a font file or collection); a variable font yields one face per
@@ -892,6 +908,7 @@ fn make_face(bytes: FontBytes, index: u32, family: String, style: String, legacy
legacy_family,
local_names: Vec::new(),
full_name: coords.is_empty().then(|| name(&f, &[StringId::FULL_NAME])).flatten(),
postscript: coords.is_empty().then(|| name(&f, &[StringId::POSTSCRIPT_NAME])).flatten(),
weight,
italic,
upem: m.units_per_em.max(1) as f64,
@@ -1236,6 +1253,7 @@ impl FontDb {
style: f.style,
legacy: f.legacy,
full_name: f.full_name,
postscript: f.postscript,
local: f.local,
east_asian: f.east_asian,
path: p.clone(),
@@ -1282,6 +1300,7 @@ impl FontDb {
c.family.eq_ignore_ascii_case(family)
|| c.legacy.eq_ignore_ascii_case(family)
|| c.full_name.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))
|| c.postscript.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))
})
.map(|c| c.family.as_str())
.collect();
@@ -1294,7 +1313,7 @@ impl FontDb {
/// Resolve a family + style to a face, falling back to the closest style of the family, then to
/// Source Sans 3 Regular. `family` is a typographic family ("Roboto") or a face's legacy family
/// name ("Roboto Light", whose styles are Light and Light Italic). Installed system fonts are
/// found by name whatever ran before. An exact full name selects its face.
/// found by name whatever ran before. An exact full or PostScript name selects its face.
pub fn face(&self, family: &str, style: &str) -> Arc<FontFace> {
if let Some(f) = self.find(family, style) {
return f;
@@ -1392,7 +1411,7 @@ impl FontDb {
}
/// Is `family` available (loaded, or installed on the system)? English and localized family
/// names and exact full face names count.
/// names and exact full or PostScript face names count.
pub fn has_family(&self, family: &str) -> bool {
if self.is_loaded(family) {
return true;
@@ -1402,13 +1421,14 @@ impl FontDb {
c.family.eq_ignore_ascii_case(family)
|| c.legacy.eq_ignore_ascii_case(family)
|| c.full_name.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))
|| c.postscript.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))
}) {
return true;
}
self.english_family(family).is_some()
}
/// Is a face named `family` (its typographic, legacy or full name) loaded?
/// Is a face named `family` (typographic, legacy, full or PostScript) loaded?
pub fn is_loaded(&self, family: &str) -> bool {
self.read_faces().iter().any(|f| f.named(family))
}
@@ -1472,8 +1492,12 @@ impl FontDb {
cands = faces.iter().filter(|f| f.legacy_family.eq_ignore_ascii_case(family)).collect();
}
if cands.is_empty() {
// A full name selects one exact face, regardless of the requested style.
return faces.iter().find(|f| f.full_name.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))).cloned();
// Exact full names keep precedence over PostScript aliases; either selects one face.
return faces
.iter()
.find(|f| f.full_name.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family)))
.or_else(|| faces.iter().find(|f| f.postscript.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(family))))
.cloned();
}
let ns = norm(style);
if let Some(f) = cands.iter().find(|f| norm(&f.style) == ns) {
@@ -2065,4 +2089,20 @@ mod tests {
assert!(!db.has_family("NotAFont Bold"));
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
}
#[test]
fn postscript_names_select_faces() {
let db = FontDb::with_font_dirs(vec![]);
for style in ["Regular", "Bold", "Italic", "Bold Italic"] {
let expected = db.face("Source Serif 4", style);
let f = expected.skrifa().unwrap();
let ps = name(&f, &[StringId::POSTSCRIPT_NAME]).unwrap();
assert!(db.has_family(&ps), "{ps}");
let actual = db.face(&ps, "Regular");
assert_eq!(actual.id(), expected.id(), "{ps}");
assert_eq!(db.face(&ps.to_ascii_lowercase(), "Bold").id(), expected.id());
}
assert!(!db.has_family("NotAFont-Bold"));
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
}
}
+28
View File
@@ -678,3 +678,31 @@ fn circle_checks_face_coverage() {
let primary = db.face("Source Serif 4", "Regular");
assert_eq!(db.fallback_for('●', primary.id()).unwrap().family, FALLBACK_FAMILY);
}
#[test]
fn catalog_postscript_names() {
let dir = font_dir("postscript");
for path in [dir.join("Sysfont-Regular.ttf"), dir.join("Sub/Sysfont-Bold.TTF")] {
let mut bytes = std::fs::read(&path).unwrap();
for (from, to) in [
(b"SourceSans3".to_vec(), b"SysfontSans".to_vec()),
("SourceSans3".encode_utf16().flat_map(u16::to_be_bytes).collect(), "SysfontSans".encode_utf16().flat_map(u16::to_be_bytes).collect()),
] {
let mut offset = 0;
while let Some(at) = bytes[offset..].windows(from.len()).position(|w| w == from) {
bytes[offset + at..offset + at + to.len()].copy_from_slice(&to);
offset += at + to.len();
}
}
std::fs::write(path, bytes).unwrap();
}
let db = FontDb::with_font_dirs(vec![dir.clone()]);
assert!(!db.is_loaded("SysfontSans-Bold"));
assert!(db.has_family("SysfontSans-Bold"));
let face = db.face("SysfontSans-Bold", "Regular");
assert_eq!((face.family.as_str(), face.style.as_str()), (FAMILY, "Bold"));
assert_eq!(db.face("sysfontsans-bold", "Italic").id(), face.id());
assert_eq!(db.face(FAMILY, "Regular").style, "Regular");
assert!(!has(&db.families(), "SysfontSans-Bold"));
std::fs::remove_dir_all(dir).unwrap();
}
+3
View File
@@ -219,10 +219,13 @@ The deleted/hidden-text wrapping, ellipse wrapping and widow-control regressions
The legacy text-shadow control compares visible grey pixels with a translated Art-path mask. Foreground occlusion makes visible pixel centroids unsuitable for measuring the shadow offset. A reversed-offset mutation must fail this spatial check. Renderer behavior is unchanged.
Installed static fonts also resolve by their OpenType PostScript name (name ID 6), retaining the named face rather than substituting an unrelated family. Missing faces still use the existing fallback rules. A local Best for printing control from Word for Mac 16.113.2 (build 26092012) retained the directly requested Cambria-Bold face.
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-16 | trivial | Exact PostScript names resolve installed static faces without changing family lookup |
| 2026-10-21 | trivial | Pin the font input of four geometric layout controls; assertions and renderer font policy unchanged. |
| 2026-10-20 | trivial | A click below a floating table, outside its cells, goes to the paragraph beside or after it instead of a cell of its last row, as checked in Word (#2038) |
| 2026-10-19 | trivial | A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, starts the next line with its text instead of ending the line (a TOC entry's page number keeps its leader); a left tab still past the right indent there takes no room (#1840) |