| // 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. |
| |
| #![allow(deprecated)] |
| |
| use { |
| crate::{ |
| constants::{LOCAL_ASSET_DIRECTORY, PKG_URL_PREFIX}, |
| font_catalog as fc, |
| font_catalog::{AssetInFamilyIndex, FamilyIndex}, |
| product_config, FontCatalog, FontPackageListing, FontSet, FontSets, ProductConfig, |
| TypefaceInAssetIndex, |
| }, |
| anyhow::{format_err, Error}, |
| char_set::CharSet, |
| font_info::{FontAssetSource, FontInfo, FontInfoLoader}, |
| fuchsia_url::pkg_url::PkgUrl, |
| itertools::{Either, Itertools}, |
| manifest::v2, |
| std::{ |
| collections::{BTreeMap, BTreeSet}, |
| fmt, |
| path::{Path, PathBuf}, |
| }, |
| thiserror::Error, |
| }; |
| |
| type AssetKey = (FamilyIndex, AssetInFamilyIndex); |
| |
| /// Collection of font metadata used for generating a font manifest for a particular target. |
| /// |
| /// Contains indices by family and asset names, which all provide access only to those fonts that |
| /// are included in `font_sets`. (`font_catalog` may contain other fonts that are not included in |
| /// the target product.) |
| /// |
| /// For test coverage, please see the integration tests. |
| #[derive(Debug)] |
| pub struct FontDb { |
| font_catalog: FontCatalog, |
| font_sets: FontSets, |
| product_config: ProductConfig, |
| |
| family_name_to_family: BTreeMap<String, FamilyIndex>, |
| asset_name_to_assets: BTreeMap<String, BTreeSet<AssetKey>>, |
| asset_name_to_pkg_url: BTreeMap<String, PkgUrl>, |
| typeface_to_char_set: BTreeMap<(String, TypefaceInAssetIndex), CharSet>, |
| } |
| |
| impl FontDb { |
| /// Tries to create a new instance of `FontDb`. |
| pub fn new<P: AsRef<Path>>( |
| font_catalog: FontCatalog, |
| font_pkgs: FontPackageListing, |
| font_sets: FontSets, |
| product_config: ProductConfig, |
| font_info_loader: impl FontInfoLoader, |
| font_dir: P, |
| ) -> Result<FontDb, FontDbErrors> { |
| let mut family_name_to_family = BTreeMap::new(); |
| let mut asset_name_to_assets = BTreeMap::new(); |
| let mut asset_name_to_pkg_url = BTreeMap::new(); |
| let typeface_to_char_set = BTreeMap::new(); |
| |
| let mut errors: Vec<FontDbError> = vec![]; |
| |
| for (family_idx, family) in (&font_catalog.families).iter().enumerate() { |
| let family_idx = FamilyIndex(family_idx); |
| let mut asset_count = 0; |
| for (asset_idx, asset) in (&family.assets).iter().enumerate() { |
| let asset_idx = AssetInFamilyIndex(asset_idx); |
| let asset_name = asset.file_name.clone(); |
| if asset.typefaces.is_empty() { |
| errors.push(FontDbError::FontCatalogNoTypeFaces { asset_name }); |
| continue; |
| } |
| |
| if font_sets.get_font_set(&asset.file_name).is_some() { |
| let safe_name = font_pkgs.get_safe_name(&asset_name); |
| if safe_name.is_none() { |
| errors.push(FontDbError::FontPkgsMissingEntry { asset_name }); |
| continue; |
| } |
| |
| let pkg_url = Self::make_pkg_url(safe_name.unwrap()); |
| if let Err(error) = pkg_url { |
| errors.push(error); |
| continue; |
| } |
| let pkg_url = pkg_url.unwrap(); |
| |
| asset_name_to_assets |
| .entry(asset_name.clone()) |
| .or_insert_with(|| BTreeSet::new()) |
| .insert((family_idx, asset_idx)); |
| asset_name_to_pkg_url.insert(asset_name.clone(), pkg_url); |
| |
| asset_count += 1; |
| } |
| } |
| // Skip families where no assets are included in the target product |
| if asset_count > 0 { |
| family_name_to_family.insert(family.name.clone(), family_idx); |
| } |
| } |
| |
| // typeface_to_char_set is empty at this point. |
| let mut db = FontDb { |
| font_catalog, |
| font_sets, |
| product_config, |
| family_name_to_family, |
| asset_name_to_assets, |
| asset_name_to_pkg_url, |
| typeface_to_char_set, |
| }; |
| |
| let mut fallback_typeface_counts: BTreeMap<v2::TypefaceId, usize> = BTreeMap::new(); |
| // TODO(fxbug.dev/46156): Switch to iter_fallback_chain() when legacy fallbacks are removed. |
| for typeface_id in db.iter_explicit_fallback_chain() { |
| *fallback_typeface_counts.entry(typeface_id.clone()).or_insert(0) += 1; |
| // Confirm that the TypefaceId in the fallback chain actually exists in the database. |
| if !db |
| .get_assets_by_name(&typeface_id.file_name) |
| .iter() |
| .any(|asset| asset.typefaces.contains_key(&typeface_id.index.into())) |
| { |
| errors.push(FontDbError::UnknownFallbackChainEntry { |
| typeface_id: typeface_id.clone(), |
| }) |
| } |
| } |
| for (typeface_id, count) in fallback_typeface_counts.into_iter() { |
| if count > 1 { |
| errors.push(FontDbError::DuplicateFallbackChainEntry { typeface_id }) |
| } |
| } |
| |
| let font_infos = Self::load_font_infos(&db, &font_pkgs, font_info_loader, font_dir); |
| |
| match font_infos { |
| Ok(font_infos) => { |
| for (request, font_info) in font_infos { |
| db.typeface_to_char_set.insert( |
| (request.asset_name(), TypefaceInAssetIndex(request.index)), |
| font_info.char_set, |
| ); |
| } |
| } |
| Err(mut font_info_errors) => { |
| errors.append(&mut font_info_errors); |
| } |
| } |
| |
| if errors.is_empty() { |
| Ok(db) |
| } else { |
| Err(FontDbErrors(errors.into())) |
| } |
| } |
| |
| pub fn get_family_by_name(&self, family_name: impl AsRef<str>) -> Option<&fc::Family> { |
| let family_idx = self.family_name_to_family.get(family_name.as_ref())?; |
| self.font_catalog.families.get(family_idx.0) |
| } |
| |
| /// Get all [`Asset`]s with the given file name. There may be more than one instance if the |
| /// asset appears in multiple font families. |
| pub fn get_assets_by_name(&self, asset_name: impl AsRef<str>) -> Vec<&fc::Asset> { |
| self.asset_name_to_assets |
| .get(asset_name.as_ref()) |
| // Iterate over the 0 or 1 values inside Option |
| .iter() |
| .flat_map(|asset_keys| asset_keys.iter()) |
| .flat_map(move |(family_idx, asset_idx)| { |
| self.font_catalog |
| .families |
| .get(family_idx.0) |
| .and_then(|family| family.get_asset(*asset_idx)) |
| }) |
| .collect_vec() |
| } |
| |
| /// The asset must be in the `FontDb` or this method will panic. |
| pub fn get_code_points(&self, asset: &fc::Asset, index: TypefaceInAssetIndex) -> &CharSet { |
| // Alas, no sane way to transpose between `(&str, &x)` and `&(String, x)`. |
| let key = (asset.file_name.to_owned(), index); |
| self.typeface_to_char_set |
| .get(&key) |
| .ok_or_else(|| format_err!("No code points for {:?}", &key)) |
| .unwrap() |
| } |
| |
| /// The asset must be in the `FontDb` or this method will panic. |
| pub fn get_asset_location(&self, asset: &fc::Asset) -> v2::AssetLocation { |
| match self.font_sets.get_font_set(&*asset.file_name).unwrap() { |
| FontSet::Local => v2::AssetLocation::LocalFile(v2::LocalFileLocator { |
| directory: PathBuf::from(LOCAL_ASSET_DIRECTORY), |
| }), |
| FontSet::Download => v2::AssetLocation::Package(v2::PackageLocator { |
| url: self.asset_name_to_pkg_url.get(&*asset.file_name).unwrap().clone(), |
| }), |
| } |
| } |
| |
| /// Iterates over all the _included_ font families in the `FontDb`. |
| pub fn iter_families(&self) -> impl Iterator<Item = &'_ fc::Family> + '_ { |
| self.font_catalog |
| .families |
| .iter() |
| .filter(move |family| self.get_family_by_name(&*family.name).is_some()) |
| } |
| |
| /// Iterates over all the _included_ assets in the given font family. Note this is _not_ the |
| /// same as iterating over `Family::assets`. |
| pub fn iter_assets<'a>( |
| &'a self, |
| family: &'a fc::Family, |
| ) -> impl Iterator<Item = &'a fc::Asset> + 'a { |
| family |
| .assets |
| .iter() |
| .filter(move |asset| !self.get_assets_by_name(&*asset.file_name).is_empty()) |
| } |
| |
| /// Iterates over the `TypefaceId`s in the target product's fallback chain, plus those marked as |
| /// `"fallback": true` in font catalog files. |
| pub fn iter_fallback_chain<'a>(&'a self) -> impl Iterator<Item = v2::TypefaceId> + 'a { |
| // .unique() eliminates any duplicates from the legacy chain that are already in the |
| // explicit one. |
| itertools::chain(self.iter_explicit_fallback_chain(), self.iter_legacy_fallback_chain()) |
| .unique() |
| } |
| |
| /// Gets a list of `TypefaceId`s that are not used in the _explicit_ fallback chain, for debugging. |
| pub fn iter_non_fallback_typefaces<'a>(&'a self) -> impl Iterator<Item = v2::TypefaceId> + 'a { |
| let fallback_typefaces: BTreeSet<v2::TypefaceId> = |
| self.iter_explicit_fallback_chain().collect(); |
| self.iter_families() |
| .flat_map(move |family| self.family_to_typeface_ids(family)) |
| .filter(move |typeface_id| !fallback_typefaces.contains(typeface_id)) |
| } |
| |
| /// Iterates over the `TypefaceId`s in the target product's fallback chain. |
| fn iter_explicit_fallback_chain<'a>(&'a self) -> impl Iterator<Item = v2::TypefaceId> + 'a { |
| /// Converts a product config TypefaceId into one or more manifest typeface IDs |
| /// (an omitted font index expands to multiple indices). |
| fn get_manifest_typeface_ids<'b>( |
| font_db: &'b FontDb, |
| id: &product_config::TypefaceId, |
| ) -> impl Iterator<Item = v2::TypefaceId> + 'b { |
| // TODO: Dynamic type acrobatics instead of collecting into `Vec`s. |
| match id.index { |
| Some(index) => vec![v2::TypefaceId::new(&id.file_name, index.0)], |
| None => font_db |
| .get_assets_by_name(&id.file_name) |
| .iter() |
| .flat_map(|asset| FontDb::asset_to_typeface_ids(asset)) |
| .collect_vec(), |
| } |
| .into_iter() |
| } |
| |
| self.product_config |
| .iter_fallback_chain() |
| .flat_map(move |id| get_manifest_typeface_ids(self, id)) |
| .into_iter() |
| } |
| |
| /// Iterates over legacy fallbacks specified by `"fallback": true` in the font catalog. |
| // TODO(fxbug.dev/46156): Remove this code after all product font collections and font catalogs |
| // are updated. |
| fn iter_legacy_fallback_chain<'a>(&'a self) -> impl Iterator<Item = v2::TypefaceId> + 'a { |
| self.iter_families() |
| .filter(|family| family.fallback) |
| .flat_map(move |family| self.family_to_typeface_ids(family)) |
| .into_iter() |
| } |
| |
| fn family_to_typeface_ids<'a>( |
| &'a self, |
| family: &'a fc::Family, |
| ) -> impl Iterator<Item = v2::TypefaceId> + 'a { |
| self.iter_assets(family).flat_map(FontDb::asset_to_typeface_ids) |
| } |
| |
| fn asset_to_typeface_ids<'a>( |
| asset: &'a fc::Asset, |
| ) -> impl Iterator<Item = v2::TypefaceId> + 'a { |
| let file_name = asset.file_name.clone(); |
| asset |
| .typefaces |
| .keys() |
| .map(move |key| v2::TypefaceId { file_name: file_name.clone(), index: key.0 }) |
| } |
| |
| fn make_pkg_url(safe_name: impl AsRef<str>) -> Result<PkgUrl, FontDbError> { |
| let pkg_url = format!("{}{}", PKG_URL_PREFIX, safe_name.as_ref()); |
| Ok(PkgUrl::parse(&pkg_url).map_err(|error| FontDbError::PkgUrl { error: error.into() })?) |
| } |
| |
| fn load_font_infos( |
| db: &FontDb, |
| font_pkgs: &FontPackageListing, |
| font_info_loader: impl FontInfoLoader, |
| font_dir: impl AsRef<Path>, |
| ) -> Result<Vec<(FontInfoRequest, FontInfo)>, Vec<FontDbError>> { |
| let (requests, errors): (Vec<_>, Vec<_>) = db |
| .font_sets |
| .iter() |
| .map(|(asset_name, _)| { |
| Self::asset_to_font_info_requests(db, font_pkgs, font_dir.as_ref(), asset_name) |
| }) |
| .flatten() |
| .partition_map(|r| match r { |
| Ok(data) => Either::Left(data), |
| Err(err) => Either::Right(err), |
| }); |
| |
| if !errors.is_empty() { |
| return Err(errors); |
| } |
| |
| let (font_infos, errors): (Vec<_>, Vec<_>) = requests |
| .into_iter() |
| .map(|request| { |
| let source = FontAssetSource::FilePath(request.path.to_str().unwrap().to_owned()); |
| let font_info = font_info_loader.load_font_info(source, request.index); |
| match font_info { |
| Ok(font_info) => Ok((request, font_info)), |
| Err(error) => Err(FontDbError::FontInfo { request, error }), |
| } |
| }) |
| .partition_map(|r| match r { |
| Ok(data) => Either::Left(data), |
| Err(err) => Either::Right(err), |
| }); |
| |
| if !errors.is_empty() { |
| Err(errors) |
| } else { |
| Ok(font_infos) |
| } |
| } |
| |
| fn asset_to_font_info_requests( |
| db: &FontDb, |
| font_pkgs: &FontPackageListing, |
| font_dir: impl AsRef<Path>, |
| asset_name: &str, |
| ) -> Vec<Result<FontInfoRequest, FontDbError>> { |
| let mut path = font_dir.as_ref().to_path_buf(); |
| |
| let path_prefix = font_pkgs.get_path_prefix(asset_name); |
| if path_prefix.is_none() { |
| return vec![Err(FontDbError::FontPkgsMissingEntry { |
| asset_name: asset_name.to_owned(), |
| })]; |
| } |
| |
| let path_prefix = path_prefix.unwrap(); |
| path.push(path_prefix); |
| path.push(asset_name); |
| |
| // We have to collect into a vector here because otherwise there's no way to return a |
| // consistent `Iterator` type. |
| let requests = db |
| .get_assets_by_name(asset_name) |
| .iter() |
| .flat_map(|asset| asset.typefaces.keys()) |
| .map(move |index| Ok(FontInfoRequest { path: path.clone(), index: index.0 })) |
| .collect_vec(); |
| |
| if requests.is_empty() { |
| vec![Err(FontDbError::FontCatalogMissingEntry { asset_name: asset_name.to_owned() })] |
| } else { |
| requests |
| } |
| } |
| } |
| |
| /// Collection of errors from loading / building `FontDb`. |
| #[derive(Debug, Error)] |
| #[error("Errors occurred while building FontDb: {}", _0)] |
| pub struct FontDbErrors(FontDbErrorVec); |
| |
| #[derive(Debug)] |
| pub struct FontDbErrorVec(Vec<FontDbError>); |
| |
| impl From<Vec<FontDbError>> for FontDbErrorVec { |
| fn from(errors: Vec<FontDbError>) -> Self { |
| FontDbErrorVec(errors) |
| } |
| } |
| |
| impl From<FontDbErrors> for Vec<FontDbError> { |
| fn from(wrapper: FontDbErrors) -> Self { |
| (wrapper.0).0 |
| } |
| } |
| |
| impl fmt::Display for FontDbErrorVec { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| let descriptions = self.0.iter().map(|e| format!("{}", e)).collect_vec(); |
| write!(f, "{:#?}", descriptions) |
| } |
| } |
| |
| /// An error in a single `FontDb` operation. |
| #[derive(Debug, Error)] |
| pub enum FontDbError { |
| #[error("Asset {} has no typefaces", asset_name)] |
| FontCatalogNoTypeFaces { asset_name: String }, |
| |
| #[error("Asset {} is not listed in *.font_pkgs.json", asset_name)] |
| FontPkgsMissingEntry { asset_name: String }, |
| |
| #[error("Asset {} is not listed in *.font_catalog.json", asset_name)] |
| FontCatalogMissingEntry { asset_name: String }, |
| |
| #[error("Fallback asset {}, index {} is unknown", typeface_id.file_name, typeface_id.index)] |
| UnknownFallbackChainEntry { typeface_id: v2::TypefaceId }, |
| |
| #[error("Multiple entries in fallback chain for {}, index {}", typeface_id.file_name, typeface_id.index)] |
| DuplicateFallbackChainEntry { typeface_id: v2::TypefaceId }, |
| |
| #[error("PkgUrl error: {:?}", error)] |
| PkgUrl { error: Error }, |
| |
| #[error("Failed to load font info for {:?}: {:?}", request, error)] |
| FontInfo { request: FontInfoRequest, error: Error }, |
| } |
| |
| /// Metadata needed for [`FontInfoLoader::load_font_info`]. |
| #[derive(Debug, Clone)] |
| pub struct FontInfoRequest { |
| /// Path to the file |
| path: PathBuf, |
| /// Index of the font in the file |
| index: u32, |
| } |
| |
| impl FontInfoRequest { |
| fn asset_name(&self) -> String { |
| self.path.file_name().and_then(|os_str| os_str.to_str()).unwrap().to_owned() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::{ |
| fake_font_info_loader::FakeFontInfoLoaderImpl, |
| font_catalog::{Asset, Family, Typeface, TypefaceInAssetIndex}, |
| font_pkgs::{FontPackageEntry, FontPackageListing}, |
| font_sets::{FontSet, FontSets}, |
| product_config::{ProductConfig, Settings, TypefaceId}, |
| }, |
| fidl_fuchsia_fonts::{GenericFontFamily, Slant, Width}, |
| manifest::v2::Style, |
| maplit::btreemap, |
| matches::assert_matches, |
| }; |
| |
| fn make_font_db_contents() -> (FontCatalog, FontPackageListing, FontSets) { |
| let font_catalog = FontCatalog { |
| families: vec![Family { |
| name: "Alpha Sans".to_string(), |
| aliases: vec![], |
| generic_family: Some(GenericFontFamily::SansSerif), |
| fallback: true, |
| assets: vec![Asset { |
| file_name: "AlphaSans-Regular.ttc".to_string(), |
| typefaces: btreemap! { |
| TypefaceInAssetIndex(0) => Typeface { |
| index: 0, |
| languages: vec!["en".to_string()], |
| style: Style { |
| slant: Slant::Upright, |
| weight: 400, |
| width: Width::Normal, |
| }, |
| }, |
| TypefaceInAssetIndex(2) => Typeface { |
| index: 2, |
| languages: vec!["ru".to_string()], |
| style: Style { |
| slant: Slant::Upright, |
| weight: 400, |
| width: Width::Normal, |
| }, |
| }, |
| }, |
| }], |
| }], |
| }; |
| |
| let font_pkgs = FontPackageListing::new(btreemap! { |
| "AlphaSans-Regular.ttc".to_string() => |
| FontPackageEntry::new( |
| "AlphaSans-Regular.ttc", |
| "alphasans-regular-ttc", |
| "alphasans/", |
| ), |
| }); |
| |
| let font_sets = FontSets::new(btreemap! { |
| "AlphaSans-Regular.ttc".to_string() => FontSet::Local, |
| }); |
| |
| (font_catalog, font_pkgs, font_sets) |
| } |
| |
| /// The fallback chain references an unknown asset. |
| #[test] |
| fn test_unknown_fallback_asset() -> Result<(), Error> { |
| let (font_catalog, font_pkgs, font_sets) = make_font_db_contents(); |
| |
| let product_config = ProductConfig { |
| fallback_chain: vec![TypefaceId::new("BetaSans-Regular.ttc", 0)], |
| settings: Settings { cache_size_bytes: None }, |
| }; |
| |
| let result = FontDb::new( |
| font_catalog, |
| font_pkgs, |
| font_sets, |
| product_config, |
| FakeFontInfoLoaderImpl::new(), |
| "foo/bar", |
| ); |
| |
| assert!(result.is_err()); |
| let errors: Vec<FontDbError> = result.unwrap_err().into(); |
| assert_matches!(errors[0], FontDbError::UnknownFallbackChainEntry{ typeface_id: _ }); |
| |
| Ok(()) |
| } |
| |
| /// The fallback chain references a known asset with an unknown typeface index. |
| #[test] |
| fn test_unknown_fallback_typeface_index() -> Result<(), Error> { |
| let (font_catalog, font_pkgs, font_sets) = make_font_db_contents(); |
| |
| let product_config = ProductConfig { |
| fallback_chain: vec![TypefaceId::new("AlphaSans-Regular.ttc", 1)], |
| settings: Settings { cache_size_bytes: None }, |
| }; |
| |
| let result = FontDb::new( |
| font_catalog, |
| font_pkgs, |
| font_sets, |
| product_config, |
| FakeFontInfoLoaderImpl::new(), |
| "foo/bar", |
| ); |
| |
| assert!(result.is_err()); |
| let errors: Vec<FontDbError> = result.unwrap_err().into(); |
| assert_matches!(errors[0], FontDbError::UnknownFallbackChainEntry{ typeface_id: _ }); |
| |
| Ok(()) |
| } |
| } |