use std::cell::{OnceCell, RefCell};
use std::collections::HashMap;
use skia_safe::{FontMgr, FontStyle, FontStyleSet, Typeface};
use super::face::{
fold_name, FaceIdentity, FaceName, FaceRecord, IntrinsicStyle, NameKind, VariationInstance,
};
use super::opentype::fvar::{Axis, VariationCoord, Variations};
use super::opentype::name::NameRecords;
use super::opentype::{fvar, name, os2, stat, table_tag, NameId};
use super::EmbeddedFontId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FaceRef {
Embedded(usize),
Manager { family: usize, face: usize },
Deep(usize),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FaceLookup {
Found(FaceRef),
Ambiguous {
candidates: usize,
},
Missing,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Scope {
Embedded,
Deep,
}
struct FamilySlot {
name: String,
style_set: Option<FontStyleSet>,
faces: Option<Vec<FaceRecord>>,
}
#[derive(Default, Debug)]
struct NameIndex {
by_name: HashMap<String, Vec<usize>>,
}
impl NameIndex {
fn insert(&mut self, record: &FaceRecord, index: usize) {
for name in &record.names {
let key = fold_name(&name.text);
if key.is_empty() {
continue;
}
let slot = self.by_name.entry(key).or_default();
if !slot.contains(&index) {
slot.push(index);
}
}
}
fn candidates(&self, folded: &str) -> &[usize] {
self.by_name.get(folded).map_or(&[], Vec::as_slice)
}
}
#[derive(Default, Debug)]
struct DeepTier {
records: Vec<FaceRecord>,
index: NameIndex,
}
pub struct FaceCatalog {
font_mgr: FontMgr,
family_index: HashMap<String, usize>,
families: RefCell<Vec<FamilySlot>>,
embedded: Vec<FaceRecord>,
embedded_index: NameIndex,
deep: OnceCell<DeepTier>,
}
impl FaceCatalog {
pub fn build(font_mgr: FontMgr) -> Self {
let mut families = Vec::new();
let mut family_index = HashMap::new();
for name in font_mgr.family_names() {
family_index
.entry(fold_name(&name))
.or_insert(families.len());
families.push(FamilySlot {
name,
style_set: None,
faces: None,
});
}
log::debug!("[font] catalogue: {} host families", families.len());
Self {
font_mgr,
family_index,
families: RefCell::new(families),
embedded: Vec::new(),
embedded_index: NameIndex::default(),
deep: OnceCell::new(),
}
}
pub fn from_faces(font_mgr: FontMgr, faces: &[(String, Typeface)]) -> Self {
let mut catalog = Self::build(FontMgr::empty());
catalog.font_mgr = font_mgr;
let mut families: Vec<FamilySlot> = Vec::new();
let mut family_index: HashMap<String, usize> = HashMap::new();
let mut deep = DeepTier::default();
for (family, typeface) in faces {
let slot = *family_index.entry(fold_name(family)).or_insert_with(|| {
families.push(FamilySlot {
name: family.clone(),
style_set: None,
faces: Some(Vec::new()),
});
families.len() - 1
});
let face_index = families[slot].faces.as_ref().map_or(0, |faces| faces.len());
let identity = FaceIdentity::System {
family: family.clone(),
face_index,
};
let mut manager = manager_face_record(
family,
face_index,
typeface.font_style(),
typeface_style_name(typeface),
);
manager.identity = identity.clone();
let record = deep_face_record(typeface, identity, family, Some(&manager));
let instances = instance_records(typeface, &record);
if let Some(faces) = families[slot].faces.as_mut() {
faces.push(manager);
}
deep.index.insert(&record, deep.records.len());
deep.records.push(record);
for instance in instances {
deep.index.insert(&instance, deep.records.len());
deep.records.push(instance);
}
}
catalog.families = RefCell::new(families);
catalog.family_index = family_index;
let _ = catalog.deep.set(deep);
catalog
}
pub fn add_embedded_face(
&mut self,
font: EmbeddedFontId,
collection_index: u32,
declared_family: &str,
typeface: &Typeface,
) {
let mut record = deep_face_record(
typeface,
FaceIdentity::Embedded {
font,
collection_index,
},
declared_family,
None,
);
push_unique_name(
&mut record.names,
FaceName::composed(NameKind::ManagerFamily, declared_family),
);
let instances = instance_records(typeface, &record);
self.embedded_index.insert(&record, self.embedded.len());
self.embedded.push(record);
for instance in instances {
self.embedded_index.insert(&instance, self.embedded.len());
self.embedded.push(instance);
}
}
pub fn font_mgr(&self) -> &FontMgr {
&self.font_mgr
}
pub fn embedded_faces(&self) -> &[FaceRecord] {
&self.embedded
}
pub fn family_count(&self) -> usize {
self.families.borrow().len()
}
pub fn deep_is_built(&self) -> bool {
self.deep.get().is_some()
}
pub fn family_is_enumerated(&self, name: &str) -> bool {
self.family_index
.get(&fold_name(name))
.and_then(|&i| self.families.borrow().get(i).map(|f| f.faces.is_some()))
.unwrap_or(false)
}
pub fn face(&self, face: FaceRef) -> Option<FaceRecord> {
match face {
FaceRef::Embedded(i) => self.embedded.get(i).cloned(),
FaceRef::Manager { family, face } => {
self.enumerate(family);
self.families
.borrow()
.get(family)?
.faces
.as_ref()?
.get(face)
.cloned()
}
FaceRef::Deep(i) => self.deep.get().and_then(|d| d.records.get(i)).cloned(),
}
}
pub fn family_faces(&self, name: &str) -> Option<Vec<(FaceRef, FaceRecord)>> {
let family = match self.family_index.get(&fold_name(name)) {
Some(&family) => family,
None => self.adopt_unenumerated(name)?,
};
self.enumerate(family);
let families = self.families.borrow();
let faces = families.get(family)?.faces.as_ref()?;
Some(
faces
.iter()
.enumerate()
.map(|(face, record)| (FaceRef::Manager { family, face }, record.clone()))
.collect(),
)
}
fn adopt_unenumerated(&self, name: &str) -> Option<usize> {
let matched = self
.font_mgr
.match_family_style(name, FontStyle::normal())
.filter(|tf| carries_family(tf, name))?;
log::debug!(
"[font] '{name}' is matchable as '{}' but was not enumerated; adopting it",
matched.family_name()
);
let mut families = self.families.borrow_mut();
let index = families.len();
families.push(FamilySlot {
name: name.to_owned(),
style_set: None,
faces: None,
});
drop(families);
Some(index)
}
pub fn candidates(
&self,
scope: Scope,
name: &str,
accept: impl Fn(NameKind) -> bool + Copy,
) -> Vec<(FaceRef, FaceRecord)> {
let folded = fold_name(name);
if folded.is_empty() {
return Vec::new();
}
let (index, records, to_ref): (_, _, fn(usize) -> FaceRef) = match scope {
Scope::Embedded => (
&self.embedded_index,
&self.embedded,
FaceRef::Embedded as fn(usize) -> FaceRef,
),
Scope::Deep => {
let deep = self.deep();
(
&deep.index,
&deep.records,
FaceRef::Deep as fn(usize) -> FaceRef,
)
}
};
index
.candidates(&folded)
.iter()
.filter_map(|&i| {
let record = records.get(i)?;
record
.answers_to(&folded, accept)
.then(|| (to_ref(i), record.clone()))
})
.collect()
}
pub fn lookup(
&self,
scope: Scope,
name: &str,
accept: impl Fn(NameKind) -> bool + Copy,
) -> FaceLookup {
pick_unique(&self.candidates(scope, name, accept))
}
pub fn lookup_deep(&self, name: &str, accept: impl Fn(NameKind) -> bool + Copy) -> FaceLookup {
self.lookup(Scope::Deep, name, accept)
}
fn enumerate(&self, family: usize) {
let mut families = self.families.borrow_mut();
let Some(slot) = families.get_mut(family) else {
return;
};
if slot.faces.is_some() {
return;
}
let mut style_set = self.font_mgr.match_family(&slot.name);
let count = style_set.count();
let mut faces = Vec::with_capacity(count);
for face_index in 0..count {
let (style, style_name) = style_set.style(face_index);
faces.push(manager_face_record(
&slot.name, face_index, style, style_name,
));
}
slot.faces = Some(faces);
slot.style_set = Some(style_set);
}
fn deep(&self) -> &DeepTier {
self.deep.get_or_init(|| {
let started = std::time::Instant::now();
let mut tier = DeepTier::default();
let mut family = 0;
while family < self.families.borrow().len() {
self.enumerate(family);
for record in self.deep_records_for(family) {
tier.index.insert(&record, tier.records.len());
tier.records.push(record);
}
family += 1;
}
log::debug!(
"[font] catalogue deep tier: {} faces, {} distinct names, {:?}",
tier.records.len(),
tier.index.by_name.len(),
started.elapsed()
);
tier
})
}
fn deep_records_for(&self, family: usize) -> Vec<FaceRecord> {
let mut families = self.families.borrow_mut();
let Some(slot) = families.get_mut(family) else {
return Vec::new();
};
let family_name = slot.name.clone();
let Some(manager_faces) = slot.faces.clone() else {
return Vec::new();
};
let Some(style_set) = slot.style_set.as_mut() else {
return Vec::new();
};
let mut out = Vec::with_capacity(manager_faces.len());
for (face_index, manager) in manager_faces.iter().enumerate() {
let Some(typeface) = style_set.new_typeface(face_index) else {
continue;
};
let record = deep_face_record(
&typeface,
FaceIdentity::System {
family: family_name.clone(),
face_index,
},
&family_name,
Some(manager),
);
let instances = instance_records(&typeface, &record);
out.push(record);
out.extend(instances);
}
out
}
}
pub(crate) fn carries_family(typeface: &Typeface, requested: &str) -> bool {
typeface.family_name().eq_ignore_ascii_case(requested)
}
pub(crate) fn pick_unique(candidates: &[(FaceRef, FaceRecord)]) -> FaceLookup {
let Some((first_ref, first)) = candidates.first() else {
return FaceLookup::Missing;
};
let distinct = candidates[1..]
.iter()
.filter(|(_, other)| !same_face(first, other))
.count();
if distinct == 0 {
FaceLookup::Found(*first_ref)
} else {
FaceLookup::Ambiguous {
candidates: distinct + 1,
}
}
}
fn same_face(a: &FaceRecord, b: &FaceRecord) -> bool {
a.canonical_family == b.canonical_family
&& a.intrinsic == b.intrinsic
&& instance_coords(a) == instance_coords(b)
}
fn instance_coords(record: &FaceRecord) -> Option<&[VariationCoord]> {
record.instance.as_ref().map(|i| i.coords.as_slice())
}
fn typeface_style_name(typeface: &Typeface) -> Option<String> {
let bytes = read_table(typeface, b"name")?;
let names = name::read(&bytes).ok()?;
names
.preferred(NameId::TypographicSubfamily)
.or_else(|| names.preferred(NameId::Subfamily))
.map(str::to_owned)
}
fn manager_face_record(
family: &str,
face_index: usize,
style: FontStyle,
style_name: Option<String>,
) -> FaceRecord {
let mut names = vec![FaceName::composed(NameKind::ManagerFamily, family)];
if let Some(style_name) = style_name.filter(|s| !s.trim().is_empty()) {
push_unique_name(
&mut names,
FaceName::composed(NameKind::ManagerFace, format!("{family} {style_name}")),
);
}
FaceRecord {
identity: FaceIdentity::System {
family: family.to_owned(),
face_index,
},
canonical_family: family.to_owned(),
typographic_family: None,
intrinsic: IntrinsicStyle::from_font_style(style),
names,
instance: None,
}
}
fn deep_face_record(
typeface: &Typeface,
identity: FaceIdentity,
fallback_family: &str,
manager: Option<&FaceRecord>,
) -> FaceRecord {
let names_table = read_table(typeface, b"name")
.and_then(|bytes| log_malformed(name::read(&bytes), "name", fallback_family))
.unwrap_or_default();
let intrinsic = read_table(typeface, b"OS/2")
.and_then(|bytes| log_malformed(os2::read(&bytes), "OS/2", fallback_family))
.map(|m| IntrinsicStyle {
weight: m.weight,
width: m.width,
slant: m.slant,
bold_slot: m.bold_bit,
regular_slot: m.regular_bit,
})
.unwrap_or_else(|| IntrinsicStyle::from_font_style(typeface.font_style()));
let canonical_family = names_table
.preferred(NameId::Family)
.map(str::to_owned)
.or_else(|| manager.map(|m| m.canonical_family.clone()))
.unwrap_or_else(|| fallback_family.to_owned());
let typographic_family = names_table
.preferred(NameId::TypographicFamily)
.map(str::to_owned);
let mut names = Vec::new();
for record in names_table.iter() {
if !INDEXED_NAME_IDS.contains(&record.id) {
continue;
}
push_unique_name(
&mut names,
FaceName {
kind: NameKind::Table(record.id),
text: record.text.clone(),
localized: !record.language.is_english(),
},
);
}
if let Some(manager) = manager {
for name in &manager.names {
push_unique_name(&mut names, name.clone());
}
}
push_unique_name(
&mut names,
FaceName::composed(NameKind::ManagerFamily, &canonical_family),
);
if let Some(stat) = read_table(typeface, b"STAT")
.and_then(|bytes| log_malformed(stat::read(&bytes), "STAT", &canonical_family))
{
let family = typographic_family.as_deref().unwrap_or(&canonical_family);
for (composed, coords) in composed_style_names(family, &stat, &names_table) {
if coords_describe(&coords, &intrinsic) {
push_unique_name(
&mut names,
FaceName::composed(NameKind::ComposedStyle, composed),
);
}
}
}
FaceRecord {
identity,
canonical_family,
typographic_family,
intrinsic,
names,
instance: None,
}
}
const INDEXED_NAME_IDS: &[NameId] = &[
NameId::Family,
NameId::UniqueId,
NameId::Full,
NameId::PostScript,
NameId::TypographicFamily,
NameId::CompatibleFull,
NameId::WwsFamily,
];
type StyleWord<'a> = (&'a str, Vec<VariationCoord>);
type ComposedName = (String, Vec<VariationCoord>);
const MAX_COMPOSED_NAMES: usize = 128;
fn composed_style_names(
family: &str,
stat: &stat::StatAxisValues,
names_table: &NameRecords,
) -> Vec<ComposedName> {
let mut per_axis: Vec<(u16, Vec<StyleWord<'_>>)> = Vec::new();
for axis in &stat.axes {
let mut words: Vec<StyleWord<'_>> = Vec::new();
for value in stat.nameable() {
if !value.coords.iter().any(|c| c.axis == axis.tag) {
continue;
}
let Some(text) = names_table.preferred(NameId::from_raw(value.name_id)) else {
continue;
};
if !words.iter().any(|(existing, _)| *existing == text) {
words.push((text, value.coords.clone()));
}
}
if !words.is_empty() {
per_axis.push((axis.ordering, words));
}
}
per_axis.sort_by_key(|(ordering, _)| *ordering);
let mut out: Vec<ComposedName> = vec![(family.to_owned(), Vec::new())];
for (_, words) in &per_axis {
let mut next = Vec::with_capacity(out.len() * (words.len() + 1));
for (prefix, coords) in &out {
next.push((prefix.clone(), coords.clone()));
for (word, word_coords) in words {
let mut combined = coords.clone();
combined.extend(word_coords.iter().copied());
next.push((format!("{prefix} {word}"), combined));
}
}
if next.len() > MAX_COMPOSED_NAMES {
log::debug!(
"[font] '{family}': STAT cross-product exceeds {MAX_COMPOSED_NAMES}; \
composed names truncated"
);
next.truncate(MAX_COMPOSED_NAMES);
}
out = next;
}
out.retain(|(name, _)| name != family);
out
}
fn coords_describe(coords: &[VariationCoord], intrinsic: &IntrinsicStyle) -> bool {
use skia_safe::font_style::Slant;
coords.iter().all(|coord| match coord.axis {
fvar::AxisTag::WEIGHT => (coord.value.round() as i32) == intrinsic.weight,
fvar::AxisTag::WIDTH => width_from_percent(coord.value) == intrinsic.width,
fvar::AxisTag::ITALIC | fvar::AxisTag::SLANT => {
(coord.value != 0.0) == (intrinsic.slant != Slant::Upright)
}
_ => true,
})
}
fn instance_records(typeface: &Typeface, base: &FaceRecord) -> Vec<FaceRecord> {
let Some(bytes) = read_table(typeface, b"fvar") else {
return Vec::new();
};
let Some(variations) = log_malformed(fvar::read(&bytes), "fvar", &base.canonical_family) else {
return Vec::new();
};
if variations.instances.is_empty() {
return Vec::new();
}
let names_table = read_table(typeface, b"name")
.and_then(|bytes| name::read(&bytes).ok())
.unwrap_or_default();
let family = base
.typographic_family
.as_deref()
.unwrap_or(&base.canonical_family);
let mut out = Vec::new();
for instance in &variations.instances {
if instance.is_default(&variations.axes) {
continue;
}
let Some(subfamily) = names_table.preferred(NameId::from_raw(instance.subfamily_name_id))
else {
continue;
};
let post_script_name = instance
.post_script_name_id
.and_then(|id| names_table.preferred(NameId::from_raw(id)))
.map(str::to_owned);
let mut names = vec![FaceName::composed(
NameKind::Instance,
format!("{family} {subfamily}"),
)];
if family != base.canonical_family {
push_unique_name(
&mut names,
FaceName::composed(
NameKind::Instance,
format!("{} {subfamily}", base.canonical_family),
),
);
}
if let Some(ps) = &post_script_name {
push_unique_name(&mut names, FaceName::composed(NameKind::Instance, ps));
}
out.push(FaceRecord {
identity: base.identity.clone(),
canonical_family: base.canonical_family.clone(),
typographic_family: base.typographic_family.clone(),
intrinsic: intrinsic_at(&base.intrinsic, &instance.coords, &variations),
names,
instance: Some(VariationInstance {
subfamily: subfamily.to_owned(),
post_script_name,
coords: instance.coords.clone(),
}),
});
}
out.extend(composed_instance_records(
typeface,
base,
&variations,
&names_table,
&out,
));
out
}
fn composed_instance_records(
typeface: &Typeface,
base: &FaceRecord,
variations: &Variations,
names_table: &NameRecords,
named: &[FaceRecord],
) -> Vec<FaceRecord> {
let Some(stat) = read_table(typeface, b"STAT")
.and_then(|bytes| log_malformed(stat::read(&bytes), "STAT", &base.canonical_family))
else {
return Vec::new();
};
let family = base
.typographic_family
.as_deref()
.unwrap_or(&base.canonical_family);
let mut out = Vec::new();
for (composed, coords) in composed_style_names(family, &stat, names_table) {
if coords.iter().any(|c| variations.axis(c.axis).is_none()) {
continue;
}
let full = fill_defaults(&coords, variations);
if full.iter().all(|c| {
variations
.axis(c.axis)
.is_some_and(|a| a.default == c.value)
}) {
continue;
}
if named.iter().any(|record| {
record
.instance
.as_ref()
.is_some_and(|instance| instance.coords == full)
}) {
continue;
}
out.push(FaceRecord {
identity: base.identity.clone(),
canonical_family: base.canonical_family.clone(),
typographic_family: base.typographic_family.clone(),
intrinsic: intrinsic_at(&base.intrinsic, &full, variations),
names: vec![FaceName::composed(NameKind::ComposedStyle, &composed)],
instance: Some(VariationInstance {
subfamily: composed
.strip_prefix(family)
.map(|s| s.trim().to_owned())
.unwrap_or_else(|| composed.clone()),
post_script_name: None,
coords: full,
}),
});
}
out
}
fn fill_defaults(coords: &[VariationCoord], variations: &Variations) -> Vec<VariationCoord> {
variations
.axes
.iter()
.map(|axis| VariationCoord {
axis: axis.tag,
value: coords
.iter()
.find(|c| c.axis == axis.tag)
.map_or(axis.default, |c| c.value),
})
.collect()
}
fn intrinsic_at(
base: &IntrinsicStyle,
coords: &[VariationCoord],
variations: &Variations,
) -> IntrinsicStyle {
use skia_safe::font_style::Slant;
let mut out = *base;
for coord in coords {
let clamped = variations
.axis(coord.axis)
.map_or(coord.value, |axis: &Axis| axis.clamp(coord.value));
match coord.axis {
fvar::AxisTag::WEIGHT => out.weight = clamped.round() as i32,
fvar::AxisTag::WIDTH => out.width = width_from_percent(clamped),
fvar::AxisTag::ITALIC | fvar::AxisTag::SLANT if clamped != 0.0 => {
out.slant = Slant::Italic;
}
_ => {}
}
}
out.bold_slot = false;
out.regular_slot = false;
out
}
fn width_from_percent(percent: f32) -> skia_safe::font_style::Width {
use skia_safe::font_style::Width;
match percent {
p if p < 56.25 => Width::ULTRA_CONDENSED,
p if p < 68.75 => Width::EXTRA_CONDENSED,
p if p < 81.25 => Width::CONDENSED,
p if p < 93.75 => Width::SEMI_CONDENSED,
p if p < 106.25 => Width::NORMAL,
p if p < 118.75 => Width::SEMI_EXPANDED,
p if p < 137.5 => Width::EXPANDED,
p if p < 175.0 => Width::EXTRA_EXPANDED,
_ => Width::ULTRA_EXPANDED,
}
}
fn push_unique_name(names: &mut Vec<FaceName>, candidate: FaceName) {
if candidate.text.trim().is_empty() {
return;
}
if names
.iter()
.any(|n| n.kind == candidate.kind && n.text == candidate.text)
{
return;
}
names.push(candidate);
}
fn read_table(typeface: &Typeface, tag: &[u8; 4]) -> Option<Vec<u8>> {
typeface
.copy_table_data(table_tag(tag))
.map(|data| data.as_bytes().to_vec())
}
fn log_malformed<T, E: std::fmt::Display>(
result: Result<T, E>,
table: &str,
family: &str,
) -> Option<T> {
match result {
Ok(value) => Some(value),
Err(err) => {
log::debug!("[font] '{family}': unreadable {table} table ({err})");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::fonts::opentype::fvar::tests::build_fvar;
use crate::render::fonts::opentype::name::tests::build_name_table;
use crate::render::fonts::opentype::stat::tests::{build_stat, Value};
use skia_safe::font_style::{Slant, Weight, Width};
const WIN: u16 = 3;
const EN: u16 = 0x0409;
fn names_for(records: &[(u16, u16, u16, u16, &str)]) -> NameRecords {
name::read(&build_name_table(0, records, &[])).unwrap()
}
#[test]
fn stat_composes_family_plus_style_in_the_font_s_axis_order() {
let stat = stat::read(&build_stat(
1,
&[(b"wdth", 256, 0), (b"wght", 257, 1)],
&[
Value::Single(0, 0x0002, 300, 100.0), Value::Single(0, 0, 301, 75.0), Value::Single(1, 0x0002, 302, 400.0), Value::Single(1, 0, 303, 600.0), ],
302,
))
.unwrap();
let names = names_for(&[
(WIN, 1, EN, 301, "Condensed"),
(WIN, 1, EN, 303, "SemiBold"),
]);
let composed: Vec<String> = composed_style_names("Fixture Sans", &stat, &names)
.into_iter()
.map(|(name, _)| name)
.collect();
assert!(composed.contains(&"Fixture Sans Condensed".to_string()));
assert!(composed.contains(&"Fixture Sans SemiBold".to_string()));
assert!(
composed.contains(&"Fixture Sans Condensed SemiBold".to_string()),
"width precedes weight because the font's axis ordering says so, got {composed:?}"
);
assert!(
!composed.contains(&"Fixture Sans SemiBold Condensed".to_string()),
"the reverse order is not a name this font sanctions"
);
let with_coords = composed_style_names("Fixture Sans", &stat, &names);
let (_, semibold) = with_coords
.iter()
.find(|(name, _)| name == "Fixture Sans SemiBold")
.unwrap();
assert_eq!(
semibold
.iter()
.map(|c| (c.axis.to_string(), c.value))
.collect::<Vec<_>>(),
vec![("wght".to_string(), 600.0)]
);
}
#[test]
fn elided_axis_values_contribute_no_words() {
let stat = stat::read(&build_stat(
1,
&[(b"wght", 256, 0)],
&[Value::Single(0, 0x0002, 300, 400.0)],
300,
))
.unwrap();
let names = names_for(&[(WIN, 1, EN, 300, "Regular")]);
assert!(composed_style_names("Fixture Sans", &stat, &names).is_empty());
}
#[test]
fn the_family_alone_is_not_emitted_as_a_composed_style() {
let stat = stat::read(&build_stat(
1,
&[(b"wght", 256, 0)],
&[Value::Single(0, 0, 300, 600.0)],
300,
))
.unwrap();
let names = names_for(&[(WIN, 1, EN, 300, "SemiBold")]);
let composed: Vec<String> = composed_style_names("Fixture Sans", &stat, &names)
.into_iter()
.map(|(name, _)| name)
.collect();
assert_eq!(composed, vec!["Fixture Sans SemiBold"]);
}
#[test]
fn the_composed_cross_product_is_capped() {
let axes: Vec<(&[u8; 4], u16, u16)> = vec![
(b"wght", 256, 0),
(b"wdth", 257, 1),
(b"opsz", 258, 2),
(b"slnt", 259, 3),
];
let mut values = Vec::new();
let mut records: Vec<(u16, u16, u16, u16, &str)> = Vec::new();
const WORDS: [&str; 6] = ["Aa", "Bb", "Cc", "Dd", "Ee", "Ff"];
for axis in 0..4u16 {
for (w, word) in WORDS.iter().enumerate() {
let name_id = 300 + axis * 10 + w as u16;
values.push(Value::Single(axis, 0, name_id, 100.0 + w as f32));
records.push((WIN, 1, EN, name_id, word));
}
}
let stat = stat::read(&build_stat(1, &axes, &values, 300)).unwrap();
let composed = composed_style_names("Big", &stat, &names_for(&records));
assert!(
composed.len() <= MAX_COMPOSED_NAMES,
"got {} names",
composed.len()
);
}
#[test]
fn width_percentages_bucket_onto_the_width_class_scale() {
assert_eq!(width_from_percent(50.0), Width::ULTRA_CONDENSED);
assert_eq!(width_from_percent(75.0), Width::CONDENSED);
assert_eq!(width_from_percent(100.0), Width::NORMAL);
assert_eq!(width_from_percent(125.0), Width::EXPANDED);
assert_eq!(width_from_percent(200.0), Width::ULTRA_EXPANDED);
assert_eq!(width_from_percent(94.0), Width::NORMAL);
assert_eq!(width_from_percent(93.0), Width::SEMI_CONDENSED);
}
#[test]
fn an_instance_takes_its_style_from_its_coordinates() {
let variations = fvar::read(&build_fvar(
&[
(b"wght", 100.0, 400.0, 900.0, false),
(b"wdth", 75.0, 100.0, 125.0, false),
],
&[(300, &[600.0, 75.0], None)],
))
.unwrap();
let at = intrinsic_at(
&IntrinsicStyle::NEUTRAL,
&variations.instances[0].coords,
&variations,
);
assert_eq!(at.weight, 600);
assert_eq!(at.width, Width::CONDENSED);
assert!(
!at.bold_slot && !at.regular_slot,
"an instance is a position, not a family slot"
);
}
#[test]
fn a_nonzero_slant_or_italic_axis_makes_an_instance_italic() {
for tag in [b"ital", b"slnt"] {
let variations = fvar::read(&build_fvar(
&[(tag, -10.0, 0.0, 10.0, false)],
&[(300, &[10.0], None)],
))
.unwrap();
let at = intrinsic_at(
&IntrinsicStyle::NEUTRAL,
&variations.instances[0].coords,
&variations,
);
assert_eq!(at.slant, Slant::Italic, "{}", String::from_utf8_lossy(tag));
}
}
#[test]
fn an_out_of_range_coordinate_is_clamped_to_the_axis() {
let variations = fvar::read(&build_fvar(
&[(b"wght", 100.0, 400.0, 900.0, false)],
&[(300, &[400.0], None)],
))
.unwrap();
let at = intrinsic_at(
&IntrinsicStyle::NEUTRAL,
&[VariationCoord {
axis: fvar::AxisTag::WEIGHT,
value: 5000.0,
}],
&variations,
);
assert_eq!(at.weight, 900);
}
fn record(family: &str, weight: i32, name: &str) -> FaceRecord {
FaceRecord {
identity: FaceIdentity::System {
family: family.to_owned(),
face_index: 0,
},
canonical_family: family.to_owned(),
typographic_family: None,
intrinsic: IntrinsicStyle {
weight,
..IntrinsicStyle::NEUTRAL
},
names: vec![FaceName::asserted(NameId::Full, name)],
instance: None,
}
}
fn candidates(records: &[FaceRecord]) -> Vec<(FaceRef, FaceRecord)> {
records
.iter()
.enumerate()
.map(|(i, r)| (FaceRef::Deep(i), r.clone()))
.collect()
}
#[test]
fn a_single_candidate_resolves() {
let records = [record("Alpha", 400, "Alpha Regular")];
assert_eq!(
pick_unique(&candidates(&records)),
FaceLookup::Found(FaceRef::Deep(0))
);
}
#[test]
fn no_candidates_is_a_miss() {
assert_eq!(pick_unique(&[]), FaceLookup::Missing);
}
#[test]
fn two_different_faces_under_one_name_are_ambiguous() {
let records = [
record("Alpha", 400, "Shared Name"),
record("Beta", 700, "Shared Name"),
];
assert_eq!(
pick_unique(&candidates(&records)),
FaceLookup::Ambiguous { candidates: 2 }
);
}
#[test]
fn the_same_face_indexed_twice_is_not_ambiguous() {
let records = [
record("Alpha", 400, "Alpha Regular"),
record("Alpha", 400, "Alpha Regular"),
];
assert_eq!(
pick_unique(&candidates(&records)),
FaceLookup::Found(FaceRef::Deep(0))
);
}
#[test]
fn instances_at_different_locations_are_different_faces() {
let at = |weight: f32| {
let mut r = record("Alpha", weight as i32, "Alpha SemiBold");
r.instance = Some(VariationInstance {
subfamily: "SemiBold".into(),
post_script_name: None,
coords: vec![VariationCoord {
axis: fvar::AxisTag::WEIGHT,
value: weight,
}],
});
r
};
let records = [at(600.0), at(700.0)];
assert_eq!(
pick_unique(&candidates(&records)),
FaceLookup::Ambiguous { candidates: 2 }
);
}
#[test]
fn lookups_are_case_and_whitespace_insensitive() {
let catalog = FaceCatalog::build(FontMgr::new());
let first = family_names(&catalog)[0].clone();
let padded = format!(" {} ", first.to_uppercase());
assert_eq!(
catalog.family_faces(&padded).map(|f| f.len()),
catalog.family_faces(&first).map(|f| f.len())
);
}
#[test]
fn an_empty_name_never_matches() {
let catalog = FaceCatalog::build(FontMgr::new());
assert!(catalog
.candidates(Scope::Embedded, " ", |_| true)
.is_empty());
assert!(catalog.family_faces(" ").is_none());
}
#[test]
fn the_family_guard_rejects_a_substituted_face() {
let mgr = FontMgr::new();
let typeface = mgr
.legacy_make_typeface(None::<&str>, FontStyle::normal())
.expect("system default typeface");
let family = typeface.family_name();
assert!(carries_family(&typeface, &family), "its own family matches");
assert!(
carries_family(&typeface, &family.to_uppercase()),
"the comparison is case-insensitive"
);
assert!(
!carries_family(&typeface, "No Such Family 8f3a1c"),
"a face carrying a different family must be refused — this is the \
whole reason the rest of the chain is reachable"
);
}
#[test]
fn a_matchable_family_is_still_adopted() {
let catalog = FaceCatalog::build(FontMgr::new());
let Some(first) = family_names(&catalog).first().cloned() else {
return;
};
assert!(catalog.family_faces(&first).is_some());
assert!(
catalog.family_faces("No Such Family 8f3a1c").is_none(),
"a name the manager cannot match must not be adopted"
);
}
fn family_names(catalog: &FaceCatalog) -> Vec<String> {
catalog
.families
.borrow()
.iter()
.map(|f| f.name.clone())
.collect()
}
#[test]
fn construction_enumerates_names_only() {
let catalog = FaceCatalog::build(FontMgr::new());
assert!(
catalog.family_count() > 0,
"the test host must expose at least one family"
);
assert!(!catalog.deep_is_built());
assert!(
!catalog.family_is_enumerated(&family_names(&catalog)[0]),
"constructing the catalogue must not enumerate any family's faces"
);
}
#[test]
fn touching_one_family_enumerates_only_that_family() {
let catalog = FaceCatalog::build(FontMgr::new());
let names = family_names(&catalog);
if names.len() < 2 {
return;
}
assert!(catalog.family_faces(&names[0]).is_some());
assert!(catalog.family_is_enumerated(&names[0]));
assert!(
!catalog.family_is_enumerated(&names[1]),
"an unrelated family must not have been touched"
);
assert!(!catalog.deep_is_built(), "nor the deep tier");
}
#[test]
fn family_lookup_is_case_and_whitespace_folded() {
let catalog = FaceCatalog::build(FontMgr::new());
let first = family_names(&catalog)[0].clone();
assert!(catalog.family_faces(&first).is_some());
assert!(catalog.family_faces(&first.to_uppercase()).is_some());
assert!(catalog.family_faces("No Such Family 8f3a1c").is_none());
}
#[test]
fn a_manager_face_carries_manager_names_only() {
let record = manager_face_record(
"Inter",
2,
FontStyle::new(Weight::from(300), Width::NORMAL, Slant::Upright),
Some("Light".to_owned()),
);
assert_eq!(record.canonical_family, "Inter");
assert_eq!(record.intrinsic.weight, 300);
assert_eq!(
record.identity,
FaceIdentity::System {
family: "Inter".into(),
face_index: 2
}
);
assert!(
record.names.iter().all(|n| !n.kind.is_asserted()),
"nothing here was read from the font, so nothing may claim to be"
);
assert!(record
.names
.iter()
.any(|n| n.kind == NameKind::ManagerFace && n.text == "Inter Light"));
}
#[test]
fn a_face_with_no_style_name_gets_no_composed_face_name() {
let record = manager_face_record("Inter", 0, FontStyle::normal(), Some(" ".to_owned()));
assert_eq!(record.names.len(), 1);
assert_eq!(record.names[0].kind, NameKind::ManagerFamily);
}
#[test]
fn a_manager_face_ref_round_trips() {
let catalog = FaceCatalog::build(FontMgr::new());
let Some(faces) = catalog.family_faces(&family_names(&catalog)[0]) else {
return;
};
for (face_ref, record) in faces {
assert_eq!(catalog.face(face_ref).as_ref(), Some(&record));
}
}
#[test]
fn the_deep_tier_builds_once() {
let catalog = FaceCatalog::build(FontMgr::new());
let _ = catalog.lookup_deep("no such face 8f3a1c", |_| true);
assert!(catalog.deep_is_built());
let first = catalog.deep().records.len();
let _ = catalog.lookup_deep("another miss 8f3a1c", |_| true);
assert_eq!(catalog.deep().records.len(), first, "built exactly once");
}
#[test]
fn every_deep_record_is_findable_by_its_own_asserted_names() {
let catalog = FaceCatalog::build(FontMgr::new());
let _ = catalog.lookup_deep("prime the tier 8f3a1c", |_| true);
let mut checked = 0;
let records = catalog.deep().records.clone();
for record in &records {
for name in record.names_matching(NameKind::is_primary_identity) {
match catalog.lookup_deep(&name.text, NameKind::is_asserted) {
FaceLookup::Found(face_ref) => {
assert!(catalog.face(face_ref).is_some(), "ref must round-trip");
checked += 1;
}
FaceLookup::Ambiguous { .. } => checked += 1,
FaceLookup::Missing => {
panic!("'{}' is indexed but not findable", name.text)
}
}
}
if checked > 200 {
break;
}
}
assert!(
checked > 0,
"the test host exposes no face with a full or PostScript name"
);
}
#[test]
fn the_deep_tier_covers_every_manager_family() {
let catalog = FaceCatalog::build(FontMgr::new());
let _ = catalog.lookup_deep("prime the tier 8f3a1c", |_| true);
let covered: std::collections::HashSet<String> = catalog
.deep()
.records
.iter()
.filter_map(|r| match &r.identity {
FaceIdentity::System { family, .. } => Some(family.clone()),
FaceIdentity::Embedded { .. } => None,
})
.collect();
let enumerated = catalog.family_count();
assert!(
covered.len() >= enumerated.saturating_sub(enumerated / 10),
"deep tier covers {} of {enumerated} families",
covered.len()
);
}
}