blob: 4ae2a371870dabeaf4703dce971a14709247bb92 [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.
use {
crate::{
font_catalog::TypefaceInAssetIndex, font_db::FontDb, FontCatalog, FontPackageListing,
FontSets, ProductConfig,
},
anyhow::Error,
font_info::FontInfoLoader,
itertools::Itertools,
manifest::{v2, FontManifestWrapper},
std::{collections::BTreeMap, fmt, path::Path},
thiserror::Error,
};
/// Builds a `FontDb` and then generates a manifest from all of the font metadata that has been
/// loaded.
pub fn generate_manifest(
font_catalog: FontCatalog,
font_pkgs: FontPackageListing,
font_sets: FontSets,
product_config: ProductConfig,
font_info_loader: impl FontInfoLoader,
font_dir: impl AsRef<Path>,
verbose: bool,
target_asset_dir: impl AsRef<Path>,
) -> Result<FontManifestWrapper, Error> {
let service_settings = product_config.settings.clone();
let db = FontDb::new(
font_catalog,
font_pkgs,
font_sets,
product_config,
font_info_loader,
font_dir,
target_asset_dir,
)?;
let mut errors: Vec<GeneratorError> = vec![];
// For detecting name collisions
let mut postscript_name_to_typeface = BTreeMap::new();
let mut full_name_to_typeface = BTreeMap::new();
let manifest = v2::FontsManifest {
families: db
.iter_families()
.map(|catalog_family| v2::Family {
name: catalog_family.name.clone(),
aliases: catalog_family
.aliases
.iter()
.cloned()
.map(|string_or_alias_set| string_or_alias_set.into())
.collect(),
generic_family: catalog_family.generic_family,
assets: db
.iter_assets(catalog_family)
.map(|catalog_asset| v2::Asset {
file_name: catalog_asset.file_name.clone(),
location: db.get_asset_location(catalog_asset),
typefaces: catalog_asset
.typefaces
.values()
.map(|catalog_typeface| {
let typeface_metadata = db.get_typeface_metadata(
catalog_asset,
TypefaceInAssetIndex(catalog_typeface.index),
);
let file_name = catalog_asset.file_name.clone();
let face_index = catalog_typeface.index;
let postscript_name = typeface_metadata.postscript_name.clone();
let full_name = typeface_metadata.full_name.clone();
let manifest_typeface = v2::Typeface {
index: catalog_typeface.index,
languages: catalog_typeface.languages.clone(),
style: catalog_typeface.style.clone(),
code_points: typeface_metadata.code_points.clone(),
postscript_name: Some(postscript_name.clone()),
full_name: full_name.clone(),
};
let face_key = (file_name.clone(), face_index);
if let Some((prev_file_name, prev_face_index)) =
postscript_name_to_typeface
.insert(postscript_name.clone(), face_key.clone())
{
errors.push(GeneratorError::DuplicatePostscriptName {
postscript_name,
file_name_1: prev_file_name,
index_1: prev_face_index,
file_name_2: file_name.clone(),
index_2: face_index,
})
}
// full_name is optional, but, if present, must be unique
if let Some(full_name) = full_name {
if let Some((prev_file_name, prev_face_index)) =
full_name_to_typeface
.insert(full_name.clone(), face_key.clone())
{
errors.push(GeneratorError::DuplicateFullName {
full_name,
file_name_1: prev_file_name,
index_1: prev_face_index,
file_name_2: file_name,
index_2: face_index,
})
}
}
manifest_typeface
})
.collect(),
})
.collect(),
})
.collect(),
fallback_chain: db.iter_fallback_chain().collect(),
settings: v2::Settings { cache_size_bytes: service_settings.cache_size_bytes },
};
if verbose {
let non_fallback_typefaces: Vec<v2::TypefaceId> =
db.iter_non_fallback_typefaces().sorted().collect();
eprintln!("Non-fallback typefaces:\n{:#?}", &non_fallback_typefaces);
}
if errors.is_empty() {
Ok(FontManifestWrapper::Version2(manifest))
} else {
Err(GeneratorErrors(errors.into()))?
}
}
/// Error while generating the manifest (_after_ building the font database).
#[derive(Debug, Error)]
pub enum GeneratorError {
#[error(
"Two typefaces had the same postscript_name '{}': {}:{} and {}:{}",
postscript_name,
file_name_1,
index_1,
file_name_2,
index_2
)]
DuplicatePostscriptName {
postscript_name: String,
file_name_1: String,
index_1: u32,
file_name_2: String,
index_2: u32,
},
#[error(
"Two typefaces had the same full_name '{}': {}:{} and {}:{}",
full_name,
file_name_1,
index_1,
file_name_2,
index_2
)]
DuplicateFullName {
full_name: String,
file_name_1: String,
index_1: u32,
file_name_2: String,
index_2: u32,
},
}
/// Collection of errors from generating a manifest.
#[derive(Debug, Error)]
#[error("Errors occurred while generating manifest: {}", _0)]
pub struct GeneratorErrors(GeneratorErrorVec);
#[derive(Debug)]
pub struct GeneratorErrorVec(Vec<GeneratorError>);
impl From<Vec<GeneratorError>> for GeneratorErrorVec {
fn from(errors: Vec<GeneratorError>) -> Self {
GeneratorErrorVec(errors)
}
}
impl From<GeneratorErrors> for Vec<GeneratorError> {
fn from(source: GeneratorErrors) -> Self {
source.0 .0
}
}
impl fmt::Display for GeneratorErrorVec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let descriptions = self.0.iter().map(|e| format!("{}", e)).collect_vec();
write!(f, "{:#?}", descriptions)
}
}
// For additional coverage, see integration tests in `//src/fonts/tools/manifest_generator/tests`.
#[cfg(test)]
mod tests {
#![allow(deprecated)]
use {
super::*,
crate::{
constants,
font_catalog::{Asset, Family, Typeface},
font_pkgs::FontPackageEntry,
font_sets::FontSet,
product_config::{FallbackChainEntry, Settings},
},
anyhow::format_err,
assert_matches::assert_matches,
char_set::CharSet,
fidl_fuchsia_fonts::{GenericFontFamily, Slant, Width},
font_info::{FontAssetSource, FontInfo},
manifest::v2::Style,
maplit::btreemap,
};
/// Implementation of `FontInfoLoader` whose outputs for each file name and index are explicitly
/// specified in a user-provided map.
#[derive(Debug)]
struct TestFontInfoLoader {
map: BTreeMap<(String, u32), FontInfo>,
}
impl TestFontInfoLoader {
fn new(map: BTreeMap<(String, u32), FontInfo>) -> Self {
TestFontInfoLoader { map }
}
}
impl FontInfoLoader for TestFontInfoLoader {
fn load_font_info<S, E>(&self, source: S, index: u32) -> Result<FontInfo, Error>
where
S: std::convert::TryInto<FontAssetSource, Error = E>,
E: Sync + Send + Into<Error>,
{
let source: FontAssetSource = source.try_into().map_err(|e| e.into())?;
let file_name = {
if let FontAssetSource::FilePath(path) = source {
path.split('/').last().expect("file path").to_string()
} else {
unreachable!();
}
};
self.map
.get(&(file_name, index))
.map(|font_info| font_info.clone())
.ok_or_else(|| format_err!("not found"))
}
}
fn make_font_db_args() -> (FontCatalog, FontPackageListing, ProductConfig) {
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.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!["en".to_string()],
style: Style {
slant: Slant::Upright,
weight: 700,
width: Width::Normal,
},
},
},
},
Asset {
file_name: "AlphaSansCopy.ttf".to_string(),
typefaces: btreemap! {
TypefaceInAssetIndex(0) => Typeface {
index: 0,
languages: vec!["en".to_string()],
style: Style {
slant: Slant::Upright,
weight: 700,
width: Width::Normal,
},
}
},
},
],
}],
};
let font_pkgs = FontPackageListing::new(btreemap! {
"AlphaSans.ttc".to_string() =>
FontPackageEntry::new(
"AlphaSans.ttc",
"alphasans-ttc",
"alphasans/",
),
"AlphaSansCopy.ttf".to_string() => {
FontPackageEntry::new(
"AlphaSansCopy.ttf",
"alphasanscopy.ttf",
"alphasans/"
)
}
});
let product_config = ProductConfig {
fallback_chain: vec![FallbackChainEntry::with_index("AlphaSans.ttc", 0)],
settings: Settings { cache_size_bytes: None },
};
(font_catalog, font_pkgs, product_config)
}
#[test]
fn test_colliding_postscript_names() {
use GeneratorError::*;
let (font_catalog, font_pkgs, product_config) = make_font_db_args();
let font_info_loader = TestFontInfoLoader::new(btreemap! {
("AlphaSans.ttc".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Regular".to_string()),
full_name: Some("Alpha Sans".to_string())
},
("AlphaSans.ttc".to_string(), 2) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Bold".to_string()),
full_name: Some("Alpha Sans Bold".to_string())
},
("AlphaSansCopy.ttf".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Bold".to_string()),
full_name: Some("Alpha Sans Bold Copy".to_string())
},
});
let font_dir = "foo/bar";
let font_sets = FontSets::new(btreemap! {
"AlphaSans.ttc".to_string() => FontSet::Local,
"AlphaSansCopy.ttf".to_string() => FontSet::Local,
});
let result = generate_manifest(
font_catalog,
font_pkgs,
font_sets,
product_config,
font_info_loader,
font_dir,
false,
constants::LOCAL_ASSET_DIRECTORY,
);
assert_matches!(result, Err(_));
let errors: Vec<GeneratorError> =
result.unwrap_err().downcast::<GeneratorErrors>().expect("downcast").into();
assert!(errors
.iter()
.find(|e| if let DuplicatePostscriptName { .. } = e { true } else { false })
.is_some());
assert!(errors
.iter()
.find(|e| if let DuplicateFullName { .. } = e { true } else { false })
.is_none());
}
/// This is the same as the above test, but "AlphaSansCopy.ttf" is not included in the manifest,
/// so there should be no collision.
#[test]
fn test_no_collision_unless_font_is_included_in_font_set() {
let (font_catalog, font_pkgs, product_config) = make_font_db_args();
let font_info_loader = TestFontInfoLoader::new(btreemap! {
("AlphaSans.ttc".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Regular".to_string()),
full_name: Some("Alpha Sans".to_string())
},
("AlphaSans.ttc".to_string(), 2) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Bold".to_string()),
full_name: Some("Alpha Sans Bold".to_string())
},
("AlphaSansCopy.ttf".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Bold".to_string()),
full_name: Some("Alpha Sans Bold Copy".to_string())
},
});
let font_dir = "foo/bar";
let font_sets = FontSets::new(btreemap! {
"AlphaSans.ttc".to_string() => FontSet::Local,
});
let result = generate_manifest(
font_catalog,
font_pkgs,
font_sets,
product_config,
font_info_loader,
font_dir,
false,
constants::LOCAL_ASSET_DIRECTORY,
);
assert_matches!(result, Ok(_));
}
#[test]
fn test_colliding_full_names() {
use GeneratorError::*;
let (font_catalog, font_pkgs, product_config) = make_font_db_args();
let font_info_loader = TestFontInfoLoader::new(btreemap! {
("AlphaSans.ttc".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Regular".to_string()),
full_name: Some("Alpha Sans".to_string())
},
("AlphaSans.ttc".to_string(), 2) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Bold".to_string()),
full_name: Some("Alpha Sans Bold".to_string())
},
("AlphaSansCopy.ttf".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-BoldCopy".to_string()),
full_name: Some("Alpha Sans Bold".to_string())
},
});
let font_dir = "foo/bar";
let font_sets = FontSets::new(btreemap! {
"AlphaSans.ttc".to_string() => FontSet::Local,
"AlphaSansCopy.ttf".to_string() => FontSet::Local,
});
let result = generate_manifest(
font_catalog,
font_pkgs,
font_sets,
product_config,
font_info_loader,
font_dir,
false,
constants::LOCAL_ASSET_DIRECTORY,
);
assert_matches!(result, Err(_));
let errors: Vec<GeneratorError> =
result.unwrap_err().downcast::<GeneratorErrors>().expect("downcast").into();
assert!(errors
.iter()
.find(|e| if let DuplicatePostscriptName { .. } = e { true } else { false })
.is_none());
assert!(errors
.iter()
.find(|e| if let DuplicateFullName { .. } = e { true } else { false })
.is_some());
}
#[test]
fn test_no_full_names_no_problem() {
let (font_catalog, font_pkgs, product_config) = make_font_db_args();
let font_info_loader = TestFontInfoLoader::new(btreemap! {
("AlphaSans.ttc".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Regular".to_string()),
full_name: None,
},
("AlphaSans.ttc".to_string(), 2) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-Bold".to_string()),
full_name: None,
},
("AlphaSansCopy.ttf".to_string(), 0) => FontInfo {
char_set: CharSet::new(vec![0x1, 0x2]),
postscript_name: Some("AlphaSans-BoldCopy".to_string()),
full_name: None,
},
});
let font_dir = "foo/bar";
let font_sets = FontSets::new(btreemap! {
"AlphaSans.ttc".to_string() => FontSet::Local,
"AlphaSansCopy.ttf".to_string() => FontSet::Local,
});
let result = generate_manifest(
font_catalog,
font_pkgs,
font_sets,
product_config,
font_info_loader,
font_dir,
false,
constants::LOCAL_ASSET_DIRECTORY,
);
assert_matches!(result, Ok(_));
}
}