use cosmic_text::{Attrs, CacheKeyFlags, Weight, fontdb};
pub(crate) const SYNTHETIC_BOLD: CacheKeyFlags = CacheKeyFlags::from_bits_retain(1 << 31);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Weights {
pub(crate) regular: Weight,
pub(crate) bold: Weight,
}
impl Weights {
pub(crate) const CSS: Self = Self {
regular: Weight::NORMAL,
bold: Weight::BOLD,
};
pub(crate) fn of(db: &fontdb::Database, family: &str) -> Self {
let faces: Vec<(u16, Option<(f32, f32)>)> = db
.faces()
.filter(|face| face.families.iter().any(|(name, _)| name == family))
.map(|face| (face.weight.0, wght_range(db, face.id)))
.collect();
if faces.is_empty() {
return Self::CSS;
}
let serve = |asked: u16| -> Weight {
let covered = faces.iter().any(|&(weight, range)| {
weight == asked
|| range.is_some_and(|(lo, hi)| (lo..=hi).contains(&f32::from(asked)))
});
if covered {
return Weight(asked);
}
Weight(css_match(asked, faces.iter().map(|&(weight, _)| weight)).unwrap_or(asked))
};
Self {
regular: serve(Weight::NORMAL.0),
bold: serve(Weight::BOLD.0),
}
}
pub(crate) fn apply(self, attrs: Attrs<'_>, bold: bool) -> Attrs<'_> {
if !bold {
return attrs.weight(self.regular);
}
let flags = attrs.cache_key_flags;
let attrs = attrs.weight(self.bold);
if self.bold.0 < 600 {
attrs.cache_key_flags(flags | SYNTHETIC_BOLD)
} else {
attrs
}
}
}
fn css_match(asked: u16, weights: impl Iterator<Item = u16> + Clone) -> Option<u16> {
let up = |lo: u16, hi: u16| weights.clone().filter(|w| (lo..=hi).contains(w)).min();
let down = |lo: u16, hi: u16| weights.clone().filter(|w| (lo..=hi).contains(w)).max();
match asked {
400..=500 => up(asked, 500)
.or_else(|| down(0, asked - 1))
.or_else(|| up(501, u16::MAX)),
0..400 => down(0, asked).or_else(|| up(asked + 1, u16::MAX)),
_ => up(asked, u16::MAX).or_else(|| down(0, asked - 1)),
}
}
fn wght_range(db: &fontdb::Database, id: fontdb::ID) -> Option<(f32, f32)> {
use cosmic_text::skrifa::{FontRef, MetadataProvider, Tag};
db.with_face_data(id, |data, index| {
let font = FontRef::from_index(data, index).ok()?;
let axis = font.axes().get_by_tag(Tag::new(b"wght"))?;
Some((axis.min_value(), axis.max_value()))
})
.flatten()
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct WghtAxis {
min: f32,
max: f32,
points: Vec<(f32, f32)>,
}
impl WghtAxis {
pub(crate) fn read(font: swash::FontRef<'_>, weight: u16) -> Option<Self> {
let axis = font
.variations()
.find_by_tag(swash::Tag::from_be_bytes(*b"wght"))?;
let (min, max, default) = (axis.min_value(), axis.max_value(), axis.default_value());
let mut points: Vec<(f32, f32)> = Vec::new();
if (default - f32::from(weight)).abs() >= 0.5 {
for instance in font.instances() {
let Some(css) = instance
.name(None)
.and_then(|name| css_weight(&name.to_string()))
else {
continue;
};
let Some(at) = instance.values().nth(axis.index()) else {
continue;
};
if !points.iter().any(|&(w, _)| w == css) {
points.push((css, at));
}
}
if !points.iter().any(|&(w, _)| w == f32::from(weight)) {
points.push((f32::from(weight), default));
}
points.sort_by(|a, b| a.0.total_cmp(&b.0));
}
Some(Self { min, max, points })
}
pub(crate) fn coordinate(&self, css: f32) -> f32 {
let at = match self.points[..] {
[] => css,
[(_, only)] => only,
[(w0, c0), ..] if css <= w0 => c0,
[.., (wn, cn)] if css >= wn => cn,
_ => self
.points
.windows(2)
.find(|pair| css <= pair[1].0)
.map_or(css, |pair| {
let ((w0, c0), (w1, c1)) = (pair[0], pair[1]);
c0 + (c1 - c0) * (css - w0) / (w1 - w0)
}),
};
at.clamp(self.min, self.max)
}
}
fn css_weight(name: &str) -> Option<f32> {
let key: String = name
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_ascii_lowercase()
.replace("italic", "")
.replace("oblique", "");
Some(match key.as_str() {
"thin" | "hairline" => 100.0,
"extralight" | "ultralight" => 200.0,
"light" => 300.0,
"" | "regular" | "normal" | "book" | "roman" => 400.0,
"medium" => 500.0,
"semibold" | "demibold" => 600.0,
"bold" => 700.0,
"extrabold" | "ultrabold" => 800.0,
"black" | "heavy" => 900.0,
_ => return None,
})
}
pub(crate) fn embolden_strength(size: f32) -> f32 {
let t = ((size - 9.0) / (36.0 - 9.0)).clamp(0.0, 1.0);
size * (1.0 / 24.0 + (1.0 / 32.0 - 1.0 / 24.0) * t)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{font_face, variable_face};
use cosmic_text::fontdb::{Database, Source};
fn db(faces: Vec<Vec<u8>>) -> Database {
let mut db = Database::new();
for bytes in faces {
db.load_font_source(Source::Binary(std::sync::Arc::new(bytes)));
}
db
}
const BERKELEY: [(&str, u16); 2] = [("Regular", 100), ("Bold", 150)];
#[test]
fn the_flag_is_none_of_cosmic_texts() {
assert!(!CacheKeyFlags::all().intersects(SYNTHETIC_BOLD));
assert_eq!(SYNTHETIC_BOLD.bits(), 1 << 31);
}
#[test]
fn a_family_is_asked_at_weights_it_has() {
let db = db(vec![
font_face("Kui Both", 400, false, true),
font_face("Kui Both", 700, false, true),
font_face("Kui Regular", 400, false, true),
font_face("Kui Light", 300, false, true),
variable_face("Kui Var", false, [100, 100, 150], &BERKELEY),
variable_face("Kui Css", false, [100, 400, 900], &[]),
]);
let w = |family| Weights::of(&db, family);
assert_eq!(w("Kui Both"), Weights::CSS);
assert_eq!(w("Kui Css"), Weights::CSS, "the axis spans 400 and 700");
assert_eq!(w("No Such Family"), Weights::CSS);
let only = |weight| Weights {
regular: Weight(weight),
bold: Weight(weight),
};
assert_eq!(w("Kui Regular"), only(400));
assert_eq!(w("Kui Light"), only(300));
assert_eq!(w("Kui Var"), only(400));
}
#[test]
fn bold_of_a_lighter_face_is_marked_and_bold_of_a_bold_face_is_not() {
let base = Attrs::new();
let marked = |w: Weights| w.apply(base.clone(), true).cache_key_flags;
assert_eq!(marked(Weights::CSS), CacheKeyFlags::empty());
let regular = Weights {
regular: Weight::NORMAL,
bold: Weight::NORMAL,
};
assert_eq!(marked(regular), SYNTHETIC_BOLD);
assert_eq!(regular.apply(base.clone(), true).weight, Weight::NORMAL);
assert_eq!(
regular.apply(base.clone(), false).cache_key_flags,
CacheKeyFlags::empty()
);
let semibold = Weights {
regular: Weight::NORMAL,
bold: Weight::SEMIBOLD,
};
assert_eq!(marked(semibold), CacheKeyFlags::empty());
let italic = base.cache_key_flags(CacheKeyFlags::FAKE_ITALIC);
assert_eq!(
regular.apply(italic, true).cache_key_flags,
CacheKeyFlags::FAKE_ITALIC | SYNTHETIC_BOLD
);
}
fn axis(bytes: &[u8]) -> WghtAxis {
let font = swash::FontRef::from_index(bytes, 0).expect("the fixture parses");
WghtAxis::read(font, 400).expect("a wght axis")
}
#[test]
fn an_axis_off_the_css_scale_is_read_off_its_instances() {
let a = axis(&variable_face("Kui Var", false, [100, 100, 150], &BERKELEY));
assert_eq!(a.coordinate(400.0), 100.0);
assert_eq!(a.coordinate(700.0), 150.0);
assert_eq!(a.coordinate(550.0), 125.0);
assert_eq!(a.coordinate(100.0), 100.0);
assert_eq!(a.coordinate(900.0), 150.0);
let bare = axis(&variable_face("Kui Bare", false, [100, 100, 150], &[]));
assert_eq!(bare.coordinate(700.0), 100.0);
}
#[test]
fn an_instance_at_the_os2_weight_is_not_shadowed_by_the_default() {
let a = axis(&variable_face(
"Kui Thin Default",
false,
[100, 100, 900],
&[("Thin", 100), ("Regular", 400)],
));
assert_eq!(a.coordinate(400.0), 400.0);
assert_eq!(a.coordinate(100.0), 100.0);
}
#[test]
fn the_nearest_face_is_the_one_css_matching_takes() {
let m = |asked, weights: &[u16]| css_match(asked, weights.iter().copied());
assert_eq!(m(400, &[300, 500]), Some(500), "up to 500 first");
assert_eq!(m(400, &[300, 600]), Some(300), "then down");
assert_eq!(m(400, &[100, 600]), Some(100), "down before past 500");
assert_eq!(m(400, &[600, 900]), Some(600));
assert_eq!(m(700, &[500, 900]), Some(900), "bold looks up first");
assert_eq!(m(700, &[300, 500]), Some(500), "then down");
assert_eq!(m(300, &[200, 400]), Some(200), "light looks down first");
assert_eq!(m(300, &[400, 500]), Some(400));
assert_eq!(m(400, &[]), None);
let db = db(vec![
font_face("Kui Between", 300, false, true),
font_face("Kui Between", 500, false, true),
]);
assert_eq!(
Weights::of(&db, "Kui Between"),
Weights {
regular: Weight(500),
bold: Weight(500),
}
);
}
#[test]
fn an_axis_on_the_css_scale_is_its_own_coordinate() {
let a = axis(&variable_face(
"Kui Css",
false,
[200, 400, 700],
&[("Regular", 400), ("Bold", 700)],
));
assert_eq!(a.coordinate(400.0), 400.0);
assert_eq!(a.coordinate(700.0), 700.0);
assert_eq!(a.coordinate(900.0), 700.0);
}
#[test]
fn instance_names_say_their_weight() {
for (name, css) in [
("Regular", Some(400.0)),
("Italic", Some(400.0)),
("Bold", Some(700.0)),
("Bold Italic", Some(700.0)),
("SemiBold", Some(600.0)),
("Semi Bold", Some(600.0)),
("Extra-Light", Some(200.0)),
("Heavy", Some(900.0)),
("Condensed", None),
] {
assert_eq!(css_weight(name), css, "{name}");
}
}
}