1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-29 16:42:13 +03:00

fonts: adopt CSS Fonts Level 3 compatible font matching

This commit is contained in:
Wez Furlong 2021-04-09 14:05:41 -07:00
parent 99fc3ee3cd
commit b006ab923b
5 changed files with 173 additions and 81 deletions

View File

@ -2,7 +2,7 @@
use crate::ftwrap::Library; use crate::ftwrap::Library;
use crate::locator::FontDataSource; use crate::locator::FontDataSource;
use crate::parser::{load_built_in_fonts, parse_and_collect_font_info, FontMatch, ParsedFont}; use crate::parser::{load_built_in_fonts, parse_and_collect_font_info, ParsedFont};
use anyhow::Context; use anyhow::Context;
use config::{Config, FontAttributes}; use config::{Config, FontAttributes};
use rangeset::RangeSet; use rangeset::RangeSet;
@ -50,14 +50,12 @@ impl Entry {
} }
pub struct FontDatabase { pub struct FontDatabase {
by_family: HashMap<String, Vec<Arc<Entry>>>,
by_full_name: HashMap<String, Arc<Entry>>, by_full_name: HashMap<String, Arc<Entry>>,
} }
impl FontDatabase { impl FontDatabase {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
by_family: HashMap::new(),
by_full_name: HashMap::new(), by_full_name: HashMap::new(),
} }
} }
@ -69,13 +67,6 @@ impl FontDatabase {
coverage: Mutex::new(None), coverage: Mutex::new(None),
}); });
if let Some(family) = entry.parsed.names().family.as_ref() {
self.by_family
.entry(family.to_string())
.or_insert_with(Vec::new)
.push(Arc::clone(&entry));
}
self.by_full_name self.by_full_name
.entry(entry.parsed.names().full_name.clone()) .entry(entry.parsed.names().full_name.clone())
.or_insert(entry); .or_insert(entry);
@ -182,23 +173,20 @@ impl FontDatabase {
} }
pub fn resolve(&self, font_attr: &FontAttributes) -> Option<&ParsedFont> { pub fn resolve(&self, font_attr: &FontAttributes) -> Option<&ParsedFont> {
if let Some(entry) = self.by_full_name.get(&font_attr.family) { let candidates: Vec<&ParsedFont> = self
if entry.parsed.matches_attributes(font_attr) == FontMatch::FullName { .by_full_name
return Some(&entry.parsed); .values()
} .filter_map(|entry| {
if entry.parsed.matches_name(font_attr) {
Some(&entry.parsed)
} else {
None
} }
})
.collect();
if let Some(family) = self.by_family.get(&font_attr.family) { if let Some(idx) = ParsedFont::best_matching_index(font_attr, &candidates) {
let mut candidates = vec![]; return candidates.get(idx).map(|&p| p);
for entry in family {
let res = entry.parsed.matches_attributes(font_attr);
if res != FontMatch::NoMatch {
candidates.push((res, entry));
}
}
candidates.sort_by(|a, b| a.0.cmp(&b.0));
let best = candidates.first()?;
return Some(&best.1.parsed);
} }
None None

View File

@ -89,8 +89,7 @@ impl FontLocator for CoreTextFontLocator {
for attr in fonts_selection { for attr in fonts_selection {
if let Ok(descriptor) = descriptor_from_attr(attr) { if let Ok(descriptor) = descriptor_from_attr(attr) {
let handles = handles_from_descriptor(&descriptor); let handles = handles_from_descriptor(&descriptor);
let ranked = ParsedFont::rank_matches(attr, handles); if let Some(parsed) = ParsedFont::best_match(attr, handles) {
for parsed in ranked {
fonts.push(parsed); fonts.push(parsed);
loaded.insert(attr.clone()); loaded.insert(attr.clone());
} }

View File

@ -1,6 +1,6 @@
use crate::fcwrap; use crate::fcwrap;
use crate::locator::{FontDataHandle, FontDataSource, FontLocator}; use crate::locator::{FontDataHandle, FontDataSource, FontLocator};
use crate::parser::{FontMatch, ParsedFont}; use crate::parser::ParsedFont;
use anyhow::Context; use anyhow::Context;
use config::FontAttributes; use config::FontAttributes;
use fcwrap::{to_fc_weight, to_fc_width, CharSet, Pattern as FontPattern}; use fcwrap::{to_fc_weight, to_fc_width, CharSet, Pattern as FontPattern};
@ -98,7 +98,7 @@ impl FontLocator for FontConfigFontLocator {
// so we need to parse the returned font // so we need to parse the returned font
// here to see if we got what we asked for. // here to see if we got what we asked for.
if let Ok(parsed) = crate::parser::ParsedFont::from_locator(&handle) { if let Ok(parsed) = crate::parser::ParsedFont::from_locator(&handle) {
if parsed.matches_attributes(attr) != FontMatch::NoMatch { if parsed.matches_name(attr) {
log::trace!("found font-config match for {:?}", parsed.names()); log::trace!("found font-config match for {:?}", parsed.names());
fonts.push(parsed); fonts.push(parsed);
loaded.insert(attr.clone()); loaded.insert(attr.clone());

View File

@ -1,7 +1,7 @@
#![cfg(windows)] #![cfg(windows)]
use crate::locator::{FontDataSource, FontLocator}; use crate::locator::{FontDataSource, FontLocator};
use crate::parser::{parse_and_collect_font_info, rank_matching_fonts, FontMatch, ParsedFont}; use crate::parser::{best_matching_font, parse_and_collect_font_info, ParsedFont};
use config::{FontAttributes, FontStretch, FontWeight as WTFontWeight}; use config::{FontAttributes, FontStretch, FontWeight as WTFontWeight};
use dwrote::{FontDescriptor, FontStretch, FontStyle, FontWeight}; use dwrote::{FontDescriptor, FontStretch, FontStyle, FontWeight};
use std::borrow::Cow; use std::borrow::Cow;
@ -63,7 +63,7 @@ fn extract_font_data(font: HFONT, attr: &FontAttributes) -> anyhow::Result<Parse
let mut font_info = vec![]; let mut font_info = vec![];
parse_and_collect_font_info(&source, &mut font_info)?; parse_and_collect_font_info(&source, &mut font_info)?;
let matches = ParsedFont::rank_matches(attr, font_info); let matches = ParsedFont::best_match(attr, font_info);
for m in matches { for m in matches {
return Ok(m); return Ok(m);
@ -145,11 +145,9 @@ fn handle_from_descriptor(
log::debug!("{} -> {}", family_name, path.display()); log::debug!("{} -> {}", family_name, path.display());
let source = FontDataSource::OnDisk(path); let source = FontDataSource::OnDisk(path);
match rank_matching_fonts(&source, attr) { match best_matching_font(&source, attr) {
Ok(matches) => { Ok(parsed) => {
for p in matches { return Some(parsed);
return Some(p);
}
} }
Err(err) => log::warn!("While parsing: {:?}: {:#}", source, err), Err(err) => log::warn!("While parsing: {:?}: {:#}", source, err),
} }
@ -176,7 +174,7 @@ impl FontLocator for GdiFontLocator {
fonts: &mut Vec<ParsedFont>, fonts: &mut Vec<ParsedFont>,
loaded: &mut HashSet<FontAttributes>, loaded: &mut HashSet<FontAttributes>,
) -> bool { ) -> bool {
if parsed.matches_attributes(font_attr) != FontMatch::NoMatch { if parsed.matches_name(font_attr) {
fonts.push(parsed); fonts.push(parsed);
loaded.insert(font_attr.clone()); loaded.insert(font_attr.clone());
true true

View File

@ -87,57 +87,164 @@ impl ParsedFont {
self.italic self.italic
} }
pub fn matches_attributes(&self, attr: &FontAttributes) -> FontMatch { pub fn matches_name(&self, attr: &FontAttributes) -> bool {
if let Some(fam) = self.names.family.as_ref() { if let Some(fam) = self.names.family.as_ref() {
if attr.family == *fam { if attr.family == *fam {
if attr.stretch == self.stretch { return true;
let wanted_weight = attr.weight.to_opentype_weight();
let weight = self.weight.to_opentype_weight();
if weight >= wanted_weight {
if attr.italic == self.italic {
return FontMatch::Weight(weight - wanted_weight);
} }
} }
self.matches_full_or_ps_name(attr)
}
pub fn matches_full_or_ps_name(&self, attr: &FontAttributes) -> bool {
if attr.family == self.names.full_name { if attr.family == self.names.full_name {
return FontMatch::FullName; return true;
} }
} if let Some(ps) = self.names.postscript_name.as_ref() {
}
}
if attr.family == self.names.full_name {
FontMatch::FullName
} else if let Some(ps) = self.names.postscript_name.as_ref() {
if attr.family == *ps { if attr.family == *ps {
FontMatch::FullName return true;
} else {
FontMatch::NoMatch
} }
} else {
FontMatch::NoMatch
} }
false
} }
pub fn rank_matches(attr: &FontAttributes, fonts: Vec<Self>) -> Vec<Self> { /// Perform CSS Fonts Level 3 font matching.
let mut candidates = vec![]; /// This implementation is derived from the `find_best_match` function
for p in fonts { /// in the font-kit crate which is
let res = p.matches_attributes(attr); /// Copyright © 2018 The Pathfinder Project Developers.
if res != FontMatch::NoMatch { /// https://drafts.csswg.org/css-fonts-3/#font-style-matching says
candidates.push((res, p)); pub fn best_matching_index<P: std::ops::Deref<Target = Self> + std::fmt::Debug>(
} attr: &FontAttributes,
} fonts: &[P],
candidates.sort_by(|a, b| a.0.cmp(&b.0)); ) -> Option<usize> {
candidates.into_iter().map(|(_, p)| p).collect() if fonts.is_empty() {
} return None;
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] let mut candidates: Vec<usize> = (0..fonts.len()).collect();
pub enum FontMatch {
Weight(u16), // First, filter by stretch
FullName, let stretch_value = attr.stretch.to_opentype_stretch();
NoMatch, let stretch = if candidates
.iter()
.any(|&idx| fonts[idx].stretch == attr.stretch)
{
attr.stretch
} else if attr.stretch <= FontStretch::Normal {
// Find the closest stretch, looking at narrower first before
// looking at wider candidates
match candidates
.iter()
.filter(|&&idx| fonts[idx].stretch < attr.stretch)
.min_by_key(|&&idx| stretch_value - fonts[idx].stretch.to_opentype_stretch())
{
Some(&idx) => fonts[idx].stretch,
None => {
let idx = *candidates.iter().min_by_key(|&&idx| {
fonts[idx].stretch.to_opentype_stretch() - stretch_value
})?;
fonts[idx].stretch
}
}
} else {
// Look at wider values, then narrower values
match candidates
.iter()
.filter(|&&idx| fonts[idx].stretch > attr.stretch)
.min_by_key(|&&idx| fonts[idx].stretch.to_opentype_stretch() - stretch_value)
{
Some(&idx) => fonts[idx].stretch,
None => {
let idx = *candidates.iter().min_by_key(|&&idx| {
stretch_value - fonts[idx].stretch.to_opentype_stretch()
})?;
fonts[idx].stretch
}
}
};
// Reduce to matching stretches
candidates.retain(|&idx| fonts[idx].stretch == stretch);
// Now match style: italics
let styles = [attr.italic, !attr.italic];
let italic = *styles
.iter()
.filter(|&&italic| candidates.iter().any(|&idx| fonts[idx].italic == italic))
.next()?;
// Reduce to matching italics
candidates.retain(|&idx| fonts[idx].italic == italic);
// And now match by font weight
let query_weight = attr.weight.to_opentype_weight();
let weight = if candidates
.iter()
.any(|&idx| fonts[idx].weight == attr.weight)
{
// Exact match for the requested weight
attr.weight
} else if attr.weight == FontWeight::Regular
&& candidates
.iter()
.any(|&idx| fonts[idx].weight == FontWeight::Medium)
{
// https://drafts.csswg.org/css-fonts-3/#font-style-matching says
// that if they want weight=400 and we don't have 400,
// look at weight 500 first
FontWeight::Medium
} else if attr.weight == FontWeight::Medium
&& candidates
.iter()
.any(|&idx| fonts[idx].weight == FontWeight::Regular)
{
// Similarly, look at regular before Medium if they wanted
// Medium and we didn't have it
FontWeight::Regular
} else if attr.weight <= FontWeight::Medium {
// Find best lighter weight, else best heavier weight
match candidates
.iter()
.filter(|&&idx| fonts[idx].weight <= attr.weight)
.min_by_key(|&&idx| query_weight - fonts[idx].weight.to_opentype_weight())
{
Some(&idx) => fonts[idx].weight,
None => {
let idx = *candidates.iter().min_by_key(|&&idx| {
fonts[idx].weight.to_opentype_weight() - query_weight
})?;
fonts[idx].weight
}
}
} else {
// Find best heavier weight, else best lighter weight
match candidates
.iter()
.filter(|&&idx| fonts[idx].weight >= attr.weight)
.min_by_key(|&&idx| fonts[idx].weight.to_opentype_weight() - query_weight)
{
Some(&idx) => fonts[idx].weight,
None => {
let idx = *candidates.iter().min_by_key(|&&idx| {
query_weight - fonts[idx].weight.to_opentype_weight()
})?;
fonts[idx].weight
}
}
};
// Reduce to matching weight
candidates.retain(|&idx| fonts[idx].weight == weight);
// The first one in this set is our best match
candidates.into_iter().next()
}
pub fn best_match(attr: &FontAttributes, mut fonts: Vec<Self>) -> Option<Self> {
let refs: Vec<&Self> = fonts.iter().collect();
let idx = Self::best_matching_index(attr, &refs)?;
fonts.drain(idx..=idx).next()
}
} }
/// In case the user has a broken configuration, or no configuration, /// In case the user has a broken configuration, or no configuration,
@ -186,13 +293,13 @@ pub(crate) fn load_built_in_fonts(font_info: &mut Vec<ParsedFont>) -> anyhow::Re
Ok(()) Ok(())
} }
pub fn rank_matching_fonts( pub fn best_matching_font(
source: &FontDataSource, source: &FontDataSource,
font_attr: &FontAttributes, font_attr: &FontAttributes,
) -> anyhow::Result<Vec<ParsedFont>> { ) -> anyhow::Result<Option<ParsedFont>> {
let mut font_info = vec![]; let mut font_info = vec![];
parse_and_collect_font_info(source, &mut font_info)?; parse_and_collect_font_info(source, &mut font_info)?;
Ok(ParsedFont::rank_matches(font_attr, font_info)) Ok(ParsedFont::best_match(font_attr, font_info))
} }
pub(crate) fn parse_and_collect_font_info( pub(crate) fn parse_and_collect_font_info(