blob: e6370dda9c3aeaf30d8e83be2c5ef24e2a9eed88 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
super::typeface::TypefaceAndLangScore,
fidl_fuchsia_fonts::{
self as fonts, Slant, Style2, TypefaceQuery, Width, WEIGHT_MEDIUM, WEIGHT_NORMAL,
},
lazy_static::lazy_static,
};
lazy_static! {
/// The default style (or its individual properties) are is applied to fill any style properties
/// that are missing from a query passed to the matcher.
static ref DEFAULT_STYLE: Style2 = Style2 {
slant: Some(fonts::DEFAULT_SLANT),
weight: Some(fonts::DEFAULT_WEIGHT),
width: Some(fonts::DEFAULT_WIDTH),
..Style2::EMPTY
};
}
/// Selects between typefaces `a` and `b` for the `request`. Typefaces are passed in
/// `TypefaceAndLangScore` so the language match score is calculated only once for each typeface. If
/// `a` and `b` are equivalent then `a` is returned.
///
/// The style matching logic follows the CSS3 Fonts spec (see
/// [Section 5.2, Item 4](https://www.w3.org/TR/css-fonts-3/#font-style-matching) with two
/// additions:
///
/// 1. Typefaces with a higher language match score are preferred. The score value is expected to
/// be pre-calculated by `get_lang_match_score()`. Note that if the request specifies a code
/// point then the typefaces are expected to be already filtered based on that code point, i.e.
/// they both contain that character, so this function doesn't need to verify it.
/// 2. If the request specifies a `fallback_family`, then fonts with the same `fallback_family`
/// are preferred.
/// 3. The matcher supports any integer font weight in the range `[1, 1000]`, and not just
/// multiples of 100 as in CSS3. Therefore, if the request specifies a weight between 400
/// (normal) and 500 (medium), inclusive, then then the algorithm used is from
/// [CSS _4_, Section 5.2](https://www.w3.org/TR/css-fonts-4/#font-style-matching), Item 4.3,
/// first bullet.
pub fn select_best_match<'a, 'b>(
a: TypefaceAndLangScore<'a>,
b: TypefaceAndLangScore<'a>,
query: &'b TypefaceQuery,
) -> TypefaceAndLangScore<'a> {
if a.lang_score != b.lang_score {
if a.lang_score < b.lang_score {
return a;
} else {
return b;
}
}
if let Some(fallback_family) = query.fallback_family {
if a.typeface.generic_family != b.typeface.generic_family {
if a.typeface.generic_family == Some(fallback_family) {
return a;
} else if b.typeface.generic_family == Some(fallback_family) {
return b;
}
// If `generic_family` of `a` and `b` doesn't match the request, then fall through to
// compare them based on style parameters.
}
}
let query_style = query.style.as_ref().unwrap_or(&*DEFAULT_STYLE);
// Select based on width, see CSS3 Section 5.2, Item 4.a.
let query_width = query_style.width.unwrap_or(fonts::DEFAULT_WIDTH);
if a.typeface.width != b.typeface.width {
// Reorder a and b, so a has lower width.
let (a, b) = if a.typeface.width > b.typeface.width { (b, a) } else { (a, b) };
if query_width <= Width::Normal {
if b.typeface.width <= query_width {
return b;
} else {
return a;
}
} else {
if a.typeface.width >= query_width {
return a;
} else {
return b;
}
}
}
// Select based on slant, CSS3 Section 5.2, Item 4.b.
let query_slant = query_style.slant.unwrap_or(fonts::DEFAULT_SLANT);
match (query_slant, a.typeface.slant, b.typeface.slant) {
// If both fonts have the same slant then fall through to select based
// on weight.
(_, a_s, b_s) if a_s == b_s => (),
// If we have a font that exactly matches the request's slant, then use it.
(r_s, a_s, _) if r_s == a_s => return a,
(r_s, _, b_s) if r_s == b_s => return b,
// If an italic or oblique font is requested, pick whichever of italic or oblique is
// available.
(Slant::Italic, Slant::Oblique, _) => return a,
(Slant::Italic, _, Slant::Oblique) => return b,
(Slant::Oblique, Slant::Italic, _) => return a,
(Slant::Oblique, _, Slant::Italic) => return b,
// If an upright font is requested, but we have only italic and oblique, then fall through
// to select based on weight.
//
// Technically, we could strictly follow "normal faces are checked first, then oblique
// faces, then italic faces" in this case, but "User agents are permitted to distinguish
// between italic and oblique faces within platform font families but this is not required."
// A better matching weight seems more important.
(Slant::Upright, _, _) => (),
// Patterns above cover all possible inputs, but exhaustiveness
// checker doesn't see it.
_ => (),
}
// Select based on weight, CSS3 Section 5.2, Item 4.c.
let query_weight = query_style.weight.unwrap_or(fonts::DEFAULT_WEIGHT);
if a.typeface.weight != b.typeface.weight {
// Reorder a and b, so a has lower weight.
let ordered = if a.typeface.weight > b.typeface.weight { (b, a) } else { (a, b) };
let (a, b) = ordered;
if a.typeface.weight == query_weight {
return a;
}
if b.typeface.weight == query_weight {
return b;
}
if query_weight < WEIGHT_NORMAL {
// If query_weight < 400, then typefaces with weights <= query_weight are
// preferred.
if b.typeface.weight <= query_weight {
return b;
} else {
return a;
}
} else if query_weight > WEIGHT_MEDIUM {
// If query_weight > 500, then typefaces with weights >= query_weight are
// preferred.
if a.typeface.weight >= query_weight {
return a;
} else {
return b;
}
} else {
// This case is not adequately covered in CSS3, which only deals with weights in
// multiples of 100. Since we support units of 1, we have to resort to CSS4
// Section 5.2, Item 4.3, first bullet.
// If query_weight is between 400 and 500, inclusive, then the preferred intervals are
// 1. `(query_weight, 500]`, in ascending order
// 2. `[1, query_weight)`, in descending order
// 3. `[501, 1000]`, in ascending order
if b.typeface.weight <= WEIGHT_MEDIUM {
if a.typeface.weight > query_weight {
// q...a...b...500
return a;
} else {
// a...b...q...500 OR a...q...b...500
return b;
}
} else {
return a;
}
}
}
// If a and b are equivalent then give priority according to the order in the manifest.
a
}
#[cfg(test)]
mod tests {
use {
super::super::{test_util::*, Typeface},
super::*,
fidl_fuchsia_fonts::{
GenericFontFamily, Slant, TypefaceRequestFlags, Width, WEIGHT_BOLD, WEIGHT_EXTRA_BOLD,
WEIGHT_EXTRA_LIGHT, WEIGHT_LIGHT, WEIGHT_MEDIUM, WEIGHT_NORMAL, WEIGHT_SEMI_BOLD,
WEIGHT_THIN,
},
};
/// Substitute for `TypefaceAndLangScore` that owns its typeface. Convert to a
/// `TypefaceAndLangScore` using `(&self).into()`.
struct TypefaceAndLangScoreWrapper {
typeface: Typeface,
lang_score: usize,
}
impl From<Typeface> for TypefaceAndLangScoreWrapper {
fn from(typeface: Typeface) -> Self {
TypefaceAndLangScoreWrapper { typeface, lang_score: 0 }
}
}
impl<'a> From<&'a Typeface> for TypefaceAndLangScore<'a> {
fn from(typeface: &'a Typeface) -> Self {
TypefaceAndLangScore { typeface, lang_score: 0 }
}
}
impl<'a> From<&'a TypefaceAndLangScoreWrapper> for TypefaceAndLangScore<'a> {
fn from(source: &'a TypefaceAndLangScoreWrapper) -> TypefaceAndLangScore<'a> {
TypefaceAndLangScore { typeface: &source.typeface, lang_score: source.lang_score }
}
}
fn make_fake_typeface_and_lang_score(
width: Width,
slant: Slant,
weight: u16,
lang_score: usize,
) -> TypefaceAndLangScoreWrapper {
TypefaceAndLangScoreWrapper {
typeface: make_fake_typeface_style(width, slant, weight),
lang_score,
}
}
/// Lang score is more important than everything else.
#[test]
fn select_best_match_lang_score_over_others() {
let better_lang_typeface =
make_fake_typeface_and_lang_score(Width::Condensed, Slant::Italic, WEIGHT_MEDIUM, 3);
let worse_lang_typeface = make_fake_typeface_and_lang_score(
Width::UltraExpanded,
Slant::Upright,
WEIGHT_LIGHT,
6,
);
assert_eq!(
select_best_match(
(&better_lang_typeface).into(),
(&worse_lang_typeface).into(),
make_style_request(Width::UltraExpanded, Slant::Upright, WEIGHT_LIGHT, false)
.query
.as_ref()
.unwrap()
)
.typeface,
&better_lang_typeface.typeface
);
}
/// Generic family is more important than style.
#[test]
fn test_fallback_generic_family_over_style() {
let serif_typeface = make_fake_typeface(
Width::Condensed,
Slant::Italic,
WEIGHT_MEDIUM,
&vec![],
&vec![],
GenericFontFamily::Serif,
);
let fantasy_typeface = make_fake_typeface(
Width::UltraExpanded,
Slant::Upright,
WEIGHT_LIGHT,
&vec![],
&vec![],
GenericFontFamily::Fantasy,
);
let request = make_typeface_request(
Width::Condensed,
Slant::Italic,
WEIGHT_MEDIUM,
None,
TypefaceRequestFlags::empty(),
GenericFontFamily::Fantasy,
);
assert_eq!(
select_best_match(
(&serif_typeface).into(),
(&fantasy_typeface).into(),
request.query.as_ref().unwrap()
)
.typeface,
&fantasy_typeface
);
}
/// Calls `select_best_match` for the given type faces with the given simplified style request.
fn compare_for_request<'a>(
a: &'a Typeface,
b: &'a Typeface,
width: impl Into<Option<Width>>,
slant: impl Into<Option<Slant>>,
weight: impl Into<Option<u16>>,
) -> &'a Typeface {
let request = make_style_request(width, slant, weight, false);
select_best_match(a.into(), b.into(), request.query.as_ref().unwrap()).typeface
}
/// Width is more important than other style parameters.
#[test]
fn select_best_match_width_over_other_style() {
let extra_condensed_upright_semi_bold =
make_fake_typeface_style(Width::ExtraCondensed, Slant::Upright, WEIGHT_SEMI_BOLD);
let condensed_italic_thin =
make_fake_typeface_style(Width::Condensed, Slant::Italic, WEIGHT_THIN);
assert_eq!(
compare_for_request(
&extra_condensed_upright_semi_bold,
&condensed_italic_thin,
Width::Condensed,
Slant::Upright,
WEIGHT_SEMI_BOLD
)
.width,
Width::Condensed
);
}
/// Uses default width value when omitted.
#[test]
fn select_best_match_width_default() {
let extra_condensed_upright_semi_bold =
make_fake_typeface_style(Width::ExtraCondensed, Slant::Upright, WEIGHT_SEMI_BOLD);
let normal_italic_thin =
make_fake_typeface_style(Width::Normal, Slant::Italic, WEIGHT_THIN);
let extra_expanded_oblique_normal =
make_fake_typeface_style(Width::ExtraExpanded, Slant::Oblique, WEIGHT_NORMAL);
assert_eq!(
compare_for_request(
&extra_condensed_upright_semi_bold,
&normal_italic_thin,
None,
Slant::Italic,
WEIGHT_NORMAL
)
.width,
Width::Normal
);
assert_eq!(
compare_for_request(
&normal_italic_thin,
&extra_expanded_oblique_normal,
None,
Slant::Italic,
WEIGHT_NORMAL
)
.width,
Width::Normal
);
}
fn make_width_style(width: Width) -> Typeface {
make_fake_typeface_style(width, Slant::Upright, WEIGHT_NORMAL)
}
/// For requested width <= Normal (5), the priority is lower widths descending, then higher
/// widths ascending.
#[test]
fn select_best_match_width_normal_or_condensed_prefers_lower_widths() {
assert_eq!(
compare_for_request(
&make_width_style(Width::UltraCondensed),
&make_width_style(Width::Condensed),
Width::ExtraCondensed,
Slant::Upright,
WEIGHT_NORMAL
)
.width,
Width::UltraCondensed
);
assert_eq!(
compare_for_request(
&make_width_style(Width::Condensed),
&make_width_style(Width::Normal),
Width::SemiCondensed,
Slant::Upright,
WEIGHT_NORMAL
)
.width,
Width::Condensed
);
// This makes no sense as a user, but it's what the spec apparently dictates.
assert_eq!(
compare_for_request(
&make_width_style(Width::UltraCondensed),
&make_width_style(Width::SemiExpanded),
Width::Normal,
Slant::Upright,
WEIGHT_NORMAL
)
.width,
Width::UltraCondensed
);
}
/// For requested width >= SemiExpanded (6), the priority is higher widths ascending, then lower
/// widths descending.
#[test]
fn select_best_match_width_expanded_prefers_higher_widths() {
assert_eq!(
compare_for_request(
&make_width_style(Width::Normal),
&make_width_style(Width::Expanded),
Width::SemiExpanded,
Slant::Upright,
WEIGHT_NORMAL
)
.width,
Width::Expanded
);
assert_eq!(
compare_for_request(
&make_width_style(Width::Expanded),
&make_width_style(Width::UltraExpanded),
Width::ExtraExpanded,
Slant::Upright,
WEIGHT_NORMAL
)
.width,
Width::UltraExpanded
);
assert_eq!(
compare_for_request(
&make_width_style(Width::ExtraExpanded),
&make_width_style(Width::UltraExpanded),
Width::Expanded,
Slant::Upright,
WEIGHT_NORMAL
)
.width,
Width::ExtraExpanded
);
// Follow the spec, not common sense.
assert_eq!(
compare_for_request(
&make_width_style(Width::Normal),
&make_width_style(Width::UltraExpanded),
Width::SemiExpanded,
Slant::Upright,
WEIGHT_NORMAL
)
.width,
Width::UltraExpanded
);
}
/// Slant is more important than weight.
#[test]
fn select_best_match_slant_vs_weight() {
let italic_thin = make_fake_typeface_style(Width::Normal, Slant::Italic, WEIGHT_THIN);
let upright_semi_bold =
make_fake_typeface_style(Width::Normal, Slant::Upright, WEIGHT_SEMI_BOLD);
let oblique_normal = make_fake_typeface_style(Width::Normal, Slant::Oblique, WEIGHT_NORMAL);
assert_eq!(
compare_for_request(
&italic_thin,
&upright_semi_bold,
Width::Condensed,
Slant::Upright,
WEIGHT_THIN
)
.slant,
Slant::Upright
);
assert_eq!(
compare_for_request(
&upright_semi_bold,
&oblique_normal,
Width::Condensed,
Slant::Upright,
WEIGHT_NORMAL
)
.slant,
Slant::Upright
);
}
/// Uses default slant value when omitted.
#[test]
fn select_best_match_slant_default() {
let italic_thin = make_fake_typeface_style(Width::Normal, Slant::Italic, WEIGHT_THIN);
let upright_semi_bold =
make_fake_typeface_style(Width::Normal, Slant::Upright, WEIGHT_SEMI_BOLD);
let oblique_normal = make_fake_typeface_style(Width::Normal, Slant::Oblique, WEIGHT_NORMAL);
assert_eq!(
compare_for_request(
&italic_thin,
&upright_semi_bold,
Width::Condensed,
None,
WEIGHT_THIN
)
.slant,
Slant::Upright
);
assert_eq!(
compare_for_request(
&upright_semi_bold,
&oblique_normal,
Width::Condensed,
None,
WEIGHT_NORMAL
)
.slant,
Slant::Upright
);
}
fn make_slant_style(slant: Slant) -> Typeface {
make_fake_typeface_style(Width::Normal, slant, WEIGHT_NORMAL)
}
#[test]
fn select_best_match_slant_exact_match() {
assert_eq!(
compare_for_request(
&make_slant_style(Slant::Upright),
&make_slant_style(Slant::Oblique),
Width::Normal,
Slant::Upright,
WEIGHT_NORMAL
)
.slant,
Slant::Upright
);
assert_eq!(
compare_for_request(
&make_slant_style(Slant::Italic),
&make_slant_style(Slant::Oblique),
Width::Normal,
Slant::Oblique,
WEIGHT_NORMAL
)
.slant,
Slant::Oblique
);
assert_eq!(
compare_for_request(
&make_slant_style(Slant::Italic),
&make_slant_style(Slant::Oblique),
Width::Normal,
Slant::Italic,
WEIGHT_NORMAL
)
.slant,
Slant::Italic
);
}
/// Oblique can be substituted for italic and vice versa.
#[test]
fn select_best_match_slant_substitutes() {
assert_eq!(
compare_for_request(
&make_slant_style(Slant::Upright),
&make_slant_style(Slant::Oblique),
Width::Normal,
Slant::Italic,
WEIGHT_NORMAL
)
.slant,
Slant::Oblique
);
assert_eq!(
compare_for_request(
&make_slant_style(Slant::Upright),
&make_slant_style(Slant::Italic),
Width::Normal,
Slant::Oblique,
WEIGHT_NORMAL
)
.slant,
Slant::Italic
);
}
/// Requesting upright when it's unavailable means fall through to weight.
#[test]
fn select_best_match_slant_no_upright() {
assert_eq!(
compare_for_request(
&make_fake_typeface_style(Width::Normal, Slant::Italic, WEIGHT_THIN),
&make_fake_typeface_style(Width::Normal, Slant::Oblique, WEIGHT_BOLD),
Width::Normal,
Slant::Upright,
WEIGHT_BOLD
)
.weight,
WEIGHT_BOLD
);
}
fn make_weight_style(weight: u16) -> Typeface {
make_fake_typeface_style(Width::Normal, Slant::Upright, weight)
}
/// Uses default weight value when omitted.
#[test]
fn select_best_match_weight_default() {
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_BOLD),
&make_weight_style(WEIGHT_NORMAL),
Width::Normal,
Slant::Upright,
None
)
.weight,
WEIGHT_NORMAL
);
}
#[test]
fn select_best_match_weight_exact_match() {
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_BOLD),
&make_weight_style(WEIGHT_NORMAL),
Width::Normal,
Slant::Upright,
WEIGHT_BOLD
)
.weight,
WEIGHT_BOLD
);
assert_eq!(
compare_for_request(
&make_weight_style(517),
&make_weight_style(WEIGHT_THIN),
Width::Normal,
Slant::Upright,
517
)
.weight,
517
);
}
/// For requested weight < `WEIGHT_NORMAL`, the priority is lower weights descending, then
/// higher weights ascending.
#[test]
fn select_best_match_weight_thinner_prefers_thinner() {
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_THIN),
&make_weight_style(WEIGHT_EXTRA_LIGHT),
Width::Normal,
Slant::Upright,
WEIGHT_LIGHT
)
.weight,
WEIGHT_EXTRA_LIGHT
);
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_THIN),
&make_weight_style(WEIGHT_LIGHT),
Width::Normal,
Slant::Upright,
WEIGHT_EXTRA_LIGHT
)
.weight,
WEIGHT_THIN
);
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_SEMI_BOLD),
&make_weight_style(WEIGHT_EXTRA_BOLD),
Width::Normal,
Slant::Upright,
WEIGHT_LIGHT
)
.weight,
WEIGHT_SEMI_BOLD
);
}
/// For requested weight > `WEIGHT_MEDIUM`, the priority is higher weights ascending, then lower
/// weights descending.
#[test]
fn select_best_match_weight_thicker_prefers_thicker() {
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_SEMI_BOLD),
&make_weight_style(WEIGHT_EXTRA_BOLD),
Width::Normal,
Slant::Upright,
WEIGHT_BOLD
)
.weight,
WEIGHT_EXTRA_BOLD
);
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_THIN),
&make_weight_style(WEIGHT_NORMAL),
Width::Normal,
Slant::Upright,
WEIGHT_EXTRA_BOLD
)
.weight,
WEIGHT_NORMAL
);
}
/// For requested weight `WEIGHT_NORMAL`, `WEIGHT_MEDIUM` is preferred, followed by lower
/// weights.
#[test]
fn select_best_match_weight_normal_prefers_medium_then_thinner() {
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_MEDIUM),
&make_weight_style(WEIGHT_LIGHT),
Width::Normal,
Slant::Upright,
WEIGHT_NORMAL
)
.weight,
WEIGHT_MEDIUM
);
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_BOLD),
&make_weight_style(WEIGHT_EXTRA_LIGHT),
Width::Normal,
Slant::Upright,
WEIGHT_NORMAL
)
.weight,
WEIGHT_EXTRA_LIGHT
);
}
/// For requested weight `WEIGHT_MEDIUM`, `WEIGHT_NORMAL` is preferred, followed by lower
/// weights.
#[test]
fn select_best_match_weight_medium_prefers_normal_then_thinner() {
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_NORMAL),
&make_weight_style(WEIGHT_EXTRA_BOLD),
Width::Normal,
Slant::Upright,
WEIGHT_MEDIUM
)
.weight,
WEIGHT_NORMAL
);
assert_eq!(
compare_for_request(
&make_weight_style(WEIGHT_LIGHT),
&make_weight_style(WEIGHT_BOLD),
Width::Normal,
Slant::Upright,
WEIGHT_MEDIUM
)
.weight,
WEIGHT_LIGHT
);
}
/// For requested weight between `WEIGHT_NORMAL` and `WEIGHT_MEDIUM`, inclusive, weights above
/// the requested <= 500 are preferred, then below the requested descending, then above 500
/// ascending.
#[test]
fn select_best_match_weight_between_normal_and_medium() {
assert_eq!(
compare_for_request(
&make_weight_style(440),
&make_weight_style(460),
Width::Normal,
Slant::Upright,
450
)
.weight,
460
);
assert_eq!(
compare_for_request(
&make_weight_style(440),
&make_weight_style(500),
Width::Normal,
Slant::Upright,
450
)
.weight,
500
);
assert_eq!(
compare_for_request(
&make_weight_style(440),
&make_weight_style(501),
Width::Normal,
Slant::Upright,
450
)
.weight,
440
);
assert_eq!(
compare_for_request(
&make_weight_style(300),
&make_weight_style(501),
Width::Normal,
Slant::Upright,
450
)
.weight,
300
);
assert_eq!(
compare_for_request(
&make_weight_style(413),
&make_weight_style(449),
Width::Normal,
Slant::Upright,
450
)
.weight,
449
);
}
}