blob: f10c822b30c930d07a5bdbaefee03b2fb8bcc800 [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.
//! Deserialization for `.font_catalog.json` files.
#![allow(deprecated)]
use {
crate::{
merge::{MergeError, TryMerge, TryMergeGroups},
serde_ext::{self, LoadError},
},
anyhow::Error,
fidl_fuchsia_fonts::GenericFontFamily,
itertools::Itertools,
manifest::{
serde_ext::*,
v2::{FontFamilyAliasSet, Style},
},
rayon::prelude::*,
serde::de::Deserializer,
serde::Deserialize,
std::{
collections::{BTreeMap, HashSet},
path::{Path, PathBuf},
},
unicase::UniCase,
};
/// Possible versions of [FontCatalog].
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(tag = "version")]
enum FontCatalogWrapper {
#[serde(rename = "1")]
Version1(FontCatalog),
}
/// A human-defined catalog of fonts that exist in a particular CIPD repo.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct FontCatalog {
pub families: Vec<Family>,
}
/// Index into the `families` table.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct FamilyIndex(pub usize);
/// Index into a [Family]'s `assets` field.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct AssetInFamilyIndex(pub usize);
/// A [`Typeface`](manifest::v2::Typeface)'s index inside an [`Asset`](manifest::v2::Asset).
///
/// For single-typeface font files (e.g. .ttf), this will be `0`. A multiple-typeface font file,
/// such as a TrueType Collection (.ttc), can contain typefaces at other indices.
///
/// Note that an [Asset]'s [`typefaces`](manifest::v2::Asset::typefaces) indices might not start at
/// zero and can have discontinuities, depending on which typefaces from the font file are actually
/// being exposed in the font manifest.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize)]
pub struct TypefaceInAssetIndex(pub u32);
impl From<u32> for TypefaceInAssetIndex {
/// - `index`: The [index of a typeface within a font asset](TypefaceInAssetIndex).
fn from(index: u32) -> Self {
Self(index)
}
}
impl FontCatalog {
/// Loads and merges multiple catalogs.
pub fn load_from_paths<T, P>(paths: T) -> Result<Self, Error>
where
T: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let paths: Vec<PathBuf> =
paths.into_iter().map(|path_ref| path_ref.as_ref().into()).collect();
let catalogs: Result<Vec<Self>, _> =
paths.par_iter().map(|path| Self::load_from_path(path)).collect();
Self::try_merge(catalogs?)
}
/// Loads a single catalog.
pub fn load_from_path<T: AsRef<Path>>(path: T) -> Result<Self, LoadError> {
match serde_ext::load_from_path(path) {
Ok(FontCatalogWrapper::Version1(catalog)) => Ok(catalog),
Err(err) => Err(err),
}
}
/// Tries to merge multiple catalogs into one.
pub fn try_merge<T>(catalogs: T) -> Result<FontCatalog, Error>
where
T: IntoIterator<Item = Self>,
{
let families: Vec<Family> =
catalogs.into_iter().flat_map(|catalog| catalog.families).try_merge_groups()?;
Ok(FontCatalog { families })
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
pub struct Family {
pub name: String,
#[serde(default)]
pub aliases: Vec<FontFamilyAliasSet>,
#[serde(with = "OptGenericFontFamily", default)] // Default to `None`
pub generic_family: Option<GenericFontFamily>,
#[deprecated(note = "Use per-product fallback chain instead")]
pub fallback: bool,
pub assets: Vec<Asset>,
}
impl Family {
pub fn get_asset(&self, asset_index: AssetInFamilyIndex) -> Option<&Asset> {
self.assets.get(asset_index.0)
}
}
/// We merge Families with the same name by combining and deduplicating their aliases and assets.
impl TryMerge for Family {
type Key = String;
fn key(&self) -> Self::Key {
self.name.clone()
}
fn has_matching_fields(&self, other: &Self) -> bool {
self.generic_family == other.generic_family && self.fallback == other.fallback
}
fn try_merge_group(mut group: Vec<Self>) -> Result<Self, Error> {
let name = (&group[0].name).to_string();
let generic_family = (&group[0]).generic_family;
let fallback = (&group[0]).fallback;
let aliases = group
.iter_mut()
// Move `aliases` out
.flat_map(|family| std::mem::replace(&mut family.aliases, Default::default()))
.try_merge_groups()?
.into_iter()
.map(|alias_set: FontFamilyAliasSet| alias_set.into())
.collect();
let assets = group.into_iter().flat_map(|family| family.assets).try_merge_groups()?;
Ok(Family { name, aliases, generic_family, fallback, assets })
}
}
impl TryMerge for FontFamilyAliasSet {
type Key = (StyleOptions, Vec<String>);
fn key(&self) -> Self::Key {
(self.style_overrides().clone(), self.language_overrides().cloned().collect_vec())
}
fn has_matching_fields(&self, _other: &Self) -> bool {
// All of the fields we want to match are already part of the `Key`, so this is trivially
// true.
true
}
fn try_merge_group(group: Vec<Self>) -> Result<Self, Error> {
let names = group
.iter()
.flat_map(|set| set.names())
.map(|name| UniCase::new(name))
.sorted()
.unique()
.collect_vec();
FontFamilyAliasSet::new(
names,
group[0].style_overrides().clone(),
group[0].language_overrides().cloned().collect_vec(),
)
}
/// Ensure that every alias `name` is unique among all the `FontFamilyAliasSet`s.
fn post_validate(groups: Vec<Self>) -> Result<Vec<Self>, MergeError<Self>> {
let mut unique = HashSet::new();
let first_duplicate =
groups.iter().flat_map(|group| group.names()).find(|&name| !unique.insert(name));
match first_duplicate {
Some(name) => {
Err(MergeError::PostInvalid(format!("{:?} appeared more than once", name), groups))
}
None => Ok(groups),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
pub struct Asset {
pub file_name: String,
#[serde(deserialize_with = "Asset::deserialize_typefaces")]
pub typefaces: BTreeMap<TypefaceInAssetIndex, Typeface>,
}
impl Asset {
/// Deserialize an array of [`Typeface`]s into an indexed map.
fn deserialize_typefaces<'de, D>(
deserializer: D,
) -> Result<BTreeMap<TypefaceInAssetIndex, Typeface>, D::Error>
where
D: Deserializer<'de>,
{
let typefaces: Vec<Typeface> = Vec::deserialize(deserializer)?;
let mut map = BTreeMap::new();
for typeface in typefaces {
map.insert(TypefaceInAssetIndex(typeface.index), typeface);
}
Ok(map)
}
}
/// `Asset`s with the same `file_name`s are expected to be identical within a given [`Family`].
///
/// Notably, we do not attempt to merge lists of `Typeface`s.
///
/// (On the other hand, a single font file might contain typefaces from different font families.
/// In this case, the different `Family` structs would have `Asset`s with the same `file_name` but
/// different subsets of the `typeface` array.)
impl TryMerge for Asset {
type Key = String;
fn key(&self) -> Self::Key {
self.file_name.clone()
}
fn try_merge_group(mut group: Vec<Self>) -> Result<Self, Error> {
// Just take the last one.
Ok(group.pop().unwrap())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
pub struct Typeface {
#[serde(default)]
pub index: u32,
#[serde(default)]
pub languages: Vec<String>,
#[serde(flatten)]
pub style: Style,
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl_fuchsia_fonts::{GenericFontFamily, Slant, Style2 as FidlStyle, Width, WEIGHT_NORMAL},
maplit::btreemap,
pretty_assertions::assert_eq,
std::iter,
};
#[test]
fn test_try_merge_aliases() -> Result<(), Error> {
let aliases: Vec<FontFamilyAliasSet> = vec![
FontFamilyAliasSet::without_overrides(vec!["Abc"])?,
FontFamilyAliasSet::without_overrides(vec!["Def"])?,
// Duplicate
FontFamilyAliasSet::without_overrides(vec!["Abc"])?,
FontFamilyAliasSet::new(
vec!["Abc Condensed", "Abc Squished"],
FidlStyle {
slant: None,
weight: None,
width: Some(Width::Condensed),
..FidlStyle::EMPTY
},
iter::empty::<String>(),
)?,
// Duplicate
FontFamilyAliasSet::new(
vec!["Abc Condensed", "Condensed Abc"],
FidlStyle {
slant: None,
weight: None,
width: Some(Width::Condensed),
..FidlStyle::EMPTY
},
iter::empty::<String>(),
)?,
];
let expected = vec![
FontFamilyAliasSet::without_overrides(vec!["Abc", "Def"])?,
FontFamilyAliasSet::new(
vec!["Abc Condensed", "Abc Squished", "Condensed Abc"],
StyleOptions { width: Some(Width::Condensed), ..Default::default() },
iter::empty::<String>(),
)?,
];
let actual = aliases.into_iter().try_merge_groups()?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn test_try_merge_aliases_collision() -> Result<(), Error> {
let aliases: Vec<FontFamilyAliasSet> = vec![
FontFamilyAliasSet::without_overrides(vec!["Abc"])?,
FontFamilyAliasSet::without_overrides(vec!["Def"])?,
FontFamilyAliasSet::new(
vec!["Def"],
FidlStyle { slant: None, weight: None, width: None, ..FidlStyle::EMPTY },
vec!["en", "es"],
)?,
];
let actual = aliases.into_iter().try_merge_groups();
println!("{:#?}", actual);
assert!(actual.is_err());
Ok(())
}
#[test]
fn test_try_merge_assets() -> Result<(), Error> {
let assets = vec![
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
// Duplicate
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
Asset {
file_name: "FamilyA-1.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
TypefaceInAssetIndex(1) => Typeface {
index: 1,
languages: vec!["he".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
],
},
];
let expected = vec![
Asset {
file_name: "FamilyA-1.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
TypefaceInAssetIndex(1) => Typeface {
index: 1,
languages: vec!["he".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
],
},
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
];
let actual = assets.into_iter().try_merge_groups()?;
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn test_try_merge_assets_collision() -> Result<(), Error> {
let assets = vec![
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
// Duplicate with collision
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["ru".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
Asset {
file_name: "FamilyA-1.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
TypefaceInAssetIndex(1) => Typeface {
index: 1,
languages: vec!["he".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
],
},
];
let actual = assets.into_iter().try_merge_groups();
assert!(actual.is_err());
Ok(())
}
#[test]
fn test_merge_catalogs() -> Result<(), Error> {
let catalogs = vec![
FontCatalog {
families: vec![Family {
name: "Family A".to_string(),
aliases: vec![
FontFamilyAliasSet::without_overrides(vec!["Family Ay", "A Family"])?,
FontFamilyAliasSet::new(
vec!["Family A Condensed"],
StyleOptions { width: Some(Width::Condensed), ..Default::default() },
iter::empty::<String>(),
)?,
]
.into_iter()
.collect(),
generic_family: Some(GenericFontFamily::Serif),
fallback: true,
assets: vec![
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
Asset {
file_name: "FamilyA-1.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
TypefaceInAssetIndex(1) => Typeface {
index: 1,
languages: vec!["he".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
],
},
],
}],
},
FontCatalog {
families: vec![
Family {
name: "Family A".to_string(),
aliases: vec![
FontFamilyAliasSet::without_overrides(vec![
"Family Ayyyy",
"FamilyA",
"a family",
])?,
FontFamilyAliasSet::new(
vec!["Family A Squished"],
StyleOptions {
width: Some(Width::Condensed),
..Default::default()
},
iter::empty::<String>(),
)?,
]
.into_iter()
.collect(),
generic_family: Some(GenericFontFamily::Serif),
fallback: true,
assets: vec![
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) =>Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
Asset {
file_name: "FamilyA-2.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
],
},
Family {
name: "Family B".to_string(),
aliases: vec![FontFamilyAliasSet::without_overrides(vec![
"FamilyB", "BFamily",
])?]
.into_iter()
.collect(),
generic_family: Some(GenericFontFamily::Serif),
fallback: true,
assets: vec![Asset {
file_name: "FamilyB.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) =>Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
}],
},
],
},
];
let expected = FontCatalog {
families: vec![
Family {
name: "Family A".to_string(),
aliases: vec![
FontFamilyAliasSet::without_overrides(vec![
"A Family",
"Family Ay",
"Family Ayyyy",
"FamilyA",
])?,
FontFamilyAliasSet::new(
vec!["Family A Condensed", "Family A Squished"],
StyleOptions { width: Some(Width::Condensed), ..Default::default() },
iter::empty::<String>(),
)?,
]
.into_iter()
.collect(),
generic_family: Some(GenericFontFamily::Serif),
fallback: true,
assets: vec![
Asset {
file_name: "FamilyA-1.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
TypefaceInAssetIndex(1) => Typeface {
index: 1,
languages: vec!["he".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
},
],
},
Asset {
file_name: "FamilyA-2.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
Asset {
file_name: "FamilyA.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) =>Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
},
],
},
Family {
name: "Family B".to_string(),
aliases: vec![FontFamilyAliasSet::new(
vec!["BFamily", "FamilyB"],
StyleOptions::default(),
iter::empty::<String>(),
)?
.into()]
.into_iter()
.collect(),
generic_family: Some(GenericFontFamily::Serif),
fallback: true,
assets: vec![Asset {
file_name: "FamilyB.ttf".to_string(),
typefaces: btreemap![
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
width: Width::Condensed,
weight: WEIGHT_NORMAL,
slant: Slant::Upright,
},
}
],
}],
},
],
};
let actual = FontCatalog::try_merge(catalogs)?;
assert_eq!(actual, expected);
Ok(())
}
}