Skip to content

Commit

Permalink
feat(font-enumeration): add style, weight and stretch (macos untested…
Browse files Browse the repository at this point in the history
…, windows todo)
  • Loading branch information
tomcur committed Jul 25, 2024
1 parent 975c5a4 commit b6e5732
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 6 deletions.
99 changes: 97 additions & 2 deletions font-enumeration/src/core_text.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,99 @@
use std::path::PathBuf;

use core_text::font_collection;
use core_text::{
font_collection,
font_descriptor::{CTFontTraits, SymbolicTraitAccessors, TraitAccessors},
};

use crate::{Error, OwnedFont};
use crate::{Error, OwnedFont, Stretch, Style, Weight};

fn roughly_eq(a: f32, b: f32) -> bool {
const EPSILON: f32 = 0.00001;

(a - b).abs() <= EPSILON
}

impl Style {
fn from_core_text(traits: &CTFontTraits) -> Self {
let symbolic = traits.symbolic_traits();

if symbolic.is_vertical() {
return Style::Normal;
}

// TODO: check if this true if and only if the font is italic, and not, e.g., when it is
// oblique
if symbolic.is_italic() {
return Style::Italic;
}

let angle_degrees = traits.normalized_slant() / 30. * 360.;
Style::Oblique(Some(angle_degrees as f32))
}
}

impl Weight {
fn from_core_text(weight: f64) -> Self {
const CT_ULTRA_LIGHT: f32 = -0.8;
const CT_THIN: f32 = -0.6;
const CT_LIGHT: f32 = -0.4;
const CT_REGULAR: f32 = 0.;
const CT_MEDIUM: f32 = 0.23;
const CT_SEMI_BOLD: f32 = 0.3;
const CT_BOLD: f32 = 0.4;
const CT_HEAVY: f32 = 0.56;
const CT_BLACK: f32 = 0.62;

const MAPPING: &[(f32, Weight)] = &[
(CT_ULTRA_LIGHT, Weight::new(50.)),
(CT_THIN, Weight::THIN),
(CT_LIGHT, Weight::LIGHT),
(CT_REGULAR, Weight::NORMAL),
(CT_MEDIUM, Weight::MEDIUM),
(CT_SEMI_BOLD, Weight::SEMI_BOLD),
(CT_BOLD, Weight::BOLD),
(CT_HEAVY, Weight::EXTRA_BOLD),
(CT_BLACK, Weight::BLACK),
];

let ct_weight = weight as f32;

if ct_weight <= CT_ULTRA_LIGHT {
// TODO: perhaps interpolate below Weight::THIN up to a min of 1.?
return Weight::new(50.);
}
for idx in 1..MAPPING.len() {
let (ct_weight_b, ot_weight_b) = MAPPING[idx];

if roughly_eq(ct_weight, ct_weight_b) {
return ot_weight_b;
}

if ct_weight < ct_weight_b {
let (ct_weight_a, ot_weight_a) = MAPPING[idx - 1];
let ot_weight_a = ot_weight_a.0;
let ot_weight_b = ot_weight_b.0;

let ot_weight = ot_weight_a
+ (ct_weight - ct_weight_a) / (ct_weight_a - ct_weight_b)
* (ot_weight_a - ot_weight_b);
return Weight::new(ot_weight);
}
}

// TODO: perhaps interpolate above Weight::BLACK up to a max of 1000.?
Weight::BLACK
}
}

impl Stretch {
fn from_core_text(width: f64) -> Self {
let stretch = (width + 1.0) * 4.0;

// TODO: perhaps clamp this by rough equality checking to conventional stretch values.
Stretch::new(stretch as f32)
}
}

pub fn all_fonts() -> Result<Box<[OwnedFont]>, Error> {
let collection = font_collection::create_for_all_families();
Expand All @@ -12,10 +103,14 @@ pub fn all_fonts() -> Result<Box<[OwnedFont]>, Error> {
.ok_or(Error::SystemCollection)?
.iter()
.filter_map(|font| {
let traits = font.traits();
Some(OwnedFont {
family_name: font.family_name(),
font_name: font.font_name(),
path: font.font_path()?,
style: Style::from_core_text(&traits),
weight: Weight::from_core_text(traits.normalized_weight()),
stretch: Stretch::from_core_text(traits.normalized_width()),
})
})
.collect();
Expand Down
7 changes: 6 additions & 1 deletion font-enumeration/src/direct_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use dwrote::FontCollection;

use crate::{Error, OwnedFont};
use crate::{Error, OwnedFont, Stretch, Style, Weight};

pub fn all_fonts() -> Result<Box<[OwnedFont]>, Error> {
let collection = FontCollection::system();
Expand All @@ -20,6 +20,11 @@ pub fn all_fonts() -> Result<Box<[OwnedFont]>, Error> {
family_name: font.family_name(),
font_name: font.face_name(),
path,

// TODO: calculate
style: Style::Normal,
weight: Weight::NORMAL,
stretch: Stretch::NORMAL,
})
}
}
Expand Down
107 changes: 106 additions & 1 deletion font-enumeration/src/fontconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,78 @@ use std::path::PathBuf;

use fontconfig::{Fontconfig, ObjectSet, Pattern};

use crate::{Error, OwnedFont};
use crate::{Error, OwnedFont, Stretch, Style, Weight};

impl Stretch {
fn from_fc(width: i32) -> Self {
match width {
// probably 62.5%
63 => Self::EXTRA_CONDENSED,
// probably 86.5%
87 => Self::SEMI_CONDENSED,
// probably 112.5%
113 => Self::SEMI_EXPANDED,
_ => Self(width as f32 / 100.0),
}
}
}

impl Style {
fn from_fc(slant: i32) -> Self {
match slant {
100 => Self::Italic,
110 => Self::Oblique(None),
_ => Self::Normal,
}
}
}

impl Weight {
fn from_fc(weight: i32) -> Self {
use fontconfig as fc;

const MAPPING: &[(i32, Weight)] = &[
(fc::FC_WEIGHT_THIN, Weight::THIN),
(fc::FC_WEIGHT_THIN, Weight::THIN),
(fc::FC_WEIGHT_EXTRALIGHT, Weight::EXTRA_LIGHT),
(fc::FC_WEIGHT_LIGHT, Weight::LIGHT),
(fc::FC_WEIGHT_BOOK, Weight::SEMI_LIGHT),
(fc::FC_WEIGHT_REGULAR, Weight::NORMAL),
(fc::FC_WEIGHT_MEDIUM, Weight::MEDIUM),
(fc::FC_WEIGHT_DEMIBOLD, Weight::SEMI_BOLD),
(fc::FC_WEIGHT_BOLD, Weight::BOLD),
(fc::FC_WEIGHT_EXTRABOLD, Weight::EXTRA_BOLD),
(fc::FC_WEIGHT_BLACK, Weight::BLACK),
(fc::FC_WEIGHT_EXTRABLACK, Weight::EXTRA_BLACK),
];

for idx in 1..MAPPING.len() {
let (fc_weight_b, ot_weight_b) = MAPPING[idx];

if weight == fc_weight_b {
return ot_weight_b;
}

if weight < fc_weight_b {
let (fc_weight_a, ot_weight_a) = MAPPING[idx - 1];
let fc_weight_a = fc_weight_a as f32;
let fc_weight_b = fc_weight_b as f32;
let ot_weight_a = ot_weight_a.0;
let ot_weight_b = ot_weight_b.0;

let weight = weight as f32;

let ot_weight = ot_weight_a
+ (weight - fc_weight_a) / (fc_weight_a - fc_weight_b)
* (ot_weight_a - ot_weight_b);
return Weight::new(ot_weight);
}
}

// if weight is more than FC_WEIGHT_EXTRABLACK, default to Weight::EXTRA_BLACK
Weight::EXTRA_BLACK
}
}

pub fn all_fonts() -> Result<Box<[OwnedFont]>, Error> {
let fc = Fontconfig::new().ok_or(Error::SystemCollection)?;
Expand All @@ -12,6 +83,9 @@ pub fn all_fonts() -> Result<Box<[OwnedFont]>, Error> {
objects.add(fontconfig::FC_FAMILY);
objects.add(fontconfig::FC_FULLNAME);
objects.add(fontconfig::FC_FILE);
objects.add(fontconfig::FC_SLANT);
objects.add(fontconfig::FC_WEIGHT);
objects.add(fontconfig::FC_WIDTH);
let fonts = fontconfig::list_fonts(&pattern, Some(&objects));

let fonts = fonts
Expand All @@ -21,13 +95,44 @@ pub fn all_fonts() -> Result<Box<[OwnedFont]>, Error> {
let name = font.get_string(fontconfig::FC_FULLNAME).unwrap_or("");
let path = font.get_string(fontconfig::FC_FILE)?;

// is it ok to assume these defaults when the value is missing?
let slant = font.slant().unwrap_or(fontconfig::FC_SLANT_ROMAN);
let weight = font.weight().unwrap_or(fontconfig::FC_WEIGHT_REGULAR);
let width = font.width().unwrap_or(fontconfig::FC_WIDTH_NORMAL);

Some(OwnedFont {
family_name: family.to_owned(),
font_name: name.to_owned(),
path: PathBuf::from(path),
style: Style::from_fc(slant),
weight: Weight::from_fc(weight),
stretch: Stretch::from_fc(width),
})
})
.collect();

Ok(fonts)
}

#[cfg(test)]
mod tests {
#[test]
fn test_weight_conversion() {
use fontconfig as fc;

use crate::Weight;

assert_eq!(Weight::from_fc(fc::FC_WEIGHT_THIN), Weight::THIN);
assert_eq!(Weight::from_fc(1), Weight::new(102.5));
assert_eq!(Weight::from_fc(70), Weight::new(340.));
assert_eq!(
Weight::from_fc(fc::FC_WEIGHT_EXTRABLACK - 1),
Weight::new(940.)
);
assert_eq!(
Weight::from_fc(fc::FC_WEIGHT_EXTRABLACK + 1),
Weight::EXTRA_BLACK
);
assert_eq!(Weight::from_fc(50000), Weight::EXTRA_BLACK);
}
}
Loading

0 comments on commit b6e5732

Please sign in to comment.