blob: 9e32b0408cfa79c72ed17a796b2d58938bea58cb [file] [log] [blame]
// Copyright 2018 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::{
matcher::select_best_match,
typeface::{Typeface, TypefaceAndLangScore},
},
crate::font_service::font_info::CharSet,
failure::{format_err, Error},
fidl_fuchsia_fonts::{Style2, TypefaceQuery, TypefaceRequest, TypefaceRequestFlags},
std::sync::Arc,
};
fn unwrap_query<'a>(query: &'a Option<TypefaceQuery>) -> Result<&'a TypefaceQuery, Error> {
query.as_ref().ok_or(format_err!("Missing query"))
}
#[derive(Debug)]
pub struct Collection {
/// Some typefaces may be in more than one collection. In particular, fallback typefaces are
/// added to the family collection and also to the fallback collection.
pub faces: Vec<Arc<Typeface>>,
}
impl Collection {
pub fn new() -> Collection {
Collection { faces: vec![] }
}
pub fn match_request<'a, 'b>(
&'a self,
request: &'b TypefaceRequest,
) -> Result<Option<&'a Typeface>, Error> {
let query = unwrap_query(&request.query)?;
if let Some(flags) = request.flags {
if is_style_defined(query) && flags.contains(TypefaceRequestFlags::ExactStyle) {
return Ok(self
.faces
.iter()
.find(|typeface| {
does_style_match(&query, typeface)
&& (query.languages.as_ref().map_or(true, |value| {
value.is_empty()
|| value
.iter()
.find(|lang| typeface.languages.contains(&*lang.id))
.is_some()
}))
&& do_code_points_match(&typeface.char_set, &query.code_points)
})
.map(|f| f as &Typeface));
}
}
fn is_style_defined(query: &TypefaceQuery) -> bool {
if let Some(style) = &query.style {
style.width.is_some() && style.weight.is_some() && style.slant.is_some()
} else {
false
}
}
/// Returns true if the `typeface` has the same style values as the query. `query.style`
/// must be present.
fn does_style_match(query: &TypefaceQuery, typeface: &Typeface) -> bool {
query.style
== Some(Style2 {
width: Some(typeface.width),
weight: Some(typeface.weight),
slant: Some(typeface.slant),
})
}
/// Returns true if there are no code points in the query, or if all of the code points are
/// present in the `char_set`.
fn do_code_points_match(char_set: &CharSet, query_code_points: &Option<Vec<u32>>) -> bool {
match &query_code_points {
Some(code_points) => {
code_points.iter().all(|code_point| char_set.contains(*code_point))
}
None => true,
}
}
fn fold<'a, 'b>(
best: Option<TypefaceAndLangScore<'a>>,
x: &'a Typeface,
request: &'b TypefaceRequest,
) -> Result<Option<TypefaceAndLangScore<'a>>, Error> {
let x = TypefaceAndLangScore::new(x, request);
match best {
Some(b) => Ok(Some(select_best_match(b, x, unwrap_query(&request.query)?))),
None => Ok(Some(x)),
}
}
Ok(self
.faces
.iter()
.filter(|f| do_code_points_match(&f.char_set, &query.code_points))
.try_fold(None, |best, x| fold(best, x, request))?
.map(|a| a.typeface))
}
pub fn is_empty(&self) -> bool {
self.faces.is_empty()
}
pub fn add_typeface(&mut self, typeface: Arc<Typeface>) {
self.faces.push(typeface);
}
pub fn get_styles<'a>(&'a self) -> impl Iterator<Item = Style2> + 'a {
self.faces.iter().map(|f| Style2 {
width: Some(f.width),
slant: Some(f.slant),
weight: Some(f.weight),
})
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::font_service::manifest,
fidl_fuchsia_fonts::{
GenericFontFamily, Slant, Width, WEIGHT_BOLD, WEIGHT_EXTRA_BOLD, WEIGHT_EXTRA_LIGHT,
WEIGHT_LIGHT, WEIGHT_MEDIUM, WEIGHT_NORMAL, WEIGHT_SEMI_BOLD, WEIGHT_THIN,
},
fidl_fuchsia_intl::LocaleId,
};
fn make_fake_typeface_collection(mut faces: Vec<Typeface>) -> Collection {
let mut result = Collection::new();
for (i, mut typeface) in faces.drain(..).enumerate() {
// Assign fake asset_id to each font
typeface.asset_id = i as u32;
result.add_typeface(Arc::new(typeface));
}
result
}
fn make_fake_typeface(
width: Width,
slant: Slant,
weight: u16,
languages: &[&str],
char_set: &[u32],
generic_family: Option<GenericFontFamily>,
) -> Typeface {
// Prevent error if char_set is empty
let char_set = if char_set.is_empty() { &[0] } else { char_set };
Typeface::new(
0,
manifest::Font {
asset: std::path::PathBuf::new(),
index: 0,
slant,
weight,
width,
languages: languages.iter().map(|s| s.to_string()).collect(),
code_points: CharSet::new(char_set.to_vec()),
package: None,
},
generic_family,
)
.unwrap() // Safe because char_set is not empty
}
fn request_typeface<'a, 'b>(
collection: &'a Collection,
width: Width,
slant: Slant,
weight: u16,
languages: Option<&'b [&'b str]>,
flags: TypefaceRequestFlags,
fallback_family: Option<GenericFontFamily>,
) -> Result<Option<&'a Typeface>, Error> {
let request = TypefaceRequest {
query: Some(TypefaceQuery {
family: None,
style: Some(Style2 {
weight: Some(weight),
width: Some(width),
slant: Some(slant),
}),
code_points: None,
languages: languages
.map(|l| l.iter().map(|s| LocaleId { id: s.to_string() }).collect()),
fallback_family,
}),
flags: Some(flags),
};
collection.match_request(&request)
}
fn make_fake_font_style(width: Width, slant: Slant, weight: u16) -> Typeface {
make_fake_typeface(width, slant, weight, &[], &[], None)
}
fn request_style(
collection: &Collection,
width: Width,
slant: Slant,
weight: u16,
) -> Result<&Typeface, Error> {
request_typeface(
collection,
width,
slant,
weight,
None,
TypefaceRequestFlags::empty(),
None,
)?
.ok_or(format_err!("No typeface found for style"))
}
#[test]
fn test_font_matching_width() -> Result<(), Error> {
let collection = make_fake_typeface_collection(vec![
make_fake_font_style(Width::ExtraCondensed, Slant::Upright, WEIGHT_SEMI_BOLD),
make_fake_font_style(Width::Condensed, Slant::Italic, WEIGHT_THIN),
make_fake_font_style(Width::ExtraExpanded, Slant::Oblique, WEIGHT_NORMAL),
]);
// width is more important than other style parameters.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Italic, WEIGHT_NORMAL)?.width,
Width::Condensed
);
// For width <= Normal (5) lower widths are preferred.
assert_eq!(
request_style(&collection, Width::ExtraCondensed, Slant::Italic, WEIGHT_NORMAL)?.width,
Width::ExtraCondensed
);
assert_eq!(
request_style(&collection, Width::SemiCondensed, Slant::Italic, WEIGHT_NORMAL)?.width,
Width::Condensed
);
// For width > SemiCondensed (4) higher widths are preferred.
assert_eq!(
request_style(&collection, Width::SemiExpanded, Slant::Italic, WEIGHT_NORMAL)?.width,
Width::ExtraExpanded
);
// Otherwise expect font with the closest width.
assert_eq!(
request_style(&collection, Width::UltraCondensed, Slant::Italic, WEIGHT_NORMAL)?.width,
Width::ExtraCondensed
);
assert_eq!(
request_style(&collection, Width::UltraExpanded, Slant::Italic, WEIGHT_NORMAL)?.width,
Width::ExtraExpanded
);
Ok(())
}
#[test]
fn test_font_matching_slant() -> Result<(), Error> {
let collection = make_fake_typeface_collection(vec![
make_fake_font_style(Width::Normal, Slant::Upright, WEIGHT_SEMI_BOLD),
make_fake_font_style(Width::Normal, Slant::Italic, WEIGHT_THIN),
make_fake_font_style(Width::Normal, Slant::Oblique, WEIGHT_NORMAL),
]);
// slant is more important than weight.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_NORMAL)?.slant,
Slant::Upright
);
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Italic, WEIGHT_NORMAL)?.slant,
Slant::Italic
);
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Oblique, WEIGHT_NORMAL)?.slant,
Slant::Oblique
);
let collection = make_fake_typeface_collection(vec![
make_fake_font_style(Width::Normal, Slant::Upright, WEIGHT_SEMI_BOLD),
make_fake_font_style(Width::Normal, Slant::Oblique, WEIGHT_NORMAL),
]);
// Oblique is selected when Italic is requested.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Italic, WEIGHT_NORMAL)?.slant,
Slant::Oblique
);
Ok(())
}
#[test]
fn test_font_matching_weight() -> Result<(), Error> {
let collection = make_fake_typeface_collection(vec![
make_fake_font_style(Width::Normal, Slant::Upright, WEIGHT_BOLD),
make_fake_font_style(Width::Normal, Slant::Upright, WEIGHT_EXTRA_LIGHT),
make_fake_font_style(Width::Normal, Slant::Upright, WEIGHT_NORMAL),
]);
// Exact match.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_EXTRA_LIGHT)?
.weight,
WEIGHT_EXTRA_LIGHT
);
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_NORMAL)?.weight,
WEIGHT_NORMAL
);
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_BOLD)?.weight,
WEIGHT_BOLD
);
// For weight < WEIGHT_NORMAL lower weights are preferred.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_LIGHT)?.weight,
WEIGHT_EXTRA_LIGHT
);
// For weight > WEIGHT_MEDIUM higher weights are preferred.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_SEMI_BOLD)?.weight,
WEIGHT_BOLD
);
// For request.weight = WEIGHT_MEDIUM the font with weight == WEIGHT_NORMAL is preferred.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_MEDIUM)?.weight,
WEIGHT_NORMAL
);
// Otherwise expect font with the closest weight.
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_THIN)?.weight,
WEIGHT_EXTRA_LIGHT
);
assert_eq!(
request_style(&collection, Width::Condensed, Slant::Upright, WEIGHT_EXTRA_BOLD)?.weight,
WEIGHT_BOLD
);
Ok(())
}
fn request_style_exact(
collection: &Collection,
width: Width,
slant: Slant,
weight: u16,
) -> Result<Option<&Typeface>, Error> {
request_typeface(
collection,
width,
slant,
weight,
None,
TypefaceRequestFlags::ExactStyle,
None,
)
}
#[test]
fn test_font_matching_exact() -> Result<(), Error> {
let collection = make_fake_typeface_collection(vec![
make_fake_font_style(Width::ExtraCondensed, Slant::Upright, WEIGHT_SEMI_BOLD),
make_fake_font_style(Width::Condensed, Slant::Italic, WEIGHT_THIN),
make_fake_font_style(Width::ExtraExpanded, Slant::Oblique, WEIGHT_NORMAL),
]);
assert_eq!(
request_style_exact(&collection, Width::Condensed, Slant::Italic, WEIGHT_THIN)?
.ok_or_else(|| format_err!("Exact style not found"))?
.asset_id,
1
);
assert!(request_style_exact(
&collection,
Width::SemiCondensed,
Slant::Italic,
WEIGHT_THIN
)?
.is_none());
assert!(request_style_exact(&collection, Width::Condensed, Slant::Upright, WEIGHT_THIN)?
.is_none());
assert!(request_style_exact(&collection, Width::Condensed, Slant::Italic, WEIGHT_LIGHT)?
.is_none());
Ok(())
}
fn make_fake_typeface_with_languages(languages: &[&str]) -> Typeface {
make_fake_typeface(Width::Normal, Slant::Upright, WEIGHT_NORMAL, languages, &[], None)
}
fn request_lang<'a, 'b>(
collection: &'a Collection,
lang: &'b [&'b str],
) -> Result<&'a Typeface, Error> {
request_typeface(
collection,
Width::Normal,
Slant::Upright,
WEIGHT_NORMAL,
Some(lang),
TypefaceRequestFlags::empty(),
None,
)?
.ok_or_else(|| format_err!("No typeface found for lang"))
}
#[test]
fn test_font_matching_lang() -> Result<(), Error> {
let collection = make_fake_typeface_collection(vec![
make_fake_typeface_with_languages(&["a"]),
make_fake_typeface_with_languages(&["b-C"]),
make_fake_typeface_with_languages(&["b-D", "b-E"]),
make_fake_typeface_with_languages(&["fooo"]),
make_fake_typeface_with_languages(&["foo-BAR"]),
]);
// Exact matches.
assert_eq!(request_lang(&collection, &["a"])?.asset_id, 0);
assert_eq!(request_lang(&collection, &["b-C"])?.asset_id, 1);
assert_eq!(request_lang(&collection, &["b-E"])?.asset_id, 2);
// Verify that request language order is respected.
assert_eq!(request_lang(&collection, &["b-C", "a"])?.asset_id, 1);
// Partial match: the first matching font is returned first.
assert_eq!(request_lang(&collection, &["b"])?.asset_id, 1);
// Exact match overrides preceding partial match.
assert_eq!(request_lang(&collection, &["b", "a"])?.asset_id, 0);
// Partial match should match a whole BCP47 segment.
assert_eq!(request_lang(&collection, &["foo"])?.asset_id, 4);
Ok(())
}
fn make_fake_typeface_with_fallback_family(fallback_family: GenericFontFamily) -> Typeface {
make_fake_typeface(
Width::Normal,
Slant::Upright,
WEIGHT_NORMAL,
&[],
&[],
Some(fallback_family),
)
}
fn request_fallback_family(
collection: &Collection,
fallback_family: GenericFontFamily,
) -> Result<&Typeface, Error> {
request_typeface(
collection,
Width::Normal,
Slant::Upright,
WEIGHT_NORMAL,
None,
TypefaceRequestFlags::empty(),
Some(fallback_family),
)?
.ok_or_else(|| format_err!("No typeface found for fallback family"))
}
#[test]
fn test_font_matching_fallback_group() -> Result<(), Error> {
let collection = make_fake_typeface_collection(vec![
make_fake_typeface_with_fallback_family(GenericFontFamily::Serif),
make_fake_typeface_with_fallback_family(GenericFontFamily::SansSerif),
make_fake_typeface_with_fallback_family(GenericFontFamily::Monospace),
]);
assert_eq!(request_fallback_family(&collection, GenericFontFamily::Serif)?.asset_id, 0);
assert_eq!(request_fallback_family(&collection, GenericFontFamily::SansSerif)?.asset_id, 1);
assert_eq!(request_fallback_family(&collection, GenericFontFamily::Monospace)?.asset_id, 2);
// First font is returned when there is no exact match.
assert_eq!(request_fallback_family(&collection, GenericFontFamily::Cursive)?.asset_id, 0);
Ok(())
}
}