blob: 570b5820888d5fdb50c1579ef65b916361314670 [file] [log] [blame] [edit]
// 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::{
asset::{AssetCollectionBuilder, AssetLoader, AssetLoaderImpl},
family::{FamilyOrAliasBuilder, FontFamilyBuilder},
inspect::ServiceInspectData,
typeface::{Typeface, TypefaceCollectionBuilder, TypefaceId},
FontService,
},
anyhow::{format_err, Error},
font_info::FontInfoLoaderImpl,
fuchsia_inspect as finspect,
fuchsia_syslog::fx_vlog,
fuchsia_trace as trace,
manifest::{v2, FontManifestWrapper, FontsManifest},
std::{
collections::BTreeMap,
convert::TryFrom,
fmt::{self, Display},
path::{Path, PathBuf},
sync::Arc,
},
thiserror::Error,
unicase::UniCase,
};
/// Builder for [`FontService`]. Allows populating the fields that remain immutable for the
/// lifetime of the service.
///
/// Create a new builder with [`new()`](FontServiceBuilder::new), then populate using
/// [`load_manifest()`](FontServiceBuilder::load_manifest), and finally construct a `FontService`
/// using [`build()`](FontServiceBuilder::build).
#[derive(Debug)]
pub struct FontServiceBuilder<'a, L>
where
L: AssetLoader,
{
manifests: Vec<ManifestOrPath>,
assets: AssetCollectionBuilder<L>,
/// Maps the font family name from the manifest (`families.family`) to a FamilyOrAlias.
families: BTreeMap<UniCase<String>, FamilyOrAliasBuilder>,
fallback_collection: TypefaceCollectionBuilder,
inspect_root: &'a finspect::Node,
}
impl<'a> FontServiceBuilder<'a, AssetLoaderImpl> {
/// Creates a new, empty builder with a real asset loader.
pub fn with_default_asset_loader(
cache_capacity_bytes: u64,
inspect_root: &'a finspect::Node,
) -> FontServiceBuilder<'a, AssetLoaderImpl> {
FontServiceBuilder::<'a, AssetLoaderImpl>::new(
AssetLoaderImpl::new(),
cache_capacity_bytes,
inspect_root,
)
}
}
impl<'a, L> FontServiceBuilder<'a, L>
where
L: AssetLoader,
{
/// Creates a new, empty builder.
pub fn new(
asset_loader: L,
default_cache_capacity_bytes: u64,
inspect_root: &'a finspect::Node,
) -> FontServiceBuilder<'a, L> {
FontServiceBuilder {
manifests: vec![],
assets: AssetCollectionBuilder::new(asset_loader, default_cache_capacity_bytes, inspect_root),
families: BTreeMap::new(),
fallback_collection: TypefaceCollectionBuilder::new(),
inspect_root,
}
}
/// Add a manifest path to be parsed and processed.
pub fn add_manifest_from_file(&mut self, manifest_path: &Path) -> &mut Self {
self.manifests.push(ManifestOrPath::Path(manifest_path.to_path_buf()));
self
}
/// Adds a parsed manifest to be processed.
#[allow(dead_code)]
#[cfg(test)]
pub fn add_manifest(&mut self, manifest_wrapper: FontManifestWrapper) -> &mut Self {
self.manifests.push(ManifestOrPath::Manifest(manifest_wrapper));
self
}
/// Tries to build a [`FontService`] from the provided manifests, with some additional error
/// checking.
pub async fn build(mut self) -> Result<FontService<L>, Error> {
let manifest_paths = self.manifests.iter().map(ManifestOrPath::to_string).collect();
let manifests: Result<Vec<(FontManifestWrapper, Option<PathBuf>)>, Error> = self
.manifests
.drain(..)
.map(|manifest_or_path| match manifest_or_path {
ManifestOrPath::Manifest(manifest) => Ok((manifest, None)),
ManifestOrPath::Path(path) => {
fx_vlog!(1, "Loading manifest {:?}", &path);
Ok((FontsManifest::load_from_file(&path)?, Some(path)))
}
})
.collect();
let mut cache_size_bytes: Option<u64> = None;
for (wrapper, path) in manifests? {
match wrapper {
FontManifestWrapper::Version1(v1) => {
self.add_fonts_from_manifest_v1(v1, path).await?
}
FontManifestWrapper::Version2(v2) => {
// Update the cache size from the first manifest that has it. (In production use
// cases, there will only be one manifest.)
if v2.settings.cache_size_bytes.is_some() && cache_size_bytes.is_none() {
cache_size_bytes = v2.settings.cache_size_bytes.clone();
}
self.add_fonts_from_manifest_v2(v2, path).await?
}
}
}
if let Some(cache_size_bytes) = cache_size_bytes {
self.assets.set_cache_capacity(cache_size_bytes);
}
// It's fine to have no fallback collection IFF we loaded an empty manifest.
if self.fallback_collection.is_empty() && !self.families.is_empty() {
return Err(FontServiceBuilderError::NoFallbackCollection.into());
}
let assets = self.assets.build();
let families = self.families.into_iter().map(|(key, value)| (key, value.build())).collect();
let fallback_collection = self.fallback_collection.build();
let inspect_data = ServiceInspectData::new(
self.inspect_root,
manifest_paths,
&assets,
&families,
&fallback_collection,
);
Ok(FontService { assets, families, fallback_collection, inspect_data })
}
async fn add_fonts_from_manifest_v2(
&mut self,
manifest: v2::FontsManifest,
manifest_path: Option<PathBuf>,
) -> Result<(), Error> {
// Hold on to the typefaces defined in this manifest so that they can be referenced when
// building the fallback chain.
let mut manifest_typefaces: BTreeMap<TypefaceId, Arc<Typeface>> = BTreeMap::new();
for mut manifest_family in manifest.families {
// Register the family itself
let family_name = UniCase::new(manifest_family.name.clone());
let family = match self.families.entry(family_name.clone()).or_insert_with(|| {
FamilyOrAliasBuilder::Family(FontFamilyBuilder::new(
family_name.to_string(),
manifest_family.generic_family,
))
}) {
FamilyOrAliasBuilder::Family(f) => f,
FamilyOrAliasBuilder::Alias(_, _) => {
return Err(FontServiceBuilderError::AliasFamilyConflict {
conflicting_name: family_name.to_string(),
manifest_path: manifest_path.clone(),
}
.into());
}
};
// Register the family's assets and their typefaces.
// We have to use `.drain()` here (instead of moving `assets` out) in order to leave
// `manifest_family` in a valid state to be able to keep using it further down.
for manifest_asset in manifest_family.assets.drain(..) {
let asset_id = self.assets.add_or_get_asset_id(&manifest_asset);
for manifest_typeface in manifest_asset.typefaces {
if manifest_typeface.code_points.is_empty() {
return Err(FontServiceBuilderError::NoCodePoints {
asset_name: manifest_asset.file_name.to_string(),
typeface_idx: manifest_typeface.index,
manifest_path: manifest_path.clone(),
}
.into());
}
let generic_family = manifest_family.generic_family;
let typeface_id = TypefaceId { asset_id, index: manifest_typeface.index };
// Deduplicate typefaces across multiple manifests
if !family.has_typeface_id(&typeface_id) {
// .unwrap() because we already checked for missing code points above
let typeface = Arc::new(
Typeface::new(asset_id, manifest_typeface, generic_family).unwrap(),
);
manifest_typefaces.insert(typeface_id, typeface.clone());
family.add_typeface_once(typeface);
}
}
}
// Above, we're working with `family` mutably borrowed from `self.families`. We have to
// finish using any mutable references to `self.families` before we can create further
// references to `self.families` below.
// Register aliases
let aliases = FamilyOrAliasBuilder::aliases_from_family(&manifest_family);
for (key, value) in aliases {
match self.families.get(&key) {
None => {
self.families.insert(key, value);
}
Some(FamilyOrAliasBuilder::Family(_)) => {
return Err(FontServiceBuilderError::AliasFamilyConflict {
conflicting_name: key.to_string(),
manifest_path: manifest_path.clone(),
}
.into());
}
Some(FamilyOrAliasBuilder::Alias(other_family_name, _)) => {
// If the alias exists then it must be for the same font family.
if *other_family_name != family_name {
return Err(FontServiceBuilderError::AmbiguousAlias {
alias: key.to_string(),
canonical_1: other_family_name.to_string(),
canonical_2: family_name.to_string(),
manifest_path: manifest_path.clone(),
}
.into());
}
}
}
}
}
// We add all the fallback typefaces, preserving their order from the product font
// configuration file.
//
// Unfortunately, when there are multiple manifests with fallback chains, the best we can
// do is concatenate the fallback chains (with de-duplication). Multiple manifests are not
// expected in production use cases, so this isn't as awful as it sounds.
for fallback_typeface in &manifest.fallback_chain {
let asset_id = self
.assets
.get_asset_id_by_name(&fallback_typeface.file_name)
.ok_or_else(|| FontServiceBuilderError::UnknownFallbackEntry {
file_name: fallback_typeface.file_name.clone(),
index: fallback_typeface.index,
manifest_path: manifest_path.clone(),
})?;
let typeface_id = TypefaceId { asset_id, index: fallback_typeface.index };
if !self.fallback_collection.has_typeface_id(&typeface_id) {
let typeface = manifest_typefaces
.get(&typeface_id)
.expect("Invalid state in FontServiceBuilder")
.clone();
self.fallback_collection.add_typeface_once(typeface);
}
}
Ok(())
}
async fn add_fonts_from_manifest_v1(
&mut self,
manifest_v1: FontsManifest,
manifest_path: Option<PathBuf>,
) -> Result<(), Error> {
let path_string: String = manifest_path
.as_ref()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_default();
trace::duration!(
"fonts",
"font_service:builder:add_fonts_from_manifest_v2",
"path" => &path_string[..]);
let manifest_v2 = self.convert_manifest_v1_to_v2(manifest_v1).await.map_err(|e| {
FontServiceBuilderError::ConversionFromV1 {
manifest_path: manifest_path.clone(),
cause: e.into(),
}
})?;
self.add_fonts_from_manifest_v2(manifest_v2, manifest_path).await
}
/// Converts data format from manifest v1 to v2 and loads character sets for any typefaces that
/// lack them.
async fn convert_manifest_v1_to_v2(
&self,
manifest_v1: FontsManifest,
) -> Result<v2::FontsManifest, Error> {
let mut manifest_v2 = v2::FontsManifest::try_from(manifest_v1)?;
let asset_loader = AssetLoaderImpl::new();
let font_info_loader = FontInfoLoaderImpl::new()?;
for manifest_family in &mut manifest_v2.families {
for manifest_asset in &mut manifest_family.assets {
for manifest_typeface in &mut manifest_asset.typefaces {
if manifest_typeface.code_points.is_empty() {
match &manifest_asset.location {
v2::AssetLocation::LocalFile(v2::LocalFileLocator { directory }) => {
let asset_path = directory.join(&manifest_asset.file_name);
let buffer = asset_loader.load_vmo_from_path(&asset_path)?;
let font_info = {
trace::duration!("fonts", "FontInfoLoaderImpl:load_font_info");
font_info_loader
.load_font_info(buffer, manifest_typeface.index)?
};
manifest_typeface.code_points = font_info.char_set;
}
_ => {
return Err(format_err!(
"Impossible asset location: {:?}",
&manifest_asset
));
}
}
}
}
}
}
Ok(manifest_v2)
}
}
#[allow(dead_code)]
#[derive(Debug)]
enum ManifestOrPath {
Manifest(FontManifestWrapper),
Path(PathBuf),
}
impl Display for ManifestOrPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ManifestOrPath::Manifest(_manifest) => write!(f, "{}", "FontManifestWrapper {{ ... }}"),
ManifestOrPath::Path(path) => write!(f, "{}", path.to_string_lossy().to_string()),
}
}
}
/// Errors arising from the use of [`FontServiceBuilder`].
#[derive(Debug, Error)]
pub enum FontServiceBuilderError {
/// A name was used as both a canonical family name and a font family alias.
#[error(
"Conflict in {:?}: {} cannot be both a canonical family name and an alias",
manifest_path,
conflicting_name
)]
AliasFamilyConflict { conflicting_name: String, manifest_path: Option<PathBuf> },
/// One string was used as an alias for two different font families.
#[error(
"Conflict in {:?}: {} cannot be an alias for both {} and {}",
manifest_path,
alias,
canonical_1,
canonical_2
)]
AmbiguousAlias {
alias: String,
canonical_1: String,
canonical_2: String,
manifest_path: Option<PathBuf>,
},
/// Something went wrong when converting a manifest from v1 to v2.
#[error("Conversion from manifest v1 failed in {:?}: {:?}", manifest_path, cause)]
ConversionFromV1 {
manifest_path: Option<PathBuf>,
#[source]
cause: Error,
},
/// The font manifest's fallback chain referenced an undeclared typeface.
#[error(
"Unknown typeface in fallback chain in {:?}: file name \'{}\', index {}",
manifest_path,
file_name,
index
)]
UnknownFallbackEntry { file_name: String, index: u32, manifest_path: Option<PathBuf> },
/// None of the loaded manifests contained a non-empty fallback chain.
#[error("Need at least one fallback font family")]
NoFallbackCollection,
/// The manifest did not have defined code points for a particular typeface.
#[error("Missing code points for \"{}\"[{}] in {:?}", asset_name, typeface_idx, manifest_path)]
NoCodePoints { asset_name: String, typeface_idx: u32, manifest_path: Option<PathBuf> },
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::font_service::{
family::{FamilyOrAlias, FontFamily},
typeface::Collection as TypefaceCollection,
AssetId,
},
char_set::CharSet,
fidl_fuchsia_fonts::{GenericFontFamily, Slant, Width, WEIGHT_BOLD, WEIGHT_NORMAL},
manifest::{serde_ext::StyleOptions, v2},
maplit::{btreemap, btreeset},
pretty_assertions::assert_eq,
std::sync::Arc,
unicase::UniCase,
};
#[fuchsia_async::run_singlethreaded(test)]
async fn test_multiple_overlapping_manifests() -> Result<(), Error> {
let inspector = finspect::Inspector::new();
let mut builder = FontServiceBuilder::with_default_asset_loader(5000, inspector.root());
builder
.add_manifest(FontManifestWrapper::Version2(v2::FontsManifest {
families: vec![v2::Family {
name: "Alpha".to_string(),
aliases: vec![v2::FontFamilyAliasSet::without_overrides(vec![
"A", "Aleph", "Alif",
])?],
generic_family: Some(GenericFontFamily::SansSerif),
assets: vec![
v2::Asset {
file_name: "Alpha-Regular.ttf".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: PathBuf::from("/pkg/config/data/assets"),
}),
typefaces: vec![v2::Typeface {
index: 0,
languages: vec!["en".to_string()],
style: v2::Style {
slant: Slant::Upright,
weight: WEIGHT_NORMAL,
width: Width::Normal,
},
code_points: CharSet::new(vec![0x1, 0x2, 0x3]),
}],
},
v2::Asset {
file_name: "Alpha-Bold.ttf".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: PathBuf::from("/pkg/config/data/assets"),
}),
typefaces: vec![v2::Typeface {
index: 0,
languages: vec!["en".to_string()],
style: v2::Style {
slant: Slant::Upright,
weight: WEIGHT_BOLD,
width: Width::Normal,
},
code_points: CharSet::new(vec![0x1, 0x2, 0x3]),
}],
},
],
}],
fallback_chain: vec![
v2::TypefaceId { file_name: "Alpha-Bold.ttf".to_string(), index: 0 },
v2::TypefaceId { file_name: "Alpha-Regular.ttf".to_string(), index: 0 },
],
settings: v2::Settings { cache_size_bytes: Some(12345) },
}))
.add_manifest(FontManifestWrapper::Version2(v2::FontsManifest {
families: vec![v2::Family {
name: "Alpha".to_string(),
aliases: vec![
v2::FontFamilyAliasSet::without_overrides(vec![
"A",
"Aleph",
"Alpha Ordinary",
])?,
// Note different languages in second manifest's "Alif" alias
v2::FontFamilyAliasSet::new(
vec!["Alif"],
StyleOptions::default(),
vec!["en", "ar"],
)?,
],
generic_family: Some(GenericFontFamily::SansSerif),
assets: vec![v2::Asset {
file_name: "Alpha-Regular.ttf".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: PathBuf::from("/pkg/config/data/assets"),
}),
typefaces: vec![v2::Typeface {
index: 0,
languages: vec!["en".to_string()],
style: v2::Style {
slant: Slant::Upright,
weight: WEIGHT_NORMAL,
width: Width::Expanded, // Note difference
},
code_points: CharSet::new(vec![0x1, 0x2, 0x3]),
}],
}],
}],
fallback_chain: vec![v2::TypefaceId {
file_name: "Alpha-Regular.ttf".to_string(),
index: 0,
}],
settings: v2::Settings { cache_size_bytes: Some(99999) },
}));
let service = builder.build().await?;
let expected_typeface_regular = Arc::new(Typeface {
asset_id: AssetId(0),
font_index: 0,
slant: Slant::Upright,
weight: WEIGHT_NORMAL,
width: Width::Normal, // First version wins
languages: btreeset!["en".to_string()],
char_set: CharSet::new(vec![0x1, 0x2, 0x3]),
generic_family: Some(GenericFontFamily::SansSerif),
});
let expected_typeface_bold = Arc::new(Typeface {
asset_id: AssetId(1),
font_index: 0,
slant: Slant::Upright,
weight: WEIGHT_BOLD,
width: Width::Normal,
languages: btreeset!["en".to_string()],
char_set: CharSet::new(vec![0x1, 0x2, 0x3]),
generic_family: Some(GenericFontFamily::SansSerif),
});
assert_eq!(
service.families,
btreemap!(
UniCase::new("Alpha".to_string()) =>
FamilyOrAlias::Family(FontFamily {
name: "Alpha".to_string(),
faces: TypefaceCollection {
faces: vec![
expected_typeface_regular.clone(),
expected_typeface_bold.clone()
]
},
generic_family: Some(GenericFontFamily::SansSerif)
}),
UniCase::new("A".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Alpha".to_string()), None),
UniCase::new("Aleph".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Alpha".to_string()), None),
// First version of "Alif" wins
UniCase::new("Alif".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Alpha".to_string()), None),
UniCase::new("Alpha Ordinary".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Alpha".to_string()), None),
UniCase::new("AlphaOrdinary".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Alpha".to_string()), None),)
);
assert_eq!(service.assets.len(), 2);
assert_eq!(
service.fallback_collection,
TypefaceCollection {
faces: vec![expected_typeface_bold.clone(), expected_typeface_regular.clone()]
}
);
assert_eq!(service.assets.cache_capacity_bytes().await, 12345);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_empty_manifest() -> Result<(), Error> {
let inspector = finspect::Inspector::new();
let manifest = FontManifestWrapper::Version2(v2::FontsManifest::empty());
let mut builder = FontServiceBuilder::with_default_asset_loader(5000, inspector.root());
builder.add_manifest(manifest);
builder.build().await?; // Should succeed
Ok(())
}
}