| // 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.clone())); |
| 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(()) |
| } |
| } |