blob: bd3e3b9e4da3f114a014b55d6b276d1e54425f07 [file] [edit]
use std::{collections::HashSet, ops::Index};
#[cfg(feature = "fontations")]
use fontations::skrifa::string::StringId;
#[cfg(feature = "fontations")]
use fontations::{
read::FontRef,
read::{ReadError, TableProvider},
write::FontBuilder,
};
use indexmap::IndexMap;
include!(concat!(env!("OUT_DIR"), "/_.rs"));
include!(concat!(env!("OUT_DIR"), "/data.rs"));
const LINKED_VALUES: [(&str, (f32, f32)); 2] = [("wght", (400.0, 700.0)), ("ital", (0.0, 1.0))];
fn linked_value(axis: &str, value: f32) -> Option<f32> {
LINKED_VALUES
.iter()
.find(|(linked_axis, (cur, _link))| *linked_axis == axis && value == *cur)
.map(|(_, (_, link))| *link)
}
const GF_STATIC_STYLES: [(&str, u16); 18] = [
("Thin", 100),
("ExtraLight", 200),
("Light", 300),
("Regular", 400),
("Medium", 500),
("SemiBold", 600),
("Bold", 700),
("ExtraBold", 800),
("Black", 900),
("Thin Italic", 100),
("ExtraLight Italic", 200),
("Light Italic", 300),
("Italic", 400),
("Medium Italic", 500),
("SemiBold Italic", 600),
("Bold Italic", 700),
("ExtraBold Italic", 800),
("Black Italic", 900),
];
#[cfg(feature = "fontations")]
const PROTECTED_IDS: [StringId; 9] = [
StringId::FAMILY_NAME,
StringId::SUBFAMILY_NAME,
StringId::UNIQUE_ID,
StringId::FULL_NAME,
StringId::VERSION_STRING,
StringId::POSTSCRIPT_NAME,
StringId::TYPOGRAPHIC_FAMILY_NAME,
StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
];
pub struct AxisRegistry {
axes: BTreeMap<String, Box<AxisProto>>,
}
#[derive(Debug, Clone)]
pub struct FontAxis {
pub tag: String,
pub min: f32,
pub max: f32,
pub default: f32,
}
#[derive(Debug, Clone)]
pub struct NameParticle {
pub name: Option<String>,
pub value: f32,
pub elided: bool,
}
impl AxisRegistry {
pub fn new() -> Self {
Self {
axes: (*AXES).clone(),
}
}
pub fn get(&self, tag: &str) -> Option<&AxisProto> {
self.axes.get(tag).map(|v| &**v)
}
pub fn contains_key(&self, tag: &str) -> bool {
self.axes.contains_key(tag)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &AxisProto)> {
self.axes.iter().map(|(k, v)| (k, &**v))
}
pub fn get_fallback<'a>(&'a self, name: &str) -> Option<(&'a str, &'a FallbackProto)> {
self.axes
.iter()
.flat_map(|(tag, axis)| {
let fallback = axis
.fallback
.iter()
.find(|f| f.name.as_deref() == Some(name));
fallback.map(|f| (tag.as_str(), f))
})
.next()
}
// This is fallbacks_in_fvar, but without assuming any particular font representation
pub fn fallbacks<'a>(
&'a self,
font_axes: &'a [FontAxis],
) -> impl Iterator<Item = (String, Vec<FallbackProto>)> + 'a {
font_axes.iter().filter_map(|axis| {
self.get(&axis.tag).map(|registry_axis| {
(
registry_axis.tag.clone().unwrap_or_default(),
registry_axis
.fallback
.iter()
.filter(|f| f.value() >= axis.min && f.value() <= axis.max)
.cloned()
.collect(),
)
})
})
}
// This is fallbacks_in_name_table, but without assuming any particular font representation
pub fn name_table_fallbacks<'a>(
&'a self,
family_name: &'a str,
subfamily_name: &'a str,
font_axes: &'a [FontAxis],
) -> impl Iterator<Item = (&'a str, &'a FallbackProto)> + 'a {
let axis_names: HashSet<&str> = font_axes.iter().map(|axis| axis.tag.as_ref()).collect();
let tokens = family_name
.split_whitespace()
.skip(1)
.chain(subfamily_name.split_whitespace());
tokens
.flat_map(|token| self.get_fallback(token))
.filter(move |(tag, _)| !axis_names.contains(tag))
}
pub fn fallback_for_value<'a>(
&'a self,
axis_tag: &str,
value: f32,
) -> Option<&'a FallbackProto> {
self.get(axis_tag)
.and_then(|axis| axis.fallback.iter().find(|f| f.value == Some(value)))
}
pub fn axis_order(&self) -> Vec<&str> {
let mut axis_tags: Vec<&str> = self
.axes
.keys()
.filter(|k| k.chars().all(|c| c.is_ascii_uppercase()))
.map(|k| k.as_str())
.collect();
axis_tags.sort();
axis_tags.extend(vec!["opsz", "wdth", "wght", "ital", "slnt"]);
axis_tags
}
// This is the old "_fvar_dflts"
pub fn name_particles<'a>(&self, font_axes: &'a [FontAxis]) -> IndexMap<&'a str, NameParticle> {
let mut particles = IndexMap::new();
for axis in font_axes {
if axis.tag == "opsz" {
particles.insert(
"opsz",
NameParticle {
name: Some(format!("{}pt", axis.default)),
value: axis.default,
elided: true,
},
);
} else if let Some(fallback) = self.fallback_for_value(&axis.tag, axis.default) {
particles.insert(
&axis.tag,
NameParticle {
name: fallback.name.clone(),
value: axis.default,
elided: (fallback.value() == self.get(&axis.tag).unwrap().default_value())
&& !(["Regular", "Italic", "14pt"].contains(&fallback.name())),
},
);
} else {
particles.insert(
&axis.tag,
NameParticle {
name: None,
value: axis.default,
elided: true,
},
);
};
}
particles
}
}
impl Default for AxisRegistry {
fn default() -> Self {
Self::new()
}
}
impl Index<&str> for AxisRegistry {
type Output = AxisProto;
fn index(&self, tag: &str) -> &Self::Output {
self.get(tag).expect("No such axis")
}
}
#[cfg(feature = "fontations")]
mod monkeypatching;
#[cfg(feature = "fontations")]
mod nametable;
#[cfg(feature = "fontations")]
mod stat;
#[cfg(feature = "fontations")]
mod fontations_impl {
use super::*;
use fontations::{
skrifa::{string::StringId, MetadataProvider, Tag},
write::{
from_obj::ToOwnedTable,
tables::{
fvar::{Fvar, InstanceRecord},
name::{Name, NameRecord},
os2::Os2,
stat::Stat,
},
types::Fixed,
},
};
use monkeypatching::{AxisValueNameId, SetAxisValueNameId};
use nametable::{
add_name, best_familyname, best_subfamilyname, find_or_add_name, rewrite_or_insert,
};
use stat::{AxisLocation, AxisRecord, AxisValue, StatBuilder};
use std::{cmp::Reverse, collections::HashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RenameAggressiveness {
#[default]
Aggressive,
Conservative,
}
pub fn build_name_table(
font: FontRef,
family_name: Option<&str>,
style_name: Option<&str>,
siblings: &[FontRef],
aggressive: Option<RenameAggressiveness>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut new_font = FontBuilder::new();
let family_name = family_name
.map(|x| x.to_string())
.unwrap_or_else(|| best_familyname(&font).unwrap_or("Unknown".to_string()));
let style_name = style_name
.map(|x| x.to_string())
.unwrap_or_else(|| best_subfamilyname(&font).unwrap_or("Regular".to_string()));
let mut new_name = if font.table_data(Tag::new(b"fvar")).is_some() {
build_vf_name_table(&mut new_font, &font, &family_name, siblings, aggressive)?
} else {
build_static_name_table_v1(&mut new_font, &font, &family_name, &style_name, aggressive)?
};
let mut styles: Vec<_> = GF_STATIC_STYLES.iter().collect();
styles.sort_by_key(|(name, _weight)| Reverse(name.len()));
for (name, weight) in styles.iter() {
if style_name.contains(name) {
let mut new_os2: Os2 = font.os2()?.to_owned_table();
new_os2.us_weight_class = *weight;
new_font.add_table(&new_os2)?;
break;
}
}
// Set RIBBI bits
new_name.name_record.sort();
new_font.add_table(&new_name)?;
Ok(new_font.copy_missing_tables(font).build())
}
fn fvar_instance_collisions(font: &FontRef, siblings: &[FontRef]) -> bool {
let fonts = siblings.iter().chain(std::iter::once(font));
let is_italic = fonts
.map(|f| {
f.post()
.map(|post| post.italic_angle().abs() != Fixed::from_f64(0.0))
.unwrap_or(false)
})
.collect::<Vec<_>>();
is_italic.len() != is_italic.iter().collect::<HashSet<_>>().len()
}
fn build_vf_name_table(
newfont: &mut FontBuilder,
font: &FontRef,
family_name: &str,
siblings: &[FontRef],
aggressive: Option<RenameAggressiveness>,
) -> Result<Name, Box<dyn std::error::Error>> {
let style_name = vf_style_name(font, family_name)?;
let mut new_name: Name = (if fvar_instance_collisions(font, siblings) {
build_static_name_table_v1(newfont, font, family_name, &style_name, aggressive)
} else {
build_static_name_table(newfont, font, family_name, style_name, aggressive)
})?;
// println!("Records: {:#?}", new_name.name_record);
build_variations_ps_name(&mut new_name, font, Some(family_name));
// Ensure table records are sorted
new_name.name_record.sort();
Ok(new_name)
}
fn build_variations_ps_name(newname: &mut Name, font: &FontRef, family_name: Option<&str>) {
let fallback = best_familyname(font);
let family_name = family_name.or(fallback.as_deref()).unwrap_or("New Font");
let subfamily_name = best_subfamilyname(font).unwrap_or("Regular".to_string());
let font_axes = font_axes(font).unwrap_or_default();
let registry = AxisRegistry::new();
let font_styles = registry.name_table_fallbacks(family_name, &subfamily_name, &font_axes);
let mut var_ps = family_name.replace(" ", "");
for (_, fallback) in font_styles {
let fallback_name = fallback.name();
if !var_ps.contains(fallback_name) {
var_ps.push_str(fallback_name);
}
}
rewrite_or_insert(
&mut newname.name_record,
StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
&var_ps,
);
}
fn build_static_name_table_v1(
newfont: &mut FontBuilder,
font: &FontRef,
family_name: &str,
style_name: &str,
aggressive: Option<RenameAggressiveness>,
) -> Result<Name, Box<dyn std::error::Error>> {
let (v1_tokens, non_weight) = style_name
.split_whitespace()
.partition::<Vec<_>, _>(|token| GF_STATIC_STYLES.iter().any(|(name, _)| name == token));
let family_tokens = family_name.split_whitespace();
let mut new_family_name = vec![];
for token in family_tokens {
if non_weight.contains(&token) || new_family_name.contains(&token) {
continue;
}
new_family_name.push(token);
}
new_family_name.extend(non_weight);
let family_name = new_family_name.join(" ");
let mut style_name = v1_tokens
.join(" ")
.replace("Regular Italic", "Italic")
.trim()
.to_string();
if style_name.is_empty() {
style_name = "Regular".to_string();
}
build_static_name_table(newfont, font, &family_name, style_name, aggressive)
}
fn build_static_name_table(
newfont: &mut FontBuilder,
font: &FontRef,
family_name: &str,
style_name: String,
aggressive: Option<RenameAggressiveness>,
) -> Result<Name, Box<dyn std::error::Error>> {
let mut name: Name = font.name()?.to_owned_table();
let mut records = name.name_record.into_iter().collect::<Vec<NameRecord>>();
records.retain(|record| record.platform_id != 1);
let existing_name = best_familyname(font).unwrap_or("New Font".to_string());
let mut removed_names: HashMap<StringId, String> = HashMap::new();
let full_name = family_name.to_string() + " " + &style_name;
let ps_name = (family_name.to_string() + "-" + &style_name).replace(" ", "");
let removeable_name_ids =
if ["Italic", "Bold Italic", "Bold", "Regular"].contains(&style_name.as_str()) {
rewrite_or_insert(&mut records, StringId::FAMILY_NAME, family_name);
rewrite_or_insert(&mut records, StringId::SUBFAMILY_NAME, &style_name);
rewrite_or_insert(&mut records, StringId::FULL_NAME, &full_name);
rewrite_or_insert(&mut records, StringId::POSTSCRIPT_NAME, &ps_name);
vec![
StringId::TYPOGRAPHIC_FAMILY_NAME,
StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
StringId::new(21),
StringId::new(22),
]
} else {
let style_tokens = style_name.split_whitespace().collect::<Vec<_>>();
let mut new_family_name_tokens = family_name.split_whitespace().collect::<Vec<_>>();
let is_italic = style_tokens.contains(&"Italic");
let additional_tokens = (style_tokens.into_iter().filter(|token| {
!(*token == "Regular"
|| *token == "Italic"
|| new_family_name_tokens.contains(token))
}))
.collect::<Vec<_>>();
new_family_name_tokens.extend(additional_tokens);
let new_family_name = new_family_name_tokens.join(" ");
let new_style_name = if is_italic { "Italic" } else { "Regular" };
rewrite_or_insert(&mut records, StringId::FAMILY_NAME, &new_family_name);
rewrite_or_insert(&mut records, StringId::SUBFAMILY_NAME, new_style_name);
rewrite_or_insert(&mut records, StringId::FULL_NAME, &full_name);
rewrite_or_insert(&mut records, StringId::POSTSCRIPT_NAME, &ps_name);
rewrite_or_insert(&mut records, StringId::TYPOGRAPHIC_FAMILY_NAME, family_name);
rewrite_or_insert(
&mut records,
StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
&style_name,
);
vec![StringId::new(21), StringId::new(22)]
};
let mut to_delete = vec![];
for name_id in removeable_name_ids.into_iter() {
if let Some(existing) = records.iter_mut().position(|r| {
r.name_id == name_id
&& r.platform_id == 3
&& r.encoding_id == 1
&& r.language_id == 0x409
}) {
removed_names.insert(name_id, records[existing].string.to_string());
to_delete.push(existing);
}
}
for i in to_delete.into_iter().rev() {
records.remove(i);
}
// If STAT table was using any removed names, add then back with a new ID
if !removed_names.is_empty() && font.table_data(Tag::from_be_bytes(*b"STAT")).is_some() {
let mut stat: Stat = font.stat()?.to_owned_table();
for axis in stat.design_axes.iter_mut() {
let id = axis.axis_name_id;
if let Some(old_name) = removed_names.get(&id) {
axis.axis_name_id = find_or_add_name(&mut records, old_name);
}
}
// Also do the axis value array
if let Some(axis_values) = stat.offset_to_axis_values.as_deref_mut() {
for axis_value in axis_values.iter_mut() {
if let Some(name_id) = axis_value.value_name_id() {
if let Some(old_name) = removed_names.get(&name_id) {
axis_value.set_value_name_id(find_or_add_name(&mut records, old_name));
}
}
}
}
newfont.add_table(&stat)?;
}
if let Some(existing) = records.iter_mut().find(|r| {
r.name_id == StringId::UNIQUE_ID
&& r.platform_id == 3
&& r.encoding_id == 1
&& r.language_id == 0x409
}) {
if let Some(new_unique) = new_unique_id(font, &full_name, &ps_name, &existing.string) {
*existing.string = new_unique;
}
}
if aggressive.unwrap_or_default() == RenameAggressiveness::Aggressive {
for record in records.iter_mut() {
if PROTECTED_IDS.contains(&record.name_id)
|| !record.string.contains(&existing_name)
{
continue;
}
if !record.string.contains(' ') {
*record.string = record
.string
.replace(&existing_name, family_name)
.replace(" ", "");
} else {
*record.string = record.string.replace(&existing_name, family_name);
}
}
}
name.name_record = records;
name.name_record.sort();
Ok(name)
}
fn new_unique_id(
font: &FontRef<'_>,
full_name: &str,
ps_name: &str,
existing: &str,
) -> Option<String> {
let new = existing.to_string();
if let Some(existing_full_name) = font
.localized_strings(StringId::FULL_NAME)
.english_or_first()
{
let existing_full_name = existing_full_name.chars().collect::<String>();
if new.contains(&existing_full_name) {
return Some(new.replace(&existing_full_name, full_name));
}
}
if let Some(existing_ps_name) = font
.localized_strings(StringId::POSTSCRIPT_NAME)
.english_or_first()
{
let existing_ps_name = existing_ps_name.chars().collect::<String>();
if new.contains(&existing_ps_name) {
return Some(new.replace(&existing_ps_name, ps_name));
}
}
None
}
fn font_axes(font: &FontRef) -> Result<Vec<FontAxis>, ReadError> {
let fvar = font.fvar().unwrap();
let mut axes = vec![];
for axis in fvar.axes()? {
let tag = axis.axis_tag().to_string();
let min = axis.min_value().to_f32();
let max = axis.max_value().to_f32();
let default = axis.default_value().to_f32();
axes.push(FontAxis {
tag,
min,
max,
default,
});
}
Ok(axes)
}
fn vf_style_name(font: &FontRef, family_name: &str) -> Result<String, ReadError> {
let axisregistry = AxisRegistry::new();
let axes: Vec<_> = font_axes(font)?;
let fvar_dflts = axisregistry.name_particles(&axes);
let mut relevant_particles: Vec<String> = axisregistry
.axis_order()
.iter()
.flat_map(|tag| fvar_dflts.get(tag))
.filter(|particle| !particle.elided)
.flat_map(|particle| particle.name.clone())
.collect::<Vec<_>>();
let family_name_tokens = family_name.split_whitespace().collect::<HashSet<_>>();
let subfamily_name = best_subfamilyname(font);
let font_styles = axisregistry
.name_table_fallbacks(
family_name,
subfamily_name.as_deref().unwrap_or("Regular"),
&axes,
)
.map(|(_tag, proto)| proto.name())
.filter(|name| !family_name_tokens.contains(name))
.map(|name| name.to_string())
.filter(|name| !relevant_particles.contains(name))
.collect::<Vec<_>>();
relevant_particles.extend(font_styles);
let name = relevant_particles
.join(" ")
.replace("Regular Italic", "Italic");
Ok(name)
}
/// Return a font with an fvar table which conforms to the Google Fonts instance spec:
/// https://github.com/googlefonts/gf-docs/tree/main/Spec#fvar-instances
pub fn build_fvar_instances(
font: FontRef,
axis_dflts: Option<HashMap<String, f32>>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let axis_registry = AxisRegistry::new();
let mut new_font = FontBuilder::new();
let mut fvar: Fvar = font.fvar().unwrap().to_owned_table();
let mut name_table: Name = font.name().unwrap().to_owned_table();
let family_name = best_familyname(&font).unwrap_or("New Font".to_string());
let style_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
// Protect name IDs which are shared with the STAT table
let mut stat_name_ids = HashSet::new();
if let Ok(stat) = font.stat() {
for axis in stat.design_axes()?.iter() {
stat_name_ids.insert(axis.axis_name_id());
}
if let Some(axis_values_offset) = stat.offset_to_axis_values().transpose()? {
for axis_value in axis_values_offset.axis_values().iter().flatten() {
stat_name_ids.insert(axis_value.value_name_id());
}
}
}
// Remove old fvar subfamily and ps name records
for instance in fvar.axis_instance_arrays.instances.iter() {
if instance.subfamily_name_id != StringId::SUBFAMILY_NAME
&& instance.subfamily_name_id != StringId::TYPOGRAPHIC_SUBFAMILY_NAME
&& !stat_name_ids.contains(&instance.subfamily_name_id)
{
name_table
.name_record
.retain(|record| record.name_id != instance.subfamily_name_id);
}
if let Some(psname) = instance.post_script_name_id {
if psname != StringId::POSTSCRIPT_NAME {
name_table
.name_record
.retain(|record| record.name_id != psname);
}
}
}
let axes = font_axes(&font)?;
let fvar_defaults = axis_registry.name_particles(&axes);
let axis_dflts = axis_dflts.unwrap_or_else(|| {
fvar_defaults
.iter()
.map(|(tag, particle)| (tag.to_string(), particle.value))
.collect()
});
let is_italic = style_name.contains("Italic");
let is_roman_and_italic =
fvar_defaults.contains_key("ital") || fvar_defaults.contains_key("slnt");
let mut fallbacks: HashMap<String, Vec<FallbackProto>> =
axis_registry.fallbacks(&axes).collect();
if !fvar_defaults.contains_key("wght") {
fallbacks.insert(
"wght".to_string(),
axis_registry
.get("wght")
.unwrap()
.fallback
.iter()
.filter(|f| f.value == Some(400.0))
.take(1)
.cloned()
.collect(),
);
}
let wght_fallbacks = fallbacks.get("wght").ok_or("No wght fallbacks")?;
let min_ital: Option<f32> = axes
.iter()
.filter(|axis| axis.tag == "ital")
.map(|axis| axis.min)
.next();
let min_slnt: Option<f32> = axes
.iter()
.filter(|axis| axis.tag == "ital")
.map(|axis| axis.min)
.next();
let mut instances = vec![];
let do_italic = if is_roman_and_italic {
vec![false, true]
} else if is_italic {
vec![true]
} else {
vec![false]
};
for italic in do_italic.into_iter() {
for fallback in wght_fallbacks.iter() {
let mut name = fallback.name.as_ref().unwrap().to_string();
if italic {
name += " Italic";
}
name = name.replace("Regular Italic", "Italic");
let mut coordinates = axis_dflts.clone();
if fvar_defaults.contains_key("wght") {
coordinates.insert("wght".to_string(), fallback.value.unwrap());
}
if italic {
if let Some(min) = min_ital {
coordinates.insert("ital".to_string(), min);
} else if let Some(min) = min_slnt {
coordinates.insert("slnt".to_string(), min);
}
}
let subfamily_name_id = add_name(&mut name_table.name_record, &name);
let post_script_name_id = add_name(
&mut name_table.name_record,
&format!("{}-{}", family_name, name).replace(" ", ""),
);
let coordinates = axes
.iter()
.map(|axis| coordinates.get(&axis.tag).cloned().unwrap_or(axis.default))
.map(|val| Fixed::from_f64(val as f64))
.collect();
instances.push(InstanceRecord {
subfamily_name_id,
flags: 0,
coordinates,
post_script_name_id: Some(post_script_name_id),
})
}
}
fvar.axis_instance_arrays.instances = instances;
new_font.add_table(&fvar)?;
name_table.name_record.sort();
new_font.add_table(&name_table)?;
Ok(new_font.copy_missing_tables(font).build())
}
// All right, let's do it
pub fn build_stat(
font: FontRef,
siblings: &[FontRef],
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut new_font = FontBuilder::new();
let axes = font_axes(&font)?;
let axis_registry = AxisRegistry::new();
let fallbacks_in_fvar: IndexMap<String, Vec<FallbackProto>> =
axis_registry.fallbacks(&axes).collect();
let mut fallbacks_in_siblings: Vec<(String, FallbackProto)> = vec![];
for fnt in siblings {
let family_name = best_familyname(fnt).unwrap_or("New Font".to_string());
let subfamily_name = best_subfamilyname(fnt).unwrap_or("Regular".to_string());
let font_axes = font_axes(fnt).unwrap_or_default();
fallbacks_in_siblings.extend(
axis_registry
.name_table_fallbacks(&family_name, &subfamily_name, &font_axes)
.map(|(tag, proto)| (tag.to_string(), proto.clone())),
)
}
// And for this font
let family_name = best_familyname(&font).unwrap_or("New Font".to_string());
let subfamily_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
let fallbacks_in_names =
axis_registry.name_table_fallbacks(&family_name, &subfamily_name, &axes);
let fvar: Fvar = font.fvar().unwrap().to_owned_table();
let mut name: Name = font.name().unwrap().to_owned_table();
let fvar_name_ids: HashSet<StringId> = fvar
.axis_instance_arrays
.instances
.iter()
.map(|x| x.subfamily_name_id)
.chain(
fvar.axis_instance_arrays
.axes
.iter()
.map(|x| x.axis_name_id),
)
.collect();
let keep = |name_id: StringId| -> bool {
name_id.to_u16() <= 25 || fvar_name_ids.contains(&name_id)
};
let mut delete_ids = vec![];
if let Ok(stat) = font.stat() {
for axis in stat.design_axes()?.iter() {
let id = axis.axis_name_id.get();
if !keep(id) {
delete_ids.push(id);
}
}
if let Some(axis_values) = stat.offset_to_axis_values().transpose()? {
for axis_value in axis_values.axis_values().iter().flatten() {
let id = axis_value.value_name_id();
if !keep(id) {
delete_ids.push(id);
}
}
}
}
name.name_record
.retain(|record| !delete_ids.contains(&record.name_id));
let mut axis_records: Vec<AxisRecord> = vec![];
let mut values: Vec<AxisValue> = vec![];
let mut seen_axes = HashSet::new();
fn make_location(axis: Tag, value: f32, linked_value: Option<f32>) -> AxisLocation {
if let Some(linked_value) = linked_value {
AxisLocation::Three {
tag: axis,
value: Fixed::from_f64(value as f64),
linked: Fixed::from_f64(linked_value as f64),
}
} else {
AxisLocation::One {
tag: axis,
value: Fixed::from_f64(value as f64),
}
}
}
for (axis, fallbacks) in fallbacks_in_fvar.iter() {
let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
let ar_axis = axis_registry.get(axis).unwrap();
seen_axes.insert(tag);
axis_records.push(AxisRecord {
tag,
name: ar_axis.display_name().to_string(),
ordering: 0,
});
let fallback_values = fallbacks.iter().map(|f| f.value()).collect::<Vec<f32>>();
for fallback in fallbacks.iter() {
values.push(AxisValue {
flags: if fallback.value() == ar_axis.default_value() {
0x2
} else {
0x0
},
name: fallback.name().to_string(),
location: make_location(
tag,
fallback.value(),
linked_value(axis, fallback.value())
.filter(|value| fallback_values.contains(value)),
),
})
}
}
for (axis, fallback) in fallbacks_in_names {
let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
if seen_axes.contains(&tag) {
continue;
}
// println!("Adding {} in names", axis);
seen_axes.insert(tag);
let ar_axis = axis_registry.get(axis).unwrap();
axis_records.push(AxisRecord {
tag,
name: ar_axis.display_name().to_string(),
ordering: 0,
});
values.push(AxisValue {
flags: 0x0,
name: fallback.name().to_string(),
location: make_location(
tag,
fallback.value(),
linked_value(axis, fallback.value()),
),
});
}
for (axis, _fallback) in fallbacks_in_siblings {
let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
if seen_axes.contains(&tag) {
continue;
}
seen_axes.insert(tag);
// println!("Adding {} in siblings", axis);
let ar_axis = axis_registry.get(&axis).unwrap();
let elided_value = ar_axis.default_value();
axis_records.push(AxisRecord {
tag,
name: ar_axis.display_name().to_string(),
ordering: 0,
});
if let Some(elided_fallback) = axis_registry.fallback_for_value(&axis, elided_value) {
values.push(AxisValue {
flags: 0x2,
name: elided_fallback.name().to_string(),
location: make_location(tag, elided_value, linked_value(&axis, elided_value)),
})
}
}
axis_records.iter_mut().enumerate().for_each(|(i, record)| {
record.ordering = i as u16;
});
let stat_builder = StatBuilder {
records: axis_records,
values,
};
let stat = stat_builder.build(&mut name.name_record);
name.name_record.sort();
new_font.add_table(&name)?;
new_font.add_table(&stat)?;
Ok(new_font.copy_missing_tables(font).build())
}
pub fn build_filename(font: FontRef, extension: &str) -> String {
let family_name = best_familyname(&font)
.unwrap_or("New Font".to_string())
.replace(" ", "");
let style_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
if font.table_data(Tag::new(b"fvar")).is_some() {
let is_italic = style_name.contains("Italic");
let axes = font_axes(&font).unwrap_or_default();
// Sort uppercase axes first, then lowercase axes
let mut axes = axes
.iter()
.map(|axis| axis.tag.to_string())
.collect::<Vec<_>>();
axes.sort();
let axes = axes.join(",");
return format!(
"{}{}[{}].{}",
family_name,
if is_italic { "-Italic" } else { "" },
axes,
extension
);
}
format!("{family_name}-{style_name}.{extension}").replace(" ", "")
}
}
#[cfg(feature = "fontations")]
pub use fontations_impl::*;
#[cfg(test)]
mod tests {
use fontations::{
skrifa::{string::StringId, MetadataProvider, Tag},
write::{
from_obj::ToOwnedTable,
tables::{
name::Name,
stat::{AxisValue, Stat},
},
},
};
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn opsz() {
let ar = AxisRegistry::new();
assert!(ar.contains_key("opsz"));
assert_eq!(ar["opsz"].display_name.as_deref(), Some("Optical Size"));
}
const MAVEN_PRO: &[u8; 83576] = include_bytes!("../tests/data/MavenPro-Regular.ttf");
const OPEN_SANS: &[u8; 532636] = include_bytes!("../tests/data/OpenSans[wdth,wght].ttf");
const OPEN_SANS_ITALIC: &[u8; 584112] =
include_bytes!("../tests/data/OpenSans-Italic[wdth,wght].ttf");
const OPEN_SANS_CONDENSED: &[u8; 973780] =
include_bytes!("../tests/data/OpenSansCondensed[wght].ttf");
const OPEN_SANS_CONDENSED_ITALIC: &[u8; 1054652] =
include_bytes!("../tests/data/OpenSansCondensed-Italic[wght].ttf");
const WONKY: &[u8; 532564] = include_bytes!("../tests/data/Wonky[wdth,wght].ttf");
const PLAYFAIR: &[u8; 1150824] = include_bytes!("../tests/data/Playfair[opsz,wdth,wght].ttf");
struct NameTableTestCase {
binary: &'static [u8],
family_name: &'static str,
subfamily_name: Option<&'static str>,
siblings: Vec<&'static [u8]>,
expectations: Vec<(StringId, Option<&'static str>)>,
}
#[test]
fn test_name_table() {
let cases: Vec<NameTableTestCase> = vec![
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Regular"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Regular")),
(StringId::FULL_NAME, Some("Maven Pro Regular")),
(StringId::POSTSCRIPT_NAME, Some("MavenPro-Regular")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Italic"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro")),
(StringId::SUBFAMILY_NAME, Some("Italic")),
(StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Italic")),
(StringId::FULL_NAME, Some("Maven Pro Italic")),
(StringId::POSTSCRIPT_NAME, Some("MavenPro-Italic")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Bold"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro")),
(StringId::SUBFAMILY_NAME, Some("Bold")),
(StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Bold")),
(StringId::FULL_NAME, Some("Maven Pro Bold")),
(StringId::POSTSCRIPT_NAME, Some("MavenPro-Bold")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Bold Italic"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro")),
(StringId::SUBFAMILY_NAME, Some("Bold Italic")),
(StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-BoldItalic")),
(StringId::FULL_NAME, Some("Maven Pro Bold Italic")),
(StringId::POSTSCRIPT_NAME, Some("MavenPro-BoldItalic")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Black"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro Black")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Black")),
(StringId::FULL_NAME, Some("Maven Pro Black")),
(StringId::POSTSCRIPT_NAME, Some("MavenPro-Black")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, Some("Black")),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Black Italic"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro Black")),
(StringId::SUBFAMILY_NAME, Some("Italic")),
(StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-BlackItalic")),
(StringId::FULL_NAME, Some("Maven Pro Black Italic")),
(StringId::POSTSCRIPT_NAME, Some("MavenPro-BlackItalic")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, Some("Black Italic")),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("ExtraLight Italic"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro ExtraLight")),
(StringId::SUBFAMILY_NAME, Some("Italic")),
(
StringId::UNIQUE_ID,
Some("2.003;NONE;MavenPro-ExtraLightItalic"),
),
(StringId::FULL_NAME, Some("Maven Pro ExtraLight Italic")),
(StringId::POSTSCRIPT_NAME, Some("MavenPro-ExtraLightItalic")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
(
StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
Some("ExtraLight Italic"),
),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("UltraExpanded Regular"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro UltraExpanded")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(
StringId::UNIQUE_ID,
Some("2.003;NONE;MavenProUltraExpanded-Regular"),
),
(StringId::FULL_NAME, Some("Maven Pro UltraExpanded Regular")),
(
StringId::POSTSCRIPT_NAME,
Some("MavenProUltraExpanded-Regular"),
),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
],
},
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Condensed ExtraLight Italic"),
siblings: vec![],
expectations: vec![
(
StringId::FAMILY_NAME,
Some("Maven Pro Condensed ExtraLight"),
),
(StringId::SUBFAMILY_NAME, Some("Italic")),
(
StringId::UNIQUE_ID,
Some("2.003;NONE;MavenProCondensed-ExtraLightItalic"),
),
(
StringId::FULL_NAME,
Some("Maven Pro Condensed ExtraLight Italic"),
),
(
StringId::POSTSCRIPT_NAME,
Some("MavenProCondensed-ExtraLightItalic"),
),
(
StringId::TYPOGRAPHIC_FAMILY_NAME,
Some("Maven Pro Condensed"),
),
(
StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
Some("ExtraLight Italic"),
),
],
},
NameTableTestCase {
binary: OPEN_SANS,
family_name: "Open Sans",
subfamily_name: None,
siblings: vec![
OPEN_SANS_ITALIC,
OPEN_SANS_CONDENSED,
OPEN_SANS_CONDENSED_ITALIC,
],
expectations: vec![
(StringId::FAMILY_NAME, Some("Open Sans")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(StringId::UNIQUE_ID, Some("3.000;GOOG;OpenSans-Regular")),
(StringId::FULL_NAME, Some("Open Sans Regular")),
(StringId::POSTSCRIPT_NAME, Some("OpenSans-Regular")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
(
StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
Some("OpenSans"),
),
],
},
NameTableTestCase {
binary: OPEN_SANS_ITALIC,
family_name: "Open Sans",
subfamily_name: None,
siblings: vec![OPEN_SANS, OPEN_SANS_CONDENSED, OPEN_SANS_CONDENSED_ITALIC],
expectations: vec![
(StringId::FAMILY_NAME, Some("Open Sans")),
(StringId::SUBFAMILY_NAME, Some("Italic")),
(StringId::UNIQUE_ID, Some("3.000;GOOG;OpenSans-Italic")),
(StringId::FULL_NAME, Some("Open Sans Italic")),
(StringId::POSTSCRIPT_NAME, Some("OpenSans-Italic")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
(
StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
Some("OpenSansItalic"),
),
],
},
NameTableTestCase {
binary: OPEN_SANS_CONDENSED,
family_name: "Open Sans Condensed",
subfamily_name: None,
siblings: vec![OPEN_SANS, OPEN_SANS_ITALIC, OPEN_SANS_CONDENSED_ITALIC],
expectations: vec![
(StringId::FAMILY_NAME, Some("Open Sans Condensed")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(
StringId::UNIQUE_ID,
Some("3.000;GOOG;OpenSansCondensed-Regular"),
),
(StringId::FULL_NAME, Some("Open Sans Condensed Regular")),
(StringId::POSTSCRIPT_NAME, Some("OpenSansCondensed-Regular")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
(
StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
Some("OpenSansCondensed"),
),
],
},
NameTableTestCase {
binary: OPEN_SANS_CONDENSED_ITALIC,
family_name: "Open Sans Condensed",
subfamily_name: None,
siblings: vec![OPEN_SANS, OPEN_SANS_ITALIC, OPEN_SANS_CONDENSED],
expectations: vec![
(StringId::FAMILY_NAME, Some("Open Sans Condensed")),
(StringId::SUBFAMILY_NAME, Some("Italic")),
(
StringId::UNIQUE_ID,
Some("3.000;GOOG;OpenSansCondensed-Italic"),
),
(StringId::FULL_NAME, Some("Open Sans Condensed Italic")),
(StringId::POSTSCRIPT_NAME, Some("OpenSansCondensed-Italic")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
(
StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
Some("OpenSansCondensedItalic"),
),
],
},
// Bad names
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Maven Pro",
subfamily_name: Some("Fat"),
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Maven Pro Fat")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(StringId::UNIQUE_ID, Some("2.003;NONE;MavenProFat-Regular")),
(StringId::FULL_NAME, Some("Maven Pro Fat Regular")),
(StringId::POSTSCRIPT_NAME, Some("MavenProFat-Regular")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
],
},
NameTableTestCase {
binary: WONKY,
family_name: "Wonky",
subfamily_name: None,
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Wonky")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(StringId::UNIQUE_ID, Some("3.000;GOOG;Wonky-Regular")),
(StringId::FULL_NAME, Some("Wonky Regular")),
(StringId::POSTSCRIPT_NAME, Some("Wonky-Regular")),
(StringId::TYPOGRAPHIC_FAMILY_NAME, None),
(StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
],
},
NameTableTestCase {
binary: PLAYFAIR,
family_name: "Playfair",
subfamily_name: None,
siblings: vec![],
expectations: vec![
(StringId::FAMILY_NAME, Some("Playfair SemiExpanded Light")),
(StringId::SUBFAMILY_NAME, Some("Regular")),
(
StringId::UNIQUE_ID,
Some("2.000;FTH;Playfair-SemiExpandedLight"),
),
(StringId::FULL_NAME, Some("Playfair SemiExpanded Light")),
(
StringId::POSTSCRIPT_NAME,
Some("Playfair-SemiExpandedLight"),
),
(StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Playfair")),
(
StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
Some("SemiExpanded Light"),
),
],
},
];
run_name_table_tests(&cases, None);
}
fn run_name_table_tests(cases: &[NameTableTestCase], aggression: Option<RenameAggressiveness>) {
for (ix, case) in cases.iter().enumerate() {
let font = FontRef::new(case.binary).expect("Failed to read font");
let siblings: Vec<FontRef> = case
.siblings
.iter()
.map(|b| FontRef::new(b).unwrap())
.collect();
let result = build_name_table(
font,
Some(case.family_name),
case.subfamily_name,
&siblings,
aggression,
)
.unwrap();
let result_font = FontRef::new(&result).unwrap();
for (id, expectation) in case.expectations.iter() {
let record = result_font.localized_strings(*id).english_or_first();
if let Some(expectation) = expectation {
assert_eq!(
record.unwrap().chars().collect::<String>(),
*expectation,
"Case {}, {}",
ix + 1,
id
);
} else {
assert!(record.is_none());
}
}
}
}
#[test]
fn test_name_table_aggression() {
let cases = [
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Raven Am",
subfamily_name: Some("Regular"),
siblings: vec![],
expectations: vec![
(StringId::COPYRIGHT_NOTICE, Some("Copyright 2011 The Raven Am Project Authors (http://www.vissol.co.uk/mavenpro/), with Reserved Font Name \"Raven Am\".")),
(StringId::FULL_NAME, Some("Raven Am Regular")),
]
}
];
run_name_table_tests(&cases, Some(RenameAggressiveness::Aggressive));
let cases = [
NameTableTestCase {
binary: MAVEN_PRO,
family_name: "Raven Am",
subfamily_name: Some("Regular"),
siblings: vec![],
expectations: vec![
(StringId::COPYRIGHT_NOTICE, Some("Copyright 2011 The Maven Pro Project Authors (http://www.vissol.co.uk/mavenpro/), with Reserved Font Name \"Maven Pro\".")),
(StringId::FULL_NAME, Some("Raven Am Regular")),
]
}
];
run_name_table_tests(&cases, Some(RenameAggressiveness::Conservative));
}
#[derive(Debug, Clone, PartialEq)]
struct DumpStatValue<'a> {
axis: Tag,
name: &'a str,
value: f32,
linked: Option<f32>,
}
fn dump_stat_values<'a>(stat: &Stat, name: &'a Name) -> Vec<DumpStatValue<'a>> {
let mut values = vec![];
let tags = stat
.design_axes
.iter()
.map(|a| a.axis_tag)
.collect::<Vec<_>>();
if let Some(axis_values) = stat.offset_to_axis_values.as_ref() {
for axis_value in axis_values.iter() {
match axis_value.as_ref() {
AxisValue::Format1(v) => values.push(DumpStatValue {
axis: tags[v.axis_index as usize],
name: name
.name_record
.iter()
.find(|record| record.name_id == v.value_name_id)
.map(|record| record.string.as_str())
.unwrap_or(""),
value: v.value.to_f32(),
linked: None,
}),
AxisValue::Format2(_) => {
panic!("I didn't produce this")
}
AxisValue::Format3(v) => values.push(DumpStatValue {
axis: tags[v.axis_index as usize],
name: name
.name_record
.iter()
.find(|record| record.name_id == v.value_name_id)
.map(|record| record.string.as_str())
.unwrap_or(""),
value: v.value.to_f32(),
linked: Some(v.linked_value.to_f32()),
}),
AxisValue::Format4(_) => {
panic!("I didn't produce this")
}
}
}
}
values
}
fn value<'a>(axis: &str, name: &'a str, value: f32, linked: Option<f32>) -> DumpStatValue<'a> {
DumpStatValue {
axis: Tag::new_checked(axis.as_bytes()).unwrap(),
name,
value,
linked,
}
}
#[test]
fn test_build_stat() {
let font_data = build_stat(
FontRef::new(OPEN_SANS).unwrap(),
&[
FontRef::new(OPEN_SANS_ITALIC).unwrap(),
FontRef::new(OPEN_SANS_CONDENSED).unwrap(),
FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap(),
],
)
.unwrap();
let new_font = FontRef::new(&font_data).unwrap();
let new_stat: Stat = new_font.stat().unwrap().to_owned_table();
let name: Name = new_font.name().unwrap().to_owned_table();
// We expect three axes, wght, wdth, and ital
assert_eq!(
new_stat
.design_axes
.iter()
.map(|a| a.axis_tag)
.collect::<Vec<_>>(),
vec![Tag::new(b"wght"), Tag::new(b"wdth"), Tag::new(b"ital")]
);
assert_eq!(
dump_stat_values(&new_stat, &name),
vec![
value("wght", "Light", 300.0, None),
value("wght", "Regular", 400.0, Some(700.0)),
value("wght", "Medium", 500.0, None),
value("wght", "SemiBold", 600.0, None),
value("wght", "Bold", 700.0, None),
value("wght", "ExtraBold", 800.0, None),
value("wdth", "Condensed", 75.0, None),
value("wdth", "SemiCondensed", 87.5, None),
value("wdth", "Normal", 100.0, None),
value("ital", "Roman", 0.0, Some(1.0)),
]
)
}
#[test]
fn test_build_stat2() {
let font_data = build_stat(
FontRef::new(OPEN_SANS_ITALIC).unwrap(),
&[
FontRef::new(OPEN_SANS).unwrap(),
FontRef::new(OPEN_SANS_CONDENSED).unwrap(),
FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap(),
],
)
.unwrap();
let new_font = FontRef::new(&font_data).unwrap();
let new_stat: Stat = new_font.stat().unwrap().to_owned_table();
let name: Name = new_font.name().unwrap().to_owned_table();
// We expect three axes, wght, wdth, and ital
assert_eq!(
new_stat
.design_axes
.iter()
.map(|a| a.axis_tag)
.collect::<Vec<_>>(),
vec![Tag::new(b"wght"), Tag::new(b"wdth"), Tag::new(b"ital")]
);
assert_eq!(
dump_stat_values(&new_stat, &name),
vec![
value("wght", "Light", 300.0, None),
value("wght", "Regular", 400.0, Some(700.0)),
value("wght", "Medium", 500.0, None),
value("wght", "SemiBold", 600.0, None),
value("wght", "Bold", 700.0, None),
value("wght", "ExtraBold", 800.0, None),
value("wdth", "Condensed", 75.0, None),
value("wdth", "SemiCondensed", 87.5, None),
value("wdth", "Normal", 100.0, None),
value("ital", "Italic", 1.0, None),
]
)
}
#[test]
fn test_build_filename() {
let maven_pro = FontRef::new(MAVEN_PRO).unwrap();
assert_eq!(build_filename(maven_pro, "ttf"), "MavenPro-Regular.ttf");
let open_sans = FontRef::new(OPEN_SANS).unwrap();
assert_eq!(build_filename(open_sans, "ttf"), "OpenSans[wdth,wght].ttf");
let open_sans_italic = FontRef::new(OPEN_SANS_ITALIC).unwrap();
assert_eq!(
build_filename(open_sans_italic, "ttf"),
"OpenSans-Italic[wdth,wght].ttf"
);
let open_sans_condensed = FontRef::new(OPEN_SANS_CONDENSED).unwrap();
assert_eq!(
build_filename(open_sans_condensed, "ttf"),
"OpenSansCondensed[wght].ttf"
);
let open_sans_condensed_italic = FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap();
assert_eq!(
build_filename(open_sans_condensed_italic, "ttf"),
"OpenSansCondensed-Italic[wght].ttf"
);
let wonky = FontRef::new(WONKY).unwrap();
assert_eq!(build_filename(wonky, "ttf"), "Wonky[wdth,wght].ttf");
let playfair = FontRef::new(PLAYFAIR).unwrap();
assert_eq!(
build_filename(playfair, "ttf"),
"Playfair[opsz,wdth,wght].ttf"
);
}
}