blob: ec2a6eaef7d416b0fb80b31f43ee0e844e1ca47e [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, TypefaceId},
},
anyhow::{format_err, Error},
char_set::CharSet,
fidl_fuchsia_fonts::{Style2, TypefaceQuery, TypefaceRequest},
fidl_fuchsia_fonts_ext::TypefaceRequestExt,
std::{collections::HashSet, sync::Arc},
};
fn unwrap_query<'a>(query: &'a Option<TypefaceQuery>) -> Result<&'a TypefaceQuery, Error> {
query.as_ref().ok_or(format_err!("Missing query"))
}
/// Ordered collection of `Typeface`s. Construct using [`TypefaceCollectionBuilder`].
#[derive(Debug, Eq, PartialEq, Hash)]
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 match_request<'a, 'b>(
&'a self,
request: &'b TypefaceRequest,
) -> Result<Option<&'a Typeface>, Error> {
/// Returns true if request does not require an exact style match, _or_ if the query's style
/// values match those that are in the `typeface`.
///
/// Any style properties that are omitted from the query are ignored.
fn does_exact_style_match(request: &TypefaceRequest, typeface: &Typeface) -> bool {
if !request.exact_style() {
return true;
}
let query = request.query.as_ref().unwrap();
match &query.style {
None => true,
Some(style) => {
(style.width.is_none() || style.width.unwrap() == typeface.width)
&& (style.weight.is_none() || style.weight.unwrap() == typeface.weight)
&& (style.slant.is_none() || style.slant.unwrap() == 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)),
}
}
let query = unwrap_query(&request.query)?;
Ok(self
.faces
.iter()
.filter(|f| does_exact_style_match(&request, f))
.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 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),
..Style2::EMPTY
})
}
}
/// Builder for [typeface `Collection`](crate::font_service::typeface::Collection).
#[derive(Debug)]
pub struct TypefaceCollectionBuilder {
faces: Vec<Arc<Typeface>>,
/// Bookkeeping for deduplication
seen_ids: HashSet<TypefaceId>,
}
impl TypefaceCollectionBuilder {
/// Creates a new collection builder with no typefaces.
pub fn new() -> Self {
TypefaceCollectionBuilder { faces: vec![], seen_ids: HashSet::new() }
}
/// Returns true if the collection already has a typeface with the given ID.
pub fn has_typeface_id(&self, typeface_id: &TypefaceId) -> bool {
self.seen_ids.contains(typeface_id)
}
/// If there's no existing typeface with the same typeface ID already in the builder, adds the
/// given typeface and and returns `true`. Otherwise, returns `false`.
pub fn add_typeface_once(&mut self, typeface: Arc<Typeface>) -> bool {
let typeface_id = TypefaceId { asset_id: typeface.asset_id, index: typeface.font_index };
if self.seen_ids.contains(&typeface_id) {
false
} else {
self.seen_ids.insert(typeface_id);
self.faces.push(typeface);
true
}
}
/// Returns true if there are no typefaces currently in the collection.
pub fn is_empty(&self) -> bool {
self.faces.is_empty()
}
/// Creates a `Collection` from the builder's current state. Call this method last, once you are
/// satisfied with how the collection is configured.
pub fn build(self) -> Collection {
Collection { faces: self.faces }
}
}
#[cfg(test)]
mod tests {
use {
super::{super::test_util::*, *},
crate::font_service::AssetId,
fidl_fuchsia_fonts::{
GenericFontFamily, Slant, TypefaceRequestFlags, Width, WEIGHT_LIGHT, WEIGHT_NORMAL,
WEIGHT_SEMI_BOLD, WEIGHT_THIN,
},
manifest::v2,
};
fn make_fake_typeface_collection(mut faces: Vec<Typeface>) -> Collection {
let mut builder = TypefaceCollectionBuilder::new();
for (i, mut typeface) in faces.drain(..).enumerate() {
// Assign fake asset_id to each font
typeface.asset_id = AssetId(i as u32);
builder.add_typeface_once(Arc::new(typeface));
}
builder.build()
}
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(
AssetId(0),
v2::Typeface {
index: 0,
style: v2::Style { slant, weight, width },
languages: languages.iter().map(|s| s.to_string()).collect(),
code_points: CharSet::new(char_set.to_vec()),
},
generic_family,
)
.unwrap() // Safe because char_set is not empty
}
fn request_typeface<'a, 'b>(
collection: &'a Collection,
width: impl Into<Option<Width>>,
slant: impl Into<Option<Slant>>,
weight: impl Into<Option<u16>>,
languages: impl Into<Option<&'b [&'b str]>>,
flags: TypefaceRequestFlags,
fallback_family: impl Into<Option<GenericFontFamily>>,
) -> Result<Option<&'a Typeface>, Error> {
let request =
make_typeface_request(width, slant, weight, languages, flags, fallback_family);
collection.match_request(&request)
}
fn request_style_exact(
collection: &Collection,
width: impl Into<Option<Width>>,
slant: impl Into<Option<Slant>>,
weight: impl Into<Option<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_typeface_style(Width::ExtraCondensed, Slant::Upright, WEIGHT_SEMI_BOLD),
make_fake_typeface_style(Width::Condensed, Slant::Italic, WEIGHT_THIN),
make_fake_typeface_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,
AssetId(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(())
}
/// Exact style matches where the query includes only some of the style properties.
#[test]
fn test_font_matching_exact_partial() -> Result<(), Error> {
fn exact_style_not_found() -> Error {
format_err!("Exact style not found")
}
let collection = make_fake_typeface_collection(vec![
make_fake_typeface_style(Width::ExtraCondensed, Slant::Upright, WEIGHT_SEMI_BOLD),
make_fake_typeface_style(Width::Condensed, Slant::Italic, WEIGHT_THIN),
make_fake_typeface_style(Width::ExtraExpanded, Slant::Oblique, WEIGHT_SEMI_BOLD),
make_fake_typeface_style(Width::ExtraExpanded, Slant::Oblique, WEIGHT_NORMAL),
]);
assert_eq!(
request_style_exact(&collection, None, None, WEIGHT_THIN)?
.ok_or_else(exact_style_not_found)?
.asset_id,
AssetId(1)
);
assert_eq!(
request_style_exact(&collection, Width::ExtraExpanded, None, None)?
.ok_or_else(exact_style_not_found)?
.asset_id,
AssetId(3)
);
assert!(
request_style_exact(&collection, Width::SemiCondensed, Slant::Oblique, None)?.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, AssetId(0));
assert_eq!(request_lang(&collection, &["b-C"])?.asset_id, AssetId(1));
assert_eq!(request_lang(&collection, &["b-E"])?.asset_id, AssetId(2));
// Verify that request language order is respected.
assert_eq!(request_lang(&collection, &["b-C", "a"])?.asset_id, AssetId(1));
// Partial match: the first matching font is returned first.
assert_eq!(request_lang(&collection, &["b"])?.asset_id, AssetId(1));
// Exact match overrides preceding partial match.
assert_eq!(request_lang(&collection, &["b", "a"])?.asset_id, AssetId(0));
// Partial match should match a whole BCP47 segment.
assert_eq!(request_lang(&collection, &["foo"])?.asset_id, AssetId(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,
AssetId(0)
);
assert_eq!(
request_fallback_family(&collection, GenericFontFamily::SansSerif)?.asset_id,
AssetId(1)
);
assert_eq!(
request_fallback_family(&collection, GenericFontFamily::Monospace)?.asset_id,
AssetId(2)
);
// First font is returned when there is no exact match.
assert_eq!(
request_fallback_family(&collection, GenericFontFamily::Cursive)?.asset_id,
AssetId(0)
);
Ok(())
}
}