use skia_safe::font_style::Weight;
use super::catalog::{FaceCatalog, FaceLookup, Scope};
use super::face::{FaceRecord, IntrinsicStyle, NameKind};
use super::opentype::NameId;
use super::request::{EffectiveStyle, FaceRequest};
use super::substitutes::{canonical_weight_names, substitutes_for};
use super::FaceRef;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ResolutionStep {
EmbeddedFace,
EmbeddedFamily,
SystemFamily,
SystemFaceName,
SystemMetadataAlias,
ParsedFaceName { base_family: String, weight: i32 },
Substitution {
via: &'static str,
substitute: &'static str,
},
}
impl ResolutionStep {
pub fn label(&self) -> &'static str {
match self {
Self::EmbeddedFace => "embedded face",
Self::EmbeddedFamily => "embedded family",
Self::SystemFamily => "system family",
Self::SystemFaceName => "face name",
Self::SystemMetadataAlias => "metadata alias",
Self::ParsedFaceName { .. } => "parsed name",
Self::Substitution { .. } => "substitute",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FallbackReason {
NoCandidate,
AmbiguousName {
step: ResolutionStep,
candidates: usize,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct Selection {
pub face: FaceRef,
pub record: FaceRecord,
pub style: EffectiveStyle,
pub step: ResolutionStep,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FaceResolution {
Selected(Box<Selection>),
Default {
reason: FallbackReason,
},
}
fn identifies_one_face(kind: NameKind) -> bool {
kind.is_primary_identity()
}
fn identifies_a_family(kind: NameKind) -> bool {
matches!(
kind,
NameKind::Table(NameId::Family | NameId::TypographicFamily | NameId::WwsFamily)
| NameKind::ManagerFamily
)
}
fn is_metadata_alias(kind: NameKind) -> bool {
matches!(
kind,
NameKind::Instance
| NameKind::ComposedStyle
| NameKind::ManagerFace
| NameKind::Table(NameId::UniqueId)
)
}
pub fn resolve(request: &FaceRequest<'_>, catalog: &FaceCatalog) -> FaceResolution {
let mut ambiguity: Option<(ResolutionStep, usize)> = None;
if let Some(selection) = by_face_name(
request,
catalog,
Scope::Embedded,
identifies_one_face,
ResolutionStep::EmbeddedFace,
&mut ambiguity,
) {
return selected(selection);
}
if let Some(selection) = by_family_names(
request,
catalog,
Scope::Embedded,
identifies_a_family,
ResolutionStep::EmbeddedFamily,
) {
return selected(selection);
}
if let Some(faces) = catalog.family_faces(request.name) {
let style = EffectiveStyle::resolve(request, IntrinsicStyle::NEUTRAL);
if let Some(selection) = rank(&faces, &style, ResolutionStep::SystemFamily) {
return selected(selection);
}
}
if let Some(selection) = by_face_name(
request,
catalog,
Scope::Deep,
identifies_one_face,
ResolutionStep::SystemFaceName,
&mut ambiguity,
) {
return selected(selection);
}
if let Some(selection) = by_face_name(
request,
catalog,
Scope::Deep,
is_metadata_alias,
ResolutionStep::SystemMetadataAlias,
&mut ambiguity,
) {
return selected(selection);
}
if let Some(selection) = by_family_names(
request,
catalog,
Scope::Deep,
identifies_a_family,
ResolutionStep::SystemMetadataAlias,
) {
return selected(selection);
}
if let Some((base_family, weight)) = parse_face_name(request.name) {
if let Some(faces) = catalog.family_faces(base_family) {
let style = EffectiveStyle::resolve(
request,
IntrinsicStyle {
weight,
..IntrinsicStyle::NEUTRAL
},
);
let step = ResolutionStep::ParsedFaceName {
base_family: base_family.to_owned(),
weight,
};
if let Some(selection) = rank(&faces, &style, step) {
return selected(selection);
}
}
}
if let Some((via, substitutes)) = substitutes_for(request.name) {
let base = IntrinsicStyle {
weight: parse_face_name(request.name).map_or(400, |(_, weight)| weight),
..IntrinsicStyle::NEUTRAL
};
let style = EffectiveStyle::resolve(request, base);
for substitute in substitutes {
let Some(faces) = catalog.family_faces(substitute) else {
continue;
};
let step = ResolutionStep::Substitution { via, substitute };
if let Some(selection) = rank(&faces, &style, step) {
return selected(selection);
}
}
}
FaceResolution::Default {
reason: match ambiguity {
Some((step, candidates)) => FallbackReason::AmbiguousName { step, candidates },
None => FallbackReason::NoCandidate,
},
}
}
fn selected(selection: Selection) -> FaceResolution {
FaceResolution::Selected(Box::new(selection))
}
fn by_face_name(
request: &FaceRequest<'_>,
catalog: &FaceCatalog,
scope: Scope,
accept: impl Fn(NameKind) -> bool + Copy,
step: ResolutionStep,
ambiguity: &mut Option<(ResolutionStep, usize)>,
) -> Option<Selection> {
let candidates = catalog.candidates(scope, request.name, accept);
match super::catalog::pick_unique(&candidates) {
FaceLookup::Missing => None,
FaceLookup::Ambiguous { candidates } => {
log::debug!(
"[font] '{}': {candidates} different faces answer to this name at the {} step; \
declining to guess",
request.name,
step.label()
);
ambiguity.get_or_insert((step, candidates));
None
}
FaceLookup::Found(face) => {
let record = catalog.face(face)?;
let style = EffectiveStyle::resolve(request, record.intrinsic);
Some(refine(catalog, scope, face, record, &style, step))
}
}
}
fn by_family_names(
request: &FaceRequest<'_>,
catalog: &FaceCatalog,
scope: Scope,
accept: impl Fn(NameKind) -> bool + Copy,
step: ResolutionStep,
) -> Option<Selection> {
let candidates = catalog.candidates(scope, request.name, accept);
let style = EffectiveStyle::resolve(request, IntrinsicStyle::NEUTRAL);
rank(&candidates, &style, step)
}
fn rank(
candidates: &[(FaceRef, FaceRecord)],
style: &EffectiveStyle,
step: ResolutionStep,
) -> Option<Selection> {
let (face, record) = candidates
.iter()
.enumerate()
.min_by_key(|(order, (_, record))| score(record, style, *order))
.map(|(_, pair)| pair)?;
Some(Selection {
face: *face,
record: record.clone(),
style: *style,
step,
})
}
fn refine(
catalog: &FaceCatalog,
scope: Scope,
face: FaceRef,
record: FaceRecord,
style: &EffectiveStyle,
step: ResolutionStep,
) -> Selection {
let unchanged = Selection {
face,
record: record.clone(),
style: *style,
step: step.clone(),
};
if !style.weight_is_requested() && !style.slant_is_requested() {
return unchanged;
}
let family = record
.typographic_family
.as_deref()
.unwrap_or(&record.canonical_family);
let siblings = catalog.candidates(scope, family, identifies_a_family);
if siblings.is_empty() {
return unchanged;
}
match rank(&siblings, style, step) {
Some(better)
if score(&better.record, style, usize::MAX)
< score(&unchanged.record, style, usize::MAX) =>
{
better
}
_ => unchanged,
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
struct FaceScore {
slant: u8,
width: u16,
weight: u32,
slot: u8,
instance: u8,
order: usize,
}
fn score(record: &FaceRecord, style: &EffectiveStyle, order: usize) -> FaceScore {
FaceScore {
slant: u8::from(record.intrinsic.is_italic() != style.is_italic()),
width: (*record.intrinsic.width - *style.width).unsigned_abs() as u16,
weight: weight_distance(record.intrinsic.weight, style),
slot: slot_penalty(record, style),
instance: u8::from(record.instance.is_some()),
order,
}
}
fn weight_distance(face: i32, style: &EffectiveStyle) -> u32 {
let delta = face - style.weight;
if delta >= 0 {
delta.unsigned_abs()
} else if style.weight_is_requested() {
delta.unsigned_abs().saturating_mul(2)
} else {
delta.unsigned_abs()
}
}
fn slot_penalty(record: &FaceRecord, style: &EffectiveStyle) -> u8 {
let wants_bold = style.weight >= *Weight::BOLD;
let declares = if wants_bold {
record.intrinsic.bold_slot
} else {
record.intrinsic.regular_slot
};
u8::from(!declares)
}
fn parse_face_name(name: &str) -> Option<(&str, i32)> {
let base = super::substitutes::strip_weight_suffix(name)?;
let suffix = name[base.len()..].trim();
let weight = (100..=900)
.step_by(100)
.find(|&w| {
canonical_weight_names(w)
.iter()
.any(|word| word.eq_ignore_ascii_case(suffix))
})
.unwrap_or(*Weight::NORMAL);
Some((base, weight))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::fonts::face::{FaceIdentity, FaceName, VariationInstance};
use crate::render::fonts::opentype::fvar::{AxisTag, VariationCoord};
use crate::render::fonts::request::Toggle;
use crate::render::fonts::FaceRequest;
use skia_safe::font_style::{Slant, Width};
fn face(family: &str, weight: i32, slant: Slant) -> FaceRecord {
FaceRecord {
identity: FaceIdentity::System {
family: family.to_owned(),
face_index: 0,
},
canonical_family: family.to_owned(),
typographic_family: None,
intrinsic: IntrinsicStyle {
weight,
slant,
bold_slot: weight >= *Weight::BOLD,
regular_slot: weight == *Weight::NORMAL && slant == Slant::Upright,
width: Width::NORMAL,
},
names: vec![FaceName::composed(NameKind::ManagerFamily, family)],
instance: None,
}
}
fn candidates(records: &[FaceRecord]) -> Vec<(FaceRef, FaceRecord)> {
records
.iter()
.enumerate()
.map(|(i, r)| (FaceRef::Deep(i), r.clone()))
.collect()
}
fn want(bold: Toggle, italic: Toggle, base: IntrinsicStyle) -> EffectiveStyle {
EffectiveStyle::resolve(&FaceRequest::new("probe", bold, italic), base)
}
fn pick(records: &[FaceRecord], style: &EffectiveStyle) -> usize {
match rank(&candidates(records), style, ResolutionStep::SystemFamily)
.expect("a non-empty candidate list always ranks")
.face
{
FaceRef::Deep(i) => i,
other => panic!("unexpected ref {other:?}"),
}
}
#[test]
fn a_plain_request_picks_the_regular_face() {
let family = [
face("F", 700, Slant::Upright),
face("F", 400, Slant::Upright),
face("F", 400, Slant::Italic),
];
assert_eq!(
pick(
&family,
&want(Toggle::Absent, Toggle::Absent, IntrinsicStyle::NEUTRAL)
),
1
);
}
#[test]
fn bold_and_italic_each_select_their_face() {
let family = [
face("F", 400, Slant::Upright),
face("F", 700, Slant::Upright),
face("F", 400, Slant::Italic),
face("F", 700, Slant::Italic),
];
let n = IntrinsicStyle::NEUTRAL;
assert_eq!(pick(&family, &want(Toggle::On, Toggle::Absent, n)), 1);
assert_eq!(pick(&family, &want(Toggle::Absent, Toggle::On, n)), 2);
assert_eq!(pick(&family, &want(Toggle::On, Toggle::On, n)), 3);
}
#[test]
fn slant_outranks_weight() {
let family = [
face("F", 400, Slant::Upright),
face("F", 900, Slant::Italic),
];
assert_eq!(
pick(
&family,
&want(Toggle::Absent, Toggle::On, IntrinsicStyle::NEUTRAL)
),
1
);
}
#[test]
fn an_explicit_bold_prefers_heavier_over_nearer() {
let family = [
face("F", 300, Slant::Upright),
face("F", 600, Slant::Upright),
];
assert_eq!(
pick(
&family,
&want(Toggle::On, Toggle::Absent, IntrinsicStyle::NEUTRAL)
),
1
);
let straddle = [
face("F", 600, Slant::Upright),
face("F", 800, Slant::Upright),
];
assert_eq!(
pick(
&straddle,
&want(Toggle::On, Toggle::Absent, IntrinsicStyle::NEUTRAL)
),
1
);
}
#[test]
fn an_unrequested_weight_is_symmetric() {
let base = IntrinsicStyle {
weight: 500,
..IntrinsicStyle::NEUTRAL
};
let straddle = [
face("F", 400, Slant::Upright),
face("F", 600, Slant::Upright),
];
assert_eq!(
pick(&straddle, &want(Toggle::Absent, Toggle::Absent, base)),
0
);
}
#[test]
fn width_outranks_weight_but_not_slant() {
let mut condensed = face("F", 400, Slant::Upright);
condensed.intrinsic.width = Width::CONDENSED;
let mut condensed_bold = face("F", 900, Slant::Upright);
condensed_bold.intrinsic.width = Width::CONDENSED;
let base = IntrinsicStyle {
width: Width::CONDENSED,
..IntrinsicStyle::NEUTRAL
};
let family = [face("F", 400, Slant::Upright), condensed_bold, condensed];
assert_eq!(
pick(&family, &want(Toggle::Absent, Toggle::Absent, base)),
2,
"the condensed Regular beats both the normal-width Regular and the condensed Black"
);
}
#[test]
fn a_static_face_beats_an_equally_good_instance() {
let mut instance = face("F", 600, Slant::Upright);
instance.instance = Some(VariationInstance {
subfamily: "SemiBold".into(),
post_script_name: None,
coords: vec![VariationCoord {
axis: AxisTag::WEIGHT,
value: 600.0,
}],
});
let statik = face("F", 600, Slant::Upright);
let base = IntrinsicStyle {
weight: 600,
..IntrinsicStyle::NEUTRAL
};
assert_eq!(
pick(
&[instance.clone(), statik.clone()],
&want(Toggle::Absent, Toggle::Absent, base)
),
1
);
let mut closer = instance;
closer.intrinsic.weight = 600;
let far_static = face("F", 400, Slant::Upright);
assert_eq!(
pick(
&[closer, far_static],
&want(Toggle::Absent, Toggle::Absent, base)
),
0
);
}
#[test]
fn ranking_is_stable_for_identical_candidates() {
let family = [
face("F", 400, Slant::Upright),
face("F", 400, Slant::Upright),
];
for _ in 0..8 {
assert_eq!(
pick(
&family,
&want(Toggle::Absent, Toggle::Absent, IntrinsicStyle::NEUTRAL)
),
0
);
}
}
#[test]
fn an_empty_candidate_list_ranks_to_nothing() {
assert!(rank(
&[],
&want(Toggle::Absent, Toggle::Absent, IntrinsicStyle::NEUTRAL),
ResolutionStep::SystemFamily
)
.is_none());
}
#[test]
fn a_face_qualified_name_parses_to_a_family_and_a_weight() {
assert_eq!(parse_face_name("Segoe UI Light"), Some(("Segoe UI", 300)));
assert_eq!(parse_face_name("Calibri Light"), Some(("Calibri", 300)));
assert_eq!(parse_face_name("Arial Black"), Some(("Arial", 900)));
assert_eq!(parse_face_name("Foo Extra Bold"), Some(("Foo", 800)));
assert_eq!(parse_face_name("Inter SemiBold"), Some(("Inter", 600)));
}
#[test]
fn a_plain_family_name_does_not_parse() {
assert_eq!(parse_face_name("Times New Roman"), None);
assert_eq!(parse_face_name("Highlight"), None);
assert_eq!(parse_face_name("Calibri"), None);
}
#[test]
fn the_parsed_weight_word_is_case_insensitive() {
assert_eq!(parse_face_name("Calibri LIGHT"), Some(("Calibri", 300)));
assert_eq!(parse_face_name("Calibri semibold"), Some(("Calibri", 600)));
}
#[test]
fn the_step_predicates_do_not_overlap() {
let kinds = [
NameKind::Table(NameId::Family),
NameKind::Table(NameId::Subfamily),
NameKind::Table(NameId::UniqueId),
NameKind::Table(NameId::Full),
NameKind::Table(NameId::PostScript),
NameKind::Table(NameId::TypographicFamily),
NameKind::Table(NameId::TypographicSubfamily),
NameKind::Table(NameId::CompatibleFull),
NameKind::Table(NameId::WwsFamily),
NameKind::Table(NameId::WwsSubfamily),
NameKind::ManagerFamily,
NameKind::ManagerFace,
NameKind::Instance,
NameKind::ComposedStyle,
];
for kind in kinds {
let hits = u8::from(identifies_one_face(kind))
+ u8::from(identifies_a_family(kind))
+ u8::from(is_metadata_alias(kind));
assert!(hits <= 1, "{kind:?} is accepted by {hits} steps");
}
}
#[test]
fn every_indexable_kind_is_reachable_from_some_step() {
for kind in [
NameKind::Table(NameId::Family),
NameKind::Table(NameId::UniqueId),
NameKind::Table(NameId::Full),
NameKind::Table(NameId::PostScript),
NameKind::Table(NameId::TypographicFamily),
NameKind::Table(NameId::CompatibleFull),
NameKind::Table(NameId::WwsFamily),
NameKind::ManagerFamily,
NameKind::ManagerFace,
NameKind::Instance,
NameKind::ComposedStyle,
] {
assert!(
identifies_one_face(kind) || identifies_a_family(kind) || is_metadata_alias(kind),
"{kind:?} is indexed but no step will ever match it"
);
}
}
}