use super::{variation::Variation, Font, Kind};
use crate::{
tables::{
head::{Head, MacStyle},
os2::{Os2, SelectionFlags},
post::Post,
},
types::Tag,
TableProvider,
};
#[derive(Copy, Clone, PartialEq, Debug, Default)]
pub struct Attributes {
pub width: Width,
pub style: Style,
pub weight: Weight,
}
impl Attributes {
pub fn from_variations<V>(font: &Font, variations: V) -> Self
where
V: IntoIterator,
V::Item: Into<Variation>,
{
let mut attributes = font.default_attributes();
let axes = font.axes();
if axes.is_empty() {
return attributes;
}
let (mut width, mut italic, mut slant, mut weight) = (None, None, None, None);
for variation in variations {
let variation = variation.into();
let slot = match variation.tag {
WDTH => &mut width,
ITAL => &mut italic,
SLNT => &mut slant,
WGHT => &mut weight,
_ => continue,
};
for axis in axes.iter().filter(|axis| axis.tag() == variation.tag) {
*slot = Some(axis.clamp(variation.value));
}
}
if let Some(width) = width {
attributes.width = Width::new(width / 100.0);
}
attributes.style = style_at(attributes.style, italic, slant);
if let Some(weight) = weight {
attributes.weight = Weight::new(weight);
}
attributes
}
pub fn from_named_instance(font: &Font, index: u32) -> Self {
Self::from_named_instance_with_overrides(font, index, core::iter::empty::<Variation>())
}
pub fn from_named_instance_with_overrides<V>(font: &Font, index: u32, overrides: V) -> Self
where
V: IntoIterator,
V::Item: Into<Variation>,
{
let instance = font.named_instances().get(index);
let named = instance.iter().flat_map(|instance| instance.variations());
Self::from_variations(font, named.chain(overrides.into_iter().map(Into::into)))
}
pub(crate) fn from_font(font: &Font) -> Self {
match font.kind() {
Kind::Sfnt(..) => Self::from_sfnt(&font.tables()),
Kind::Type1(type1) => {
Self::from_postscript(type1.weight(), type1.italic_angle() as f32)
}
Kind::Cff(cff, _) => cff.metadata().map_or_else(Self::default, |metadata| {
Self::from_postscript(metadata.weight(), metadata.italic_angle().to_f32())
}),
}
}
fn from_postscript(weight: Option<&str>, italic_angle: f32) -> Self {
Self {
width: Width::default(),
style: if italic_angle != 0.0 {
Style::Italic
} else {
Style::Normal
},
weight: weight.map_or_else(Weight::default, Weight::from_name),
}
}
fn from_sfnt<'a>(tables: &impl TableProvider<'a>) -> Self {
if let Ok(os2) = tables.os2() {
Self::from_os2_post(os2, tables.post().ok())
} else if let Ok(head) = tables.head() {
Self::from_head(head)
} else {
Self::default()
}
}
fn from_os2_post(os2: Os2, post: Option<Post>) -> Self {
let width = Width::from_width_class(os2.us_width_class());
let fs_selection = os2.fs_selection();
let style = if fs_selection.contains(SelectionFlags::ITALIC) {
Style::Italic
} else if fs_selection.contains(SelectionFlags::OBLIQUE) {
Style::Oblique(post.map(|post| post.italic_angle().to_f32()))
} else {
Style::Normal
};
let weight = Weight(os2.us_weight_class() as f32);
Self {
width,
style,
weight,
}
}
fn from_head(head: Head) -> Self {
let mac_style = head.mac_style();
let style = if mac_style.contains(MacStyle::ITALIC) {
Style::Italic
} else {
Style::default()
};
let weight = if mac_style.contains(MacStyle::BOLD) {
Weight::BOLD
} else {
Weight::default()
};
Self {
width: Width::default(),
style,
weight,
}
}
}
const WDTH: Tag = Tag::new(b"wdth");
const ITAL: Tag = Tag::new(b"ital");
const SLNT: Tag = Tag::new(b"slnt");
const WGHT: Tag = Tag::new(b"wght");
fn style_at(default: Style, italic: Option<f32>, slant: Option<f32>) -> Style {
let mut style = default;
match italic {
Some(italic) if italic >= 0.5 => style = Style::Italic,
Some(_) if style == Style::Italic => style = Style::Normal,
_ => {}
}
match slant {
_ if style == Style::Italic => {}
Some(slant) if slant != 0.0 => style = Style::Oblique(Some(slant)),
Some(_) if matches!(style, Style::Oblique(_)) => style = Style::Normal,
_ => {}
}
style
}
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct Width(f32);
impl Width {
pub const ULTRA_CONDENSED: Self = Self(0.5);
pub const EXTRA_CONDENSED: Self = Self(0.625);
pub const CONDENSED: Self = Self(0.75);
pub const SEMI_CONDENSED: Self = Self(0.875);
pub const NORMAL: Self = Self(1.0);
pub const SEMI_EXPANDED: Self = Self(1.125);
pub const EXPANDED: Self = Self(1.25);
pub const EXTRA_EXPANDED: Self = Self(1.5);
pub const ULTRA_EXPANDED: Self = Self(2.0);
pub const fn new(ratio: f32) -> Self {
Self(ratio)
}
fn from_width_class(width_class: u16) -> Self {
match width_class {
0..=1 => Self::ULTRA_CONDENSED,
2 => Self::EXTRA_CONDENSED,
3 => Self::CONDENSED,
4 => Self::SEMI_CONDENSED,
5 => Self::NORMAL,
6 => Self::SEMI_EXPANDED,
7 => Self::EXPANDED,
8 => Self::EXTRA_EXPANDED,
_ => Self::ULTRA_EXPANDED,
}
}
pub const fn ratio(self) -> f32 {
self.0
}
pub fn percentage(self) -> f32 {
self.0 * 100.0
}
}
impl Default for Width {
fn default() -> Self {
Self::NORMAL
}
}
#[derive(Copy, Clone, PartialEq, Default, Debug)]
pub enum Style {
#[default]
Normal,
Italic,
Oblique(Option<f32>),
}
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct Weight(f32);
impl Weight {
pub const THIN: Self = Self(100.0);
pub const EXTRA_LIGHT: Self = Self(200.0);
pub const LIGHT: Self = Self(300.0);
pub const SEMI_LIGHT: Self = Self(350.0);
pub const NORMAL: Self = Self(400.0);
pub const MEDIUM: Self = Self(500.0);
pub const SEMI_BOLD: Self = Self(600.0);
pub const BOLD: Self = Self(700.0);
pub const EXTRA_BOLD: Self = Self(800.0);
pub const BLACK: Self = Self(900.0);
pub const EXTRA_BLACK: Self = Self(950.0);
pub const fn new(weight: f32) -> Self {
Self(weight)
}
fn from_name(name: &str) -> Self {
const NAMES: &[(&str, Weight)] = &[
("thin", Weight::THIN),
("hairline", Weight::THIN),
("extralight", Weight::EXTRA_LIGHT),
("ultralight", Weight::EXTRA_LIGHT),
("light", Weight::LIGHT),
("semilight", Weight::SEMI_LIGHT),
("demilight", Weight::SEMI_LIGHT),
("book", Weight::NORMAL),
("regular", Weight::NORMAL),
("normal", Weight::NORMAL),
("roman", Weight::NORMAL),
("medium", Weight::MEDIUM),
("semibold", Weight::SEMI_BOLD),
("demibold", Weight::SEMI_BOLD),
("demi", Weight::SEMI_BOLD),
("bold", Weight::BOLD),
("extrabold", Weight::EXTRA_BOLD),
("ultrabold", Weight::EXTRA_BOLD),
("black", Weight::BLACK),
("heavy", Weight::BLACK),
("extrablack", Weight::EXTRA_BLACK),
("ultrablack", Weight::EXTRA_BLACK),
];
let letters = name
.bytes()
.filter(|byte| !matches!(byte, b' ' | b'-' | b'_'))
.map(|byte| byte.to_ascii_lowercase());
NAMES
.iter()
.find(|(known, _)| letters.clone().eq(known.bytes()))
.map_or(Self::NORMAL, |(_, weight)| *weight)
}
pub const fn value(self) -> f32 {
self.0
}
}
impl Default for Weight {
fn default() -> Self {
Self::NORMAL
}
}
#[cfg(test)]
mod tests {
use super::*;
use font_test_data::VAZIRMATN_VAR;
#[test]
fn missing_os2() {
let font = Font::new(font_test_data::CMAP12_FONT1, 0).unwrap();
let attributes = font.default_attributes();
assert_eq!(attributes.width, Width::NORMAL);
assert_eq!(attributes.style, Style::Italic);
assert_eq!(attributes.weight, Weight::BOLD);
}
#[test]
fn so_stylish() {
let font = Font::new(font_test_data::CMAP14_FONT1, 0).unwrap();
let attributes = font.default_attributes();
assert_eq!(attributes.width, Width::SEMI_CONDENSED);
assert_eq!(attributes.style, Style::Oblique(Some(-14.0)));
assert_eq!(attributes.weight, Weight::EXTRA_BOLD);
}
#[test]
fn variations_override_the_axes_the_font_has() {
let font = Font::new(VAZIRMATN_VAR, 0).unwrap();
let default = font.default_attributes();
assert_eq!(
Attributes::from_variations(&font, core::iter::empty::<Variation>()),
default
);
let at = |variations: &[(&str, f32)]| Attributes::from_variations(&font, variations);
assert_eq!(at(&[("wght", 650.0)]).weight, Weight::new(650.0));
assert_eq!(at(&[("wght", 2000.0)]).weight, Weight::new(900.0));
assert_eq!(
at(&[("wght", 300.0), ("wght", 500.0)]).weight,
Weight::MEDIUM
);
let ignored = at(&[("wdth", 75.0), ("ital", 1.0), ("slnt", -10.0)]);
assert_eq!(ignored, default);
let tinos = Font::new(font_test_data::TINOS_SUBSET, 0).unwrap();
assert_eq!(
Attributes::from_variations(&tinos, [("wght", 700.0)]),
tinos.default_attributes()
);
}
#[test]
fn named_instances_give_their_own_attributes() {
let font = Font::new(VAZIRMATN_VAR, 0).unwrap();
assert_eq!(
Attributes::from_named_instance(&font, 6).weight,
Weight::BOLD
);
assert_eq!(
Attributes::from_named_instance_with_overrides(&font, 6, [("wght", 800.0)]).weight,
Weight::EXTRA_BOLD
);
let missing = font.named_instances().len();
assert_eq!(
Attributes::from_named_instance(&font, missing),
font.default_attributes()
);
assert_eq!(
Attributes::from_named_instance_with_overrides(&font, missing, [("wght", 300.0)])
.weight,
Weight::LIGHT
);
}
#[test]
fn a_type1_font_states_its_weight_name_and_angle() {
let regular = Font::new(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA, 0).unwrap();
assert_eq!(regular.default_attributes(), Attributes::default());
let header = core::str::from_utf8(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA)
.unwrap()
.replace("/Weight (Book)", "/Weight (Bold)")
.replace("/ItalicAngle 0 def", "/ItalicAngle -12 def");
let bold_italic = Font::new(header.into_bytes(), 0).unwrap();
let attributes = bold_italic.default_attributes();
assert_eq!(attributes.weight, Weight::BOLD);
assert_eq!(attributes.style, Style::Italic);
assert_eq!(attributes.width, Width::NORMAL);
let sfnt = crate::FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff = sfnt.cff().unwrap().offset_data().as_bytes().to_vec();
let cff = Font::new(cff, 0).unwrap();
assert!(matches!(cff.kind(), Kind::Cff(..)));
assert_eq!(cff.default_attributes(), Attributes::default());
}
#[test]
fn weight_names() {
for (name, weight) in [
("Bold", Weight::BOLD),
("bold", Weight::BOLD),
("Black", Weight::BLACK),
("Semi Bold", Weight::SEMI_BOLD),
("Demi-Bold", Weight::SEMI_BOLD),
("ExtraLight", Weight::EXTRA_LIGHT),
("Book", Weight::NORMAL),
("Roman", Weight::NORMAL),
("Medium", Weight::MEDIUM),
("Heavy", Weight::BLACK),
("Unheard Of", Weight::NORMAL),
("", Weight::NORMAL),
] {
assert_eq!(Weight::from_name(name), weight, "{name:?}");
}
}
#[test]
fn italic_and_slant_axes_choose_the_style() {
let oblique = Style::Oblique(Some(-12.0));
assert_eq!(style_at(oblique, None, None), oblique);
assert_eq!(
style_at(Style::Normal, Some(1.0), Some(-10.0)),
Style::Italic
);
assert_eq!(style_at(Style::Normal, Some(0.5), None), Style::Italic);
assert_eq!(style_at(Style::Italic, Some(0.0), None), Style::Normal);
assert_eq!(
style_at(Style::Normal, None, Some(-10.0)),
Style::Oblique(Some(-10.0))
);
assert_eq!(style_at(oblique, None, Some(0.0)), Style::Normal);
assert_eq!(style_at(Style::Italic, None, Some(-10.0)), Style::Italic);
}
}