blob: 17852d2d24563e0115b24266fcd1c6638c77f259 [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.
//! Utilities for conversion from Font Manifest v1 to v2.
use {
crate::{v2, Family as FamilyV1, Font as FontV1, FontsManifest as FontsManifestV1},
anyhow::{format_err, Error},
itertools::Itertools,
std::{
convert::{From, TryFrom},
path::{Path, PathBuf},
},
};
impl TryFrom<FontsManifestV1> for v2::FontsManifest {
type Error = Error;
/// Converts a v1 [`manifest::FontsManifest`] to a v2 [`manifest::v2::Manifest`].
///
/// This is purely an in-memory conversion, and does not load character sets for local files.
fn try_from(old: FontsManifestV1) -> Result<v2::FontsManifest, Error> {
// In v2, whether a font is a `fallback` is specified not per font family, but in an
// explicit ordered list.
// We capture each v1 family's `fallback` property for later use.
let families_and_fallbacks: Result<Vec<(v2::Family, bool)>, _> = old
.families
.iter()
.map(|v1_family| {
v2::Family::try_from(v1_family).map(|v2_family| (v2_family, v1_family.fallback))
})
.collect();
let families_and_fallbacks = families_and_fallbacks?;
// For every v1 family that's `fallback: true`, for every asset, for every typeface, add the
// typeface to the v2 `fallback_chain`.
let fallback_chain: Vec<v2::TypefaceId> = families_and_fallbacks
.iter()
.filter(|(_, is_fallback)| *is_fallback)
.flat_map(|(family, _)| {
family.assets.iter().flat_map(|asset| {
asset.typefaces.iter().map(move |typeface| v2::TypefaceId {
file_name: asset.file_name.clone(),
index: typeface.index,
})
})
})
.collect();
let families = families_and_fallbacks.into_iter().map(|(family, _)| family).collect();
Ok(v2::FontsManifest { families, fallback_chain, settings: v2::Settings::default() })
}
}
impl TryFrom<&FamilyV1> for v2::Family {
type Error = Error;
/// Converts a v1 [`manifest::Family`] to a [`manifest::v2::Family`].
///
/// Assumes that all v1 fonts are local files.
fn try_from(old: &FamilyV1) -> Result<v2::Family, Error> {
let assets: Result<Vec<v2::Asset>, _> = old
.fonts
.iter()
.group_by(|font| &font.asset)
.into_iter()
.map(|(asset_path, font_group)| group_fonts_into_assets(asset_path, font_group))
.collect();
// v1 manifests only allow plain aliases, without any typeface property overrides.
let aliases = match &old.aliases {
None => vec![],
Some(aliases) => vec![v2::FontFamilyAliasSet::without_overrides(aliases)?],
};
Ok(v2::Family {
name: old.family.clone(),
aliases,
generic_family: old.generic_family.clone(),
assets: assets?,
})
}
}
/// Groups v1 [`manifest::Font`]s that share a single path into a v2 [`manifest::v2::Asset`] with
/// one or more [`manifest::v2::Typeface`]s.
///
/// Params:
/// - `asset_path`: The path to the font file
/// - `font_group`: Iterator for all the v1 `Font`s that share the same `asset_path`
fn group_fonts_into_assets<'a>(
asset_path: &PathBuf,
font_group: impl Iterator<Item = &'a FontV1>,
) -> Result<v2::Asset, Error> {
// We unwrap PathBuf to_str conversions because we should only be reading valid Unicode-encoded
// paths from JSON manifests.
let file_name: String = asset_path
.file_name()
.ok_or_else(|| format_err!("Invalid path: {:?}", asset_path))?
.to_str()
.ok_or_else(|| format_err!("Invalid path: {:?}", asset_path))?
.to_string();
// If the file is in the package root, then the parent directory will be blank.
let directory: PathBuf =
asset_path.parent().map_or_else(|| PathBuf::from(""), Path::to_path_buf);
// Assuming that all v1 fonts are local files.
Ok(v2::Asset {
file_name,
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator { directory }),
typefaces: font_group.map(font_to_typeface).collect(),
})
}
/// Convert a v1 [`manifest::Font`] to a v2 [`v2::Typeface`].
fn font_to_typeface(font: &FontV1) -> v2::Typeface {
v2::Typeface {
index: font.index,
languages: font.languages.clone(),
style: v2::Style { slant: font.slant, weight: font.weight, width: font.width },
code_points: font.code_points.clone(),
}
}
#[cfg(test)]
mod tests {
use {
super::*,
char_set::CharSet,
fidl_fuchsia_fonts::{GenericFontFamily, Slant, Width},
};
#[test]
fn test_v1_to_v2() -> Result<(), Error> {
let old = FontsManifestV1 {
families: vec![
FamilyV1 {
family: "FamilyA".to_string(),
aliases: Some(vec!["Family A".to_string(), "A Family".to_string()]),
fonts: vec![
FontV1 {
asset: PathBuf::from("path/to/FamilyA-ExtraBold-Condensed.ttf"),
index: 0,
slant: Slant::Upright,
weight: 800,
width: Width::Condensed,
languages: vec!["en-US".to_string()],
package: None,
code_points: CharSet::new(vec![0x1, 0x2, 0x3, 0x7, 0x8, 0x9, 0x100]),
},
FontV1 {
asset: PathBuf::from("path/to/FamilyA-ExtraLight.ttf"),
index: 0,
slant: Slant::Upright,
weight: 200,
width: Width::Normal,
languages: vec!["en-US".to_string()],
package: None,
code_points: CharSet::new(vec![
0x11, 0x12, 0x13, 0x17, 0x18, 0x19, 0x100,
]),
},
],
fallback: true,
generic_family: Some(GenericFontFamily::SansSerif),
},
FamilyV1 {
family: "FamilyB".to_string(),
aliases: Some(vec!["Family B".to_string(), "B Family".to_string()]),
fonts: vec![
FontV1 {
asset: PathBuf::from("FamilyB.ttc"),
index: 0,
slant: Slant::Upright,
weight: 800,
width: Width::Condensed,
languages: vec!["en-US".to_string()],
package: None,
code_points: CharSet::new(vec![0x1, 0x2, 0x3, 0x7, 0x8, 0x9, 0x100]),
},
FontV1 {
asset: PathBuf::from("FamilyB.ttc"),
index: 1,
slant: Slant::Upright,
weight: 200,
width: Width::Normal,
languages: vec!["zh-Hant".to_string()],
package: None,
code_points: CharSet::new(vec![
0x11, 0x12, 0x13, 0x17, 0x18, 0x19, 0x100,
]),
},
],
fallback: false,
generic_family: None,
},
],
};
let expected = v2::FontsManifest {
families: vec![
v2::Family {
name: "FamilyA".to_string(),
aliases: vec![v2::FontFamilyAliasSet::without_overrides(vec![
"Family A", "A Family",
])?],
generic_family: Some(GenericFontFamily::SansSerif),
assets: vec![
v2::Asset {
file_name: "FamilyA-ExtraBold-Condensed.ttf".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: PathBuf::from("path/to"),
}),
typefaces: vec![v2::Typeface {
index: 0,
languages: vec!["en-US".to_string()],
style: v2::Style {
slant: Slant::Upright,
weight: 800,
width: Width::Condensed,
},
code_points: CharSet::new(vec![
0x1, 0x2, 0x3, 0x7, 0x8, 0x9, 0x100,
]),
}],
},
v2::Asset {
file_name: "FamilyA-ExtraLight.ttf".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: PathBuf::from("path/to"),
}),
typefaces: vec![v2::Typeface {
index: 0,
languages: vec!["en-US".to_string()],
style: v2::Style {
slant: Slant::Upright,
weight: 200,
width: Width::Normal,
},
code_points: CharSet::new(vec![
0x11, 0x12, 0x13, 0x17, 0x18, 0x19, 0x100,
]),
}],
},
],
},
v2::Family {
name: "FamilyB".to_string(),
aliases: vec![v2::FontFamilyAliasSet::without_overrides(vec![
"Family B", "B Family",
])?],
generic_family: None,
assets: vec![v2::Asset {
file_name: "FamilyB.ttc".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: PathBuf::from(""),
}),
typefaces: vec![
v2::Typeface {
index: 0,
languages: vec!["en-US".to_string()],
style: v2::Style {
slant: Slant::Upright,
weight: 800,
width: Width::Condensed,
},
code_points: CharSet::new(vec![
0x1, 0x2, 0x3, 0x7, 0x8, 0x9, 0x100,
]),
},
v2::Typeface {
index: 1,
languages: vec!["zh-Hant".to_string()],
style: v2::Style {
slant: Slant::Upright,
weight: 200,
width: Width::Normal,
},
code_points: CharSet::new(vec![
0x11, 0x12, 0x13, 0x17, 0x18, 0x19, 0x100,
]),
},
],
}],
},
],
fallback_chain: vec![
v2::TypefaceId {
file_name: "FamilyA-ExtraBold-Condensed.ttf".to_string(),
index: 0,
},
v2::TypefaceId { file_name: "FamilyA-ExtraLight.ttf".to_string(), index: 0 },
],
settings: v2::Settings::default(),
};
assert_eq!(v2::FontsManifest::try_from(old)?, expected);
Ok(())
}
}