use skia_safe::font_style::{Slant, Weight, Width};
use skia_safe::FontStyle;
use super::opentype::fvar::VariationCoord;
use super::opentype::NameId;
use super::EmbeddedFontId;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum FaceIdentity {
System {
family: String,
face_index: usize,
},
Embedded {
font: EmbeddedFontId,
collection_index: u32,
},
}
impl FaceIdentity {
pub fn is_embedded(&self) -> bool {
matches!(self, Self::Embedded { .. })
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct VariationInstance {
pub subfamily: String,
pub post_script_name: Option<String>,
pub coords: Vec<VariationCoord>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IntrinsicStyle {
pub weight: i32,
pub width: Width,
pub slant: Slant,
pub bold_slot: bool,
pub regular_slot: bool,
}
impl IntrinsicStyle {
pub const NEUTRAL: Self = Self {
weight: 400,
width: Width::NORMAL,
slant: Slant::Upright,
bold_slot: false,
regular_slot: true,
};
pub fn from_font_style(style: FontStyle) -> Self {
Self {
weight: *style.weight(),
width: style.width(),
slant: style.slant(),
bold_slot: *style.weight() >= *Weight::BOLD,
regular_slot: *style.weight() == *Weight::NORMAL && style.slant() == Slant::Upright,
}
}
pub fn font_style(&self) -> FontStyle {
FontStyle::new(Weight::from(self.weight), self.width, self.slant)
}
pub fn is_italic(&self) -> bool {
matches!(self.slant, Slant::Italic | Slant::Oblique)
}
pub fn is_already_bold(&self) -> bool {
self.bold_slot || self.weight >= *Weight::BOLD
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NameKind {
Table(NameId),
ManagerFamily,
ManagerFace,
Instance,
ComposedStyle,
}
impl NameKind {
pub fn is_asserted(self) -> bool {
matches!(self, Self::Table(_))
}
pub fn is_primary_identity(self) -> bool {
matches!(
self,
Self::Table(NameId::Full)
| Self::Table(NameId::CompatibleFull)
| Self::Table(NameId::PostScript)
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FaceName {
pub kind: NameKind,
pub text: String,
pub localized: bool,
}
impl FaceName {
pub fn asserted(id: NameId, text: impl Into<String>) -> Self {
Self {
kind: NameKind::Table(id),
text: text.into(),
localized: false,
}
}
pub fn composed(kind: NameKind, text: impl Into<String>) -> Self {
Self {
kind,
text: text.into(),
localized: false,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FaceRecord {
pub identity: FaceIdentity,
pub canonical_family: String,
pub typographic_family: Option<String>,
pub intrinsic: IntrinsicStyle,
pub names: Vec<FaceName>,
pub instance: Option<VariationInstance>,
}
impl FaceRecord {
pub fn names_matching(
&self,
accept: impl Fn(NameKind) -> bool + Copy,
) -> impl Iterator<Item = &FaceName> {
self.names.iter().filter(move |n| accept(n.kind))
}
pub fn answers_to(&self, folded: &str, accept: impl Fn(NameKind) -> bool + Copy) -> bool {
self.names_matching(accept)
.any(|n| fold_name(&n.text) == folded)
}
pub fn needs_instancing(&self) -> bool {
self.instance.is_some()
}
}
pub fn fold_name(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for (i, word) in name.split_whitespace().enumerate() {
if i > 0 {
out.push(' ');
}
out.extend(word.chars().flat_map(char::to_lowercase));
}
out
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) fn face(family: &str, weight: i32, names: Vec<FaceName>) -> 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,
instance: None,
}
}
#[test]
fn folding_ignores_case_and_repeated_whitespace() {
assert_eq!(
fold_name(" Proxima Nova SEMIBOLD "),
fold_name("proxima nova semibold")
);
assert_eq!(fold_name("A\tB\nC"), "a b c");
}
#[test]
fn folding_preserves_punctuation() {
assert_ne!(fold_name("Arial-BoldMT"), fold_name("Arial BoldMT"));
assert_ne!(fold_name("A-B"), fold_name("AB"));
}
#[test]
fn folding_lowercases_beyond_ascii() {
assert_eq!(fold_name("ПТ Санс"), fold_name("пт санс"));
assert_eq!(fold_name("ΑΒΓ"), "αβγ");
}
#[test]
fn folding_an_empty_or_blank_name_yields_empty() {
assert_eq!(fold_name(""), "");
assert_eq!(fold_name(" \t "), "");
}
#[test]
fn only_table_names_are_asserted_by_the_font() {
assert!(NameKind::Table(NameId::PostScript).is_asserted());
assert!(NameKind::Table(NameId::Family).is_asserted());
assert!(!NameKind::ManagerFamily.is_asserted());
assert!(!NameKind::ManagerFace.is_asserted());
assert!(!NameKind::Instance.is_asserted());
assert!(!NameKind::ComposedStyle.is_asserted());
}
#[test]
fn only_full_compatible_full_and_postscript_are_primary_identities() {
assert!(NameKind::Table(NameId::Full).is_primary_identity());
assert!(NameKind::Table(NameId::CompatibleFull).is_primary_identity());
assert!(NameKind::Table(NameId::PostScript).is_primary_identity());
assert!(!NameKind::Table(NameId::Family).is_primary_identity());
assert!(!NameKind::Table(NameId::TypographicFamily).is_primary_identity());
assert!(!NameKind::Table(NameId::WwsFamily).is_primary_identity());
assert!(!NameKind::Table(NameId::Subfamily).is_primary_identity());
assert!(!NameKind::ManagerFace.is_primary_identity());
}
#[test]
fn the_neutral_style_is_upright_regular() {
let n = IntrinsicStyle::NEUTRAL;
assert_eq!(n.weight, 400);
assert_eq!(n.width, Width::NORMAL);
assert_eq!(n.slant, Slant::Upright);
assert!(!n.bold_slot);
assert!(n.regular_slot);
assert!(!n.is_italic());
}
#[test]
fn a_font_style_round_trips_through_intrinsic() {
let style = FontStyle::new(Weight::from(342), Width::CONDENSED, Slant::Italic);
let i = IntrinsicStyle::from_font_style(style);
assert_eq!(i.weight, 342);
assert_eq!(i.width, Width::CONDENSED);
assert_eq!(i.slant, Slant::Italic);
assert!(i.is_italic());
assert_eq!(i.font_style(), style);
}
#[test]
fn slot_flags_inferred_from_a_font_style_use_the_bold_threshold() {
let at = |w: i32| {
IntrinsicStyle::from_font_style(FontStyle::new(
Weight::from(w),
Width::NORMAL,
Slant::Upright,
))
};
assert!(!at(600).bold_slot, "Semibold does not claim the Bold slot");
assert!(at(700).bold_slot);
assert!(at(900).bold_slot);
assert!(at(400).regular_slot);
assert!(!at(500).regular_slot);
}
#[test]
fn oblique_counts_as_italic() {
let oblique = IntrinsicStyle {
slant: Slant::Oblique,
..IntrinsicStyle::NEUTRAL
};
assert!(oblique.is_italic());
}
#[test]
fn the_bold_slot_bit_alone_counts_as_already_bold() {
let semibold_bold_slot = IntrinsicStyle {
weight: 600,
bold_slot: true,
..IntrinsicStyle::NEUTRAL
};
assert!(semibold_bold_slot.is_already_bold());
}
#[test]
fn a_weight_past_bold_alone_counts_as_already_bold() {
let black_no_bit = IntrinsicStyle {
weight: *Weight::BLACK,
bold_slot: false,
..IntrinsicStyle::NEUTRAL
};
assert!(black_no_bit.is_already_bold());
}
#[test]
fn neither_signal_present_is_not_already_bold() {
let semibold = IntrinsicStyle {
weight: 600,
bold_slot: false,
..IntrinsicStyle::NEUTRAL
};
assert!(!semibold.is_already_bold());
}
#[test]
fn a_face_answers_to_its_names_under_the_accepted_kinds() {
let f = face(
"Proxima Nova",
600,
vec![
FaceName::asserted(NameId::PostScript, "ProximaNova-Semibold"),
FaceName::composed(NameKind::ManagerFace, "Proxima Nova Semibold"),
],
);
assert!(f.answers_to(&fold_name("proximanova-semibold"), NameKind::is_asserted));
assert!(
!f.answers_to(&fold_name("Proxima Nova Semibold"), NameKind::is_asserted),
"a composed name is not evidence and must not match an evidence-only step"
);
assert!(f.answers_to(&fold_name("Proxima Nova Semibold"), |_| true));
}
#[test]
fn names_matching_filters_by_kind() {
let f = face(
"Inter",
700,
vec![
FaceName::asserted(NameId::Full, "Inter Bold"),
FaceName::asserted(NameId::Family, "Inter"),
FaceName::composed(NameKind::ComposedStyle, "Inter Bold Condensed"),
],
);
let primary: Vec<&str> = f
.names_matching(NameKind::is_primary_identity)
.map(|n| n.text.as_str())
.collect();
assert_eq!(primary, vec!["Inter Bold"]);
}
#[test]
fn only_an_instance_record_needs_instancing() {
let mut f = face("Fixture Sans", 400, vec![]);
assert!(!f.needs_instancing());
f.instance = Some(VariationInstance {
subfamily: "SemiBold".into(),
post_script_name: None,
coords: vec![VariationCoord {
axis: super::super::opentype::AxisTag::WEIGHT,
value: 600.0,
}],
});
assert!(f.needs_instancing());
}
#[test]
fn embedded_identity_is_distinguishable() {
let embedded = FaceIdentity::Embedded {
font: EmbeddedFontId::from_raw(3),
collection_index: 1,
};
assert!(embedded.is_embedded());
assert!(!FaceIdentity::System {
family: "X".into(),
face_index: 0
}
.is_embedded());
}
}