use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::ffi::OsStr;
use std::hash::Hash;
use std::num::ParseIntError;
use std::ops::RangeInclusive;
use std::str::FromStr;
use std::sync::{LazyLock, Mutex};
use std::{fs, path};
use crate::glyphdata::{Category, GlyphData, Subcategory};
use ascii_plist_derive::FromPlist;
use fontdrasil::types::WidthClass;
use indexmap::{IndexMap, IndexSet};
use kurbo::{Affine, CubicBez, Line, PathSeg, Point, QuadBez};
use log::{debug, warn};
use ordered_float::OrderedFloat;
use regex::Regex;
use smol_str::SmolStr;
use crate::error::Error;
use crate::plist::{FromPlist, Plist, Token, Tokenizer, VecDelimiters};
static LOGGED_WARNINGS: LazyLock<Mutex<HashSet<String>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
macro_rules! log_once_warn {
($($arg:tt)*) => {{
if log::log_enabled!(log::Level::Warn) {
let msg = format!($($arg)*);
let mut logged = LOGGED_WARNINGS.lock().unwrap();
if !logged.contains(&msg) {
log::warn!("{}", msg);
logged.insert(msg);
}
}
}};
}
const V3_METRIC_NAMES: [&str; 6] = [
"ascender",
"baseline",
"descender",
"cap height",
"x-height",
"italic angle",
];
#[derive(Clone, Debug, Default, PartialEq, Hash)]
pub struct UserToDesignMapping(BTreeMap<String, AxisUserToDesignMap>);
#[derive(Clone, Debug, Default, PartialEq, Hash)]
pub struct AxisUserToDesignMap(Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>);
#[derive(Clone, Debug, Default, PartialEq, Hash)]
pub struct Font {
pub units_per_em: u16,
pub axes: Vec<Axis>,
pub masters: Vec<FontMaster>,
pub default_master_idx: usize,
pub glyphs: BTreeMap<SmolStr, Glyph>,
pub glyph_order: Vec<SmolStr>,
pub axis_mappings: UserToDesignMapping,
pub virtual_masters: Vec<BTreeMap<String, OrderedFloat<f64>>>,
pub features: Vec<FeatureSnippet>,
pub all_names: BTreeMap<String, Vec<(String, String)>>,
pub instances: Vec<Instance>,
pub version_major: i32,
pub version_minor: u32,
pub date: Option<String>,
pub kerning_ltr: Kerning,
pub kerning_rtl: Kerning,
pub custom_parameters: CustomParameters,
pub user_data: BTreeMap<SmolStr, Plist>,
}
#[derive(Clone, Debug, PartialEq, Hash, Default)]
pub struct CustomParameters {
pub propagate_anchors: Option<bool>,
pub use_typo_metrics: Option<bool>,
pub is_fixed_pitch: Option<bool>,
pub fs_type: Option<u16>,
pub has_wws_names: Option<bool>,
pub typo_ascender: Option<i64>,
pub typo_descender: Option<i64>,
pub typo_line_gap: Option<i64>,
pub win_ascent: Option<i64>,
pub win_descent: Option<i64>,
pub hhea_ascender: Option<i64>,
pub hhea_descender: Option<i64>,
pub hhea_line_gap: Option<i64>,
pub vhea_ascender: Option<i64>,
pub vhea_descender: Option<i64>,
pub vhea_line_gap: Option<i64>,
pub underline_thickness: Option<OrderedFloat<f64>>,
pub underline_position: Option<OrderedFloat<f64>>,
pub strikeout_position: Option<i64>,
pub strikeout_size: Option<i64>,
pub subscript_x_offset: Option<i64>,
pub subscript_x_size: Option<i64>,
pub subscript_y_offset: Option<i64>,
pub subscript_y_size: Option<i64>,
pub superscript_x_offset: Option<i64>,
pub superscript_x_size: Option<i64>,
pub superscript_y_offset: Option<i64>,
pub superscript_y_size: Option<i64>,
pub unicode_range_bits: Option<BTreeSet<u32>>,
pub codepage_range_bits: Option<BTreeSet<u32>>,
pub panose: Option<Vec<i64>>,
pub head_flags: Option<Vec<i64>>,
pub lowest_rec_ppem: Option<i64>,
pub hhea_caret_slope_run: Option<i64>,
pub hhea_caret_slope_rise: Option<i64>,
pub hhea_caret_offset: Option<i64>,
pub vhea_caret_slope_run: Option<i64>,
pub vhea_caret_slope_rise: Option<i64>,
pub vhea_caret_offset: Option<i64>,
pub meta_table: Option<MetaTableValues>,
pub dont_use_production_names: Option<bool>,
pub link_metrics_with_first_master: Option<bool>,
pub link_metrics_with_master: Option<SmolStr>,
pub virtual_masters: Option<Vec<BTreeMap<String, OrderedFloat<f64>>>>,
pub glyph_order: Option<Vec<SmolStr>>,
pub gasp_table: Option<BTreeMap<i64, i64>>,
pub feature_for_feature_variations: Option<SmolStr>,
pub color_palettes: Option<Vec<Vec<Color>>>,
pub name_table_entries: Vec<NameTableEntry>,
}
#[derive(Clone, Debug, PartialEq, Hash, Default)]
pub struct MetaTableValues {
pub dlng: Vec<SmolStr>,
pub slng: Vec<SmolStr>,
}
impl MetaTableValues {
fn from_plist(plist: &Plist) -> Option<Self> {
let mut ret = MetaTableValues::default();
let as_array = plist.as_array()?;
for item in as_array {
let tag = item.get("tag").and_then(Plist::as_str)?;
let data = item.get("data").and_then(Plist::as_str)?;
let data = data.split(',').map(SmolStr::new).collect();
match tag {
"dlng" => ret.dlng = data,
"slng" => ret.slng = data,
_ => log_once_warn!("Unknown meta table tag '{tag}'"),
}
}
if ret.dlng.len() + ret.slng.len() > 0 {
Some(ret)
} else {
None
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash)]
pub struct Kerning(BTreeMap<String, BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>>);
impl Kerning {
pub fn get(&self, master_id: &str) -> Option<&BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>> {
self.0.get(master_id)
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.0.keys()
}
pub fn iter(
&self,
) -> impl Iterator<Item = (&String, &BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>)> {
self.0.iter()
}
fn insert(
&mut self,
master_id: String,
lhs_class_or_group: SmolStr,
rhs_class_or_group: SmolStr,
kern: f64,
) {
*self
.0
.entry(master_id)
.or_default()
.entry((lhs_class_or_group, rhs_class_or_group))
.or_default() = kern.into();
}
}
impl FromPlist for Kerning {
fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
let mut kerning = Kerning::default();
tokenizer.eat(b'{')?;
loop {
if tokenizer.eat(b'}').is_ok() {
break;
}
let master_id: String = tokenizer.parse()?;
tokenizer.eat(b'=')?;
tokenizer.eat(b'{')?;
loop {
if tokenizer.eat(b'}').is_ok() {
break;
}
let lhs_name_or_class: SmolStr = tokenizer.parse()?;
tokenizer.eat(b'=')?;
tokenizer.eat(b'{')?;
loop {
if tokenizer.eat(b'}').is_ok() {
break;
}
let rhs_name_or_class: SmolStr = tokenizer.parse()?;
tokenizer.eat(b'=')?;
let value: f64 = tokenizer.parse()?;
tokenizer.eat(b';')?;
kerning.insert(
master_id.clone(),
lhs_name_or_class.clone(),
rhs_name_or_class,
value,
);
}
tokenizer.eat(b';')?;
}
tokenizer.eat(b';')?;
}
Ok(kerning)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FeatureSnippet {
pub content: String,
pub disabled: bool,
}
impl FeatureSnippet {
pub fn new(content: String, disabled: bool) -> Self {
FeatureSnippet { content, disabled }
}
pub fn str_if_enabled(&self) -> Option<&str> {
(!self.disabled).then_some(&self.content)
}
}
#[derive(Clone, Default, Debug, PartialEq, Hash)]
pub struct Glyph {
pub name: SmolStr,
pub export: bool,
pub layers: Vec<Layer>,
pub bracket_layers: Vec<Layer>,
pub unicode: BTreeSet<u32>,
pub left_kern: Option<SmolStr>,
pub right_kern: Option<SmolStr>,
pub category: Option<Category>,
pub sub_category: Option<Subcategory>,
pub production_name: Option<SmolStr>,
pub smart_component_axes: BTreeMap<SmolStr, RangeInclusive<i64>>,
}
impl Glyph {
pub fn is_nonspacing_mark(&self) -> bool {
matches!(
(self.category, self.sub_category),
(Some(Category::Mark), Some(Subcategory::Nonspacing))
)
}
pub fn has_components(&self) -> bool {
self.layers
.iter()
.chain(self.bracket_layers.iter())
.flat_map(Layer::components)
.next()
.is_some()
}
}
impl fontdrasil::util::CompositeLike for Glyph {
fn name(&self) -> SmolStr {
self.name.clone()
}
fn has_components(&self) -> bool {
Glyph::has_components(self)
}
fn component_names(&self) -> impl Iterator<Item = SmolStr> {
self.layers
.iter()
.chain(self.bracket_layers.iter())
.flat_map(|layer| layer.shapes.iter())
.filter_map(|shape| match shape {
Shape::Path(..) => None,
Shape::Component(c) => Some(c.name.clone()),
})
}
}
#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AxisPole {
Min,
Max,
}
#[derive(Clone, Debug, Default, PartialEq, FromPlist)]
pub struct RawHint {
#[fromplist(alt_name = "type")]
hint_type: SmolStr,
name: SmolStr,
origin: RawHint2Tuple, scale: Option<RawHint2Tuple>,
options: i64, }
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct RawHint2Tuple {
x: f64,
y: f64,
}
impl FromPlist for RawHint2Tuple {
fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
if matches!(tokenizer.peek(), Ok(Token::String(_))) {
let s = tokenizer.parse::<SmolStr>().unwrap();
let s = s.trim_matches(['{', '}']);
let Some((x, y)) = s.split_once(',') else {
return Err(crate::plist::Error::ExpectedComma);
};
let x = x
.trim()
.parse()
.map_err(|_| crate::plist::Error::ExpectedNumber)?;
let y = y
.trim()
.parse()
.map_err(|_| crate::plist::Error::ExpectedNumber)?;
return Ok(RawHint2Tuple { x, y });
}
let origin: Vec<f64> = tokenizer.parse()?;
Ok(RawHint2Tuple {
x: origin.first().copied().unwrap_or_default(),
y: origin.get(1).copied().unwrap_or_default(),
})
}
}
impl RawHint {
fn to_hint(&self) -> Result<Hint, Error> {
let type_ = match self.hint_type.as_str() {
"Corner" => HintType::Corner,
"Cap" => HintType::Cap,
"Stem" => HintType::Stem,
"TTStem" => HintType::TTStem,
_other => {
log::info!("unhandled hint type '{_other}'");
HintType::Unknown
}
};
let shape_index = self.origin.x as usize;
let node_index = self.origin.y as usize;
let scale = self
.scale
.map(|RawHint2Tuple { x, y }| Scale::new(x, y))
.unwrap_or_default();
let alignment = match self.options {
0 => Alignment::OutStroke,
1 => Alignment::InStroke,
2 => Alignment::Middle,
3 => Alignment::Unused,
4 => Alignment::Unaligned,
_other => {
log::info!("unexpected hint option '{_other}'");
Default::default()
}
};
Ok(Hint {
type_,
name: self.name.clone(),
shape_index,
node_index,
scale,
alignment,
})
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct Hint {
pub type_: HintType,
pub name: SmolStr,
pub shape_index: usize,
pub node_index: usize,
pub scale: Scale,
pub alignment: Alignment,
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct Scale {
pub x: OrderedFloat<f64>,
pub y: OrderedFloat<f64>,
}
impl Default for Scale {
fn default() -> Self {
Self {
x: 1.0.into(),
y: 1.0.into(),
}
}
}
impl Scale {
fn new(x: f64, y: f64) -> Self {
Self {
x: x.into(),
y: y.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
#[non_exhaustive]
pub enum HintType {
Corner,
Stem,
Cap,
TTStem,
Unknown,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Hash)]
#[non_exhaustive]
pub enum Alignment {
#[default]
OutStroke,
InStroke,
Middle,
Unused,
Unaligned,
}
#[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct Layer {
pub layer_id: String,
pub associated_master_id: Option<String>,
pub width: OrderedFloat<f64>,
pub vert_width: Option<OrderedFloat<f64>>,
pub vert_origin: Option<OrderedFloat<f64>>,
pub shapes: Vec<Shape>,
pub anchors: Vec<Anchor>,
pub attributes: LayerAttributes,
pub smart_component_positions: BTreeMap<SmolStr, AxisPole>,
pub hints: Vec<Hint>,
}
impl Layer {
pub fn is_master(&self) -> bool {
self.associated_master_id.is_none()
}
pub fn master_id(&self) -> &str {
self.associated_master_id
.as_deref()
.unwrap_or(&self.layer_id)
}
pub fn is_intermediate(&self) -> bool {
self.associated_master_id.is_some() && !self.attributes.coordinates.is_empty()
}
pub fn is_color(&self) -> bool {
self.attributes.color || self.attributes.color_palette.is_some()
}
pub(crate) fn components(&self) -> impl Iterator<Item = &Component> + '_ {
self.shapes.iter().filter_map(|shape| match shape {
Shape::Path(_) => None,
Shape::Component(comp) => Some(comp),
})
}
fn bracket_info(&self, axes: &[Axis]) -> BTreeMap<String, (Option<i64>, Option<i64>)> {
assert!(
!self.attributes.axis_rules.is_empty(),
"all bracket layers have axis rules"
);
axes.iter()
.zip(&self.attributes.axis_rules)
.map(|(axis, rule)| (axis.tag.clone(), (rule.min, rule.max)))
.collect()
}
fn axis_rules_sort_key(&self) -> Vec<(i64, i64)> {
self.attributes
.axis_rules
.iter()
.map(|ax| (ax.min.unwrap_or(i64::MIN), ax.max.unwrap_or(i64::MAX)))
.collect()
}
pub(crate) fn get_anchor_pt(&self, anchor_name: &str) -> Option<Point> {
self.anchors
.iter()
.find(|a| a.name == anchor_name)
.map(|a| a.pos)
}
}
#[derive(Clone, Default, Debug, PartialEq, Hash)]
pub struct LayerAttributes {
pub coordinates: Vec<OrderedFloat<f64>>,
pub color: bool,
pub axis_rules: Vec<AxisRule>,
pub color_palette: Option<i64>,
}
#[derive(Clone, Default, FromPlist, Debug, PartialEq, Hash)]
pub struct AxisRule {
pub min: Option<i64>,
pub max: Option<i64>,
}
impl AxisRule {
fn from_layer_name(name: &str) -> Option<Self> {
let idx = name.find([']', '['])?;
let reversed = name.as_bytes()[idx] == b']';
let tail = name.get(idx + 1..)?;
let (value, _) = tail.split_once(']')?;
let value = str::parse::<u32>(value.trim()).ok()?;
let (min, max) = if reversed {
(None, Some(value as _))
} else {
(Some(value as _), None)
};
Some(AxisRule { min, max })
}
}
impl FromPlist for LayerAttributes {
fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
let mut coordinates = Vec::new();
let mut color = false;
let mut axis_rules = Vec::new();
let mut color_palette = None;
tokenizer.eat(b'{')?;
loop {
if tokenizer.eat(b'}').is_ok() {
break;
}
let key: String = tokenizer.parse()?;
tokenizer.eat(b'=')?;
match key.as_str() {
"coordinates" => coordinates = tokenizer.parse()?,
"color" => color = tokenizer.parse()?,
"axisRules" => axis_rules = tokenizer.parse()?,
"colorPalette" => {
let value = match tokenizer.peek()?.as_str() {
Some("*") => {
tokenizer.lex()?;
0xFFFF
}
_ => tokenizer.parse()?,
};
color_palette = Some(value);
}
_ => tokenizer.skip_rec()?,
}
tokenizer.eat(b';')?;
}
Ok(LayerAttributes {
coordinates,
color,
axis_rules,
color_palette,
})
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
pub struct ShapeAttributes {
pub gradient: Option<Gradient>,
pub fill_color: Option<Color>,
}
impl ShapeAttributes {
pub fn colors(&self) -> impl Iterator<Item = &Color> {
self.gradient
.iter()
.flat_map(|g| g.colors())
.chain(self.fill_color.iter())
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
pub struct Gradient {
pub start: Vec<OrderedFloat<f64>>,
pub end: Vec<OrderedFloat<f64>>,
pub colors: Vec<ColorStop>,
#[fromplist(key = "type")]
pub style: String,
}
impl Gradient {
fn colors(&self) -> impl Iterator<Item = &Color> {
self.colors.iter().map(|c| &c.color)
}
}
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
pub struct Color {
pub r: i64,
pub g: i64,
pub b: i64,
pub a: i64,
}
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
pub struct ColorStop {
pub color: Color,
pub stop_offset: OrderedFloat<f64>,
}
impl Color {
pub fn rgba(r: i64, g: i64, b: i64, a: i64) -> Self {
Self { r, g, b, a }
}
pub(crate) fn from_glyphs_color(colors: &[i64]) -> Result<Self, crate::plist::Error> {
match *colors {
[black, alpha] => Ok(Color::rgba(black, black, black, alpha)),
[r, g, b, a] => Ok(Color::rgba(r, g, b, a)),
_ => Err(crate::plist::Error::UnexpectedNumberOfValues {
value_type: "grayscale (2) or rgba (4)",
actual: colors.len(),
}),
}
}
}
impl FromPlist for Color {
fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
let colors = tokenizer.parse::<Vec<i64>>()?;
Color::from_glyphs_color(&colors)
}
}
impl FromPlist for ColorStop {
fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
tokenizer.eat(b'(')?;
let color = Color::parse(tokenizer)?;
tokenizer.eat(b',')?;
let stop_offset = tokenizer.parse::<f64>()?;
tokenizer.eat(b')')?;
Ok(ColorStop {
color,
stop_offset: stop_offset.into(),
})
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Shape {
Path(Path),
Component(Component),
}
impl Shape {
pub fn attributes(&self) -> &ShapeAttributes {
match self {
Shape::Path(p) => &p.attributes,
Shape::Component(c) => &c.attributes,
}
}
pub(crate) fn as_path(&self) -> Option<&Path> {
match self {
Shape::Path(path) => Some(path),
_ => None,
}
}
pub(crate) fn as_component(&self) -> Option<&Component> {
match self {
Shape::Component(component) => Some(component),
_ => None,
}
}
pub(crate) fn reverse(&mut self) {
if let Shape::Path(path) = self {
path.reverse();
if path.closed && !path.nodes.is_empty() {
path.nodes.rotate_left(1);
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
enum FormatVersion {
#[default]
V2,
V3,
}
#[derive(Default, Debug, PartialEq, FromPlist)]
#[allow(non_snake_case)]
struct RawFont {
#[fromplist(key = ".formatVersion")]
format_version: FormatVersion,
units_per_em: Option<i64>,
metrics: Vec<RawMetric>,
family_name: String,
date: Option<String>,
copyright: Option<String>,
designer: Option<String>,
designerURL: Option<String>,
manufacturer: Option<String>,
manufacturerURL: Option<String>,
versionMajor: Option<i64>,
versionMinor: Option<i64>,
axes: Vec<Axis>,
glyphs: Vec<RawGlyph>,
font_master: Vec<RawFontMaster>,
instances: Vec<RawInstance>,
feature_prefixes: Vec<RawFeature>,
features: Vec<RawFeature>,
classes: Vec<RawFeature>,
properties: Vec<RawName>,
#[fromplist(alt_name = "kerning")]
kerning_LTR: Kerning,
kerning_RTL: Kerning,
custom_parameters: RawCustomParameters,
numbers: Vec<NumberName>,
user_data: BTreeMap<SmolStr, Plist>,
}
#[derive(Default, Debug, PartialEq, FromPlist)]
struct NumberName {
name: SmolStr,
}
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)]
pub(crate) struct RawCustomParameters(Vec<RawCustomParameterValue>);
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
struct RawCustomParameterValue {
name: SmolStr,
value: Plist,
disabled: Option<bool>,
}
impl RawCustomParameterValue {
fn taken(&self) -> bool {
self == &Self::default()
}
}
impl FromPlist for FormatVersion {
fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
let raw: i64 = FromPlist::parse(tokenizer)?;
if raw == 3 {
Ok(FormatVersion::V3)
} else {
Err(crate::plist::Error::Parse(
"'3' is the only known format version".into(),
))
}
}
}
impl FormatVersion {
fn is_v2(self) -> bool {
self == FormatVersion::V2
}
fn codepoint_radix(self) -> u32 {
match self {
FormatVersion::V2 => 16,
FormatVersion::V3 => 10,
}
}
}
impl FromPlist for RawCustomParameters {
fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
Vec::parse(tokenizer).map(RawCustomParameters)
}
}
trait PlistParamsExt {
fn as_codepage_bits(&self) -> Option<BTreeSet<u32>>;
fn as_unicode_code_ranges(&self) -> Option<BTreeSet<u32>>;
fn as_fs_type(&self) -> Option<u16>;
fn as_mapping_values(&self) -> Option<Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>>;
fn as_axis_location(&self) -> Option<AxisLocation>;
fn as_axis(&self) -> Option<Axis>;
fn as_bool(&self) -> Option<bool>;
fn as_ordered_f64(&self) -> Option<OrderedFloat<f64>>;
fn as_vec_of_ints(&self) -> Option<Vec<i64>>;
fn as_axis_locations(&self) -> Option<Vec<AxisLocation>>;
fn as_vec_of_string(&self) -> Option<Vec<SmolStr>>;
fn as_axes(&self) -> Option<Vec<Axis>>;
fn as_axis_mappings(&self) -> Option<Vec<AxisMapping>>;
fn as_virtual_master(&self) -> Option<BTreeMap<String, OrderedFloat<f64>>>;
fn as_gasp_table(&self) -> Option<BTreeMap<i64, i64>>;
fn as_color_palettes(&self) -> Option<Vec<Vec<Color>>>;
}
impl PlistParamsExt for Plist {
fn as_bool(&self) -> Option<bool> {
self.as_i64().map(|val| val == 1)
}
fn as_ordered_f64(&self) -> Option<OrderedFloat<f64>> {
self.as_f64().map(OrderedFloat)
}
fn as_vec_of_ints(&self) -> Option<Vec<i64>> {
if let Some(thing_that_makes_sense) = self.as_array()?.iter().map(Plist::as_i64).collect() {
return Some(thing_that_makes_sense);
}
self.as_array()?
.iter()
.map(|val| val.as_str().and_then(|s| s.parse::<i64>().ok()))
.collect()
}
fn as_axis_location(&self) -> Option<AxisLocation> {
let plist = self.as_dict()?;
let name = plist.get("Axis").and_then(Plist::as_str)?;
let location = plist.get("Location").and_then(Plist::as_f64)?;
Some(AxisLocation {
axis_name: name.into(),
location: location.into(),
})
}
fn as_axis_locations(&self) -> Option<Vec<AxisLocation>> {
let array = self.as_array()?;
array.iter().map(Plist::as_axis_location).collect()
}
fn as_vec_of_string(&self) -> Option<Vec<SmolStr>> {
self.as_array()?
.iter()
.map(|plist| plist.as_str().map(SmolStr::from))
.collect()
}
fn as_axis(&self) -> Option<Axis> {
let plist = self.as_dict()?;
let name = plist.get("Name").and_then(Plist::as_str)?;
let tag = plist.get("Tag").and_then(Plist::as_str)?;
let hidden = plist.get("hidden").and_then(Plist::as_bool);
Some(Axis {
name: name.into(),
tag: tag.into(),
hidden,
})
}
fn as_axes(&self) -> Option<Vec<Axis>> {
self.as_array()?.iter().map(Plist::as_axis).collect()
}
fn as_mapping_values(&self) -> Option<Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>> {
let plist = self.as_dict()?;
plist
.iter()
.map(|(key, value)| {
let key = key
.parse::<f64>()
.or_else(|_| key.parse::<i64>().map(|i| i as f64))
.ok()?;
let val = value.as_f64()?;
Some((key.into(), val.into()))
})
.collect()
}
fn as_axis_mappings(&self) -> Option<Vec<AxisMapping>> {
self.as_dict()?
.iter()
.map(|(tag, mappings)| {
let user_to_design = mappings.as_mapping_values()?;
Some(AxisMapping {
tag: tag.to_string(),
user_to_design,
})
})
.collect()
}
fn as_virtual_master(&self) -> Option<BTreeMap<String, OrderedFloat<f64>>> {
self.as_array()?
.iter()
.map(Plist::as_axis_location)
.map(|loc| loc.map(|loc| (loc.axis_name, loc.location)))
.collect()
}
fn as_fs_type(&self) -> Option<u16> {
Some(self.as_vec_of_ints()?.iter().map(|bit| 1 << bit).sum())
}
fn as_unicode_code_ranges(&self) -> Option<BTreeSet<u32>> {
let bits = self.as_vec_of_ints()?;
Some(bits.iter().map(|bit| *bit as u32).collect())
}
fn as_codepage_bits(&self) -> Option<BTreeSet<u32>> {
self.as_array()?
.iter()
.map(|val| {
if let Some(n) = val.as_i64() {
return codepage_range_bit(n as u32);
}
if let Some(s) = val.as_str() {
if let Ok(n) = s.parse::<u32>() {
return codepage_range_bit(n);
}
if let Some(rest) = s.strip_prefix("bit ")
&& let Ok(bit) = rest.parse::<u32>()
{
return codepage_range_bit(bit);
}
}
Err(Error::InvalidCodePage(0))
})
.collect::<Result<_, _>>()
.ok()
}
fn as_gasp_table(&self) -> Option<BTreeMap<i64, i64>> {
Some(
self.as_dict()?
.iter()
.filter_map(|(k, v)| k.parse::<i64>().ok().zip(v.as_i64()))
.collect(),
)
}
fn as_color_palettes(&self) -> Option<Vec<Vec<Color>>> {
let raw_palettes = self.as_array()?;
let mut palettes = Vec::with_capacity(raw_palettes.len());
for raw_palette in raw_palettes {
let Some(raw_palette) = raw_palette.as_array() else {
warn!("Non-array color palette, ignored");
continue;
};
let mut palette = Vec::with_capacity(raw_palette.len());
for raw_color in raw_palette {
let Some(raw_color) = raw_color.as_vec_of_ints() else {
warn!("Non-int-array color palette entry, ignored");
continue;
};
let Ok(color) = Color::from_glyphs_color(&raw_color) else {
warn!("Invalid color palette entry, ignored");
continue;
};
palette.push(color);
}
palettes.push(palette);
}
Some(palettes)
}
}
impl RawCustomParameters {
fn to_custom_params(&self) -> Result<CustomParameters, Error> {
let mut params = CustomParameters::default();
let mut virtual_masters = Vec::<BTreeMap<String, OrderedFloat<f64>>>::new();
let mut panose = None;
let mut panose_old = None;
for param in &self.0 {
if param.taken() {
continue;
}
let name = ¶m.name;
let value = ¶m.value;
let disabled = ¶m.disabled;
macro_rules! add_and_report_issues {
($field:ident, $converter:path) => {{ add_and_report_issues!($field, $converter(value)) }};
($field:ident, $converter:path, into) => {{ add_and_report_issues!($field, $converter(value).map(Into::into)) }};
($field:ident, $value_expr:expr_2021) => {{
let value = $value_expr;
if value.is_none() {
log::warn!("failed to parse param for '{}'", stringify!($field));
}
if params.$field.is_none() {
params.$field = value;
} else {
log::warn!(
"ignoring duplicate param value for field '{}'",
stringify!($field)
);
}
}};
}
if *disabled == Some(true) {
log::debug!("skipping disabled custom param '{name}'");
continue;
}
match name.as_str() {
"Propagate Anchors" => add_and_report_issues!(propagate_anchors, Plist::as_bool),
"Use Typo Metrics" => add_and_report_issues!(use_typo_metrics, Plist::as_bool),
"postscriptIsFixedPitch" | "isFixedPitch" => {
add_and_report_issues!(is_fixed_pitch, Plist::as_bool)
}
"Has WWS Names" => add_and_report_issues!(has_wws_names, Plist::as_bool),
"typoAscender" => add_and_report_issues!(typo_ascender, Plist::as_i64),
"typoDescender" => add_and_report_issues!(typo_descender, Plist::as_i64),
"typoLineGap" => add_and_report_issues!(typo_line_gap, Plist::as_i64),
"winAscent" => add_and_report_issues!(win_ascent, Plist::as_i64),
"winDescent" => add_and_report_issues!(win_descent, Plist::as_i64),
"hheaAscender" => add_and_report_issues!(hhea_ascender, Plist::as_i64),
"hheaDescender" => add_and_report_issues!(hhea_descender, Plist::as_i64),
"hheaLineGap" => add_and_report_issues!(hhea_line_gap, Plist::as_i64),
"vheaVertAscender" => add_and_report_issues!(vhea_ascender, Plist::as_i64),
"vheaVertDescender" => add_and_report_issues!(vhea_descender, Plist::as_i64),
"vheaVertLineGap" => add_and_report_issues!(vhea_line_gap, Plist::as_i64),
"underlineThickness" => {
add_and_report_issues!(underline_thickness, Plist::as_ordered_f64)
}
"underlinePosition" => {
add_and_report_issues!(underline_position, Plist::as_ordered_f64)
}
"strikeoutPosition" | "openTypeOS2StrikeoutPosition" => {
add_and_report_issues!(strikeout_position, Plist::as_i64)
}
"strikeoutSize" | "openTypeOS2StrikeoutSize" => {
add_and_report_issues!(strikeout_size, Plist::as_i64)
}
"subscriptXOffset" | "openTypeOS2SubscriptXOffset" => {
add_and_report_issues!(subscript_x_offset, Plist::as_i64)
}
"subscriptXSize" | "openTypeOS2SubscriptXSize" => {
add_and_report_issues!(subscript_x_size, Plist::as_i64)
}
"subscriptYOffset" | "openTypeOS2SubscriptYOffset" => {
add_and_report_issues!(subscript_y_offset, Plist::as_i64)
}
"subscriptYSize" | "openTypeOS2SubscriptYSize" => {
add_and_report_issues!(subscript_y_size, Plist::as_i64)
}
"superscriptXOffset" | "openTypeOS2SuperscriptXOffset" => {
add_and_report_issues!(superscript_x_offset, Plist::as_i64)
}
"superscriptXSize" | "openTypeOS2SuperscriptXSize" => {
add_and_report_issues!(superscript_x_size, Plist::as_i64)
}
"superscriptYOffset" | "openTypeOS2SuperscriptYOffset" => {
add_and_report_issues!(superscript_y_offset, Plist::as_i64)
}
"superscriptYSize" | "openTypeOS2SuperscriptYSize" => {
add_and_report_issues!(superscript_y_size, Plist::as_i64)
}
"fsType" | "openTypeOS2Type" => add_and_report_issues!(fs_type, Plist::as_fs_type),
"unicodeRanges" | "openTypeOS2UnicodeRanges" => {
add_and_report_issues!(unicode_range_bits, Plist::as_unicode_code_ranges)
}
"codePageRanges" => {
add_and_report_issues!(codepage_range_bits, Plist::as_codepage_bits)
}
"openTypeHeadLowestRecPPEM" => {
add_and_report_issues!(lowest_rec_ppem, Plist::as_i64)
}
"openTypeHheaCaretSlopeRun" => {
add_and_report_issues!(hhea_caret_slope_run, Plist::as_i64)
}
"openTypeHheaCaretSlopeRise" => {
add_and_report_issues!(hhea_caret_slope_rise, Plist::as_i64)
}
"openTypeHheaCaretOffset" => {
add_and_report_issues!(hhea_caret_offset, Plist::as_i64)
}
"openTypeVheaCaretSlopeRun" => {
add_and_report_issues!(vhea_caret_slope_run, Plist::as_i64)
}
"openTypeVheaCaretSlopeRise" => {
add_and_report_issues!(vhea_caret_slope_rise, Plist::as_i64)
}
"openTypeVheaCaretOffset" => {
add_and_report_issues!(vhea_caret_offset, Plist::as_i64)
}
"meta Table" => {
add_and_report_issues!(meta_table, MetaTableValues::from_plist)
}
"Don't use Production Names" => {
add_and_report_issues!(dont_use_production_names, Plist::as_bool)
}
"Link Metrics With First Master" => {
add_and_report_issues!(link_metrics_with_first_master, Plist::as_bool)
}
"Link Metrics With Master" => {
add_and_report_issues!(link_metrics_with_master, Plist::as_str, into)
}
"openTypeHeadFlags" => {
add_and_report_issues!(head_flags, Plist::as_vec_of_ints)
}
"openTypeOS2FamilyClass" => {
log_once_warn!("unhandled custom param '{name}'")
}
"Virtual Master" => match value.as_virtual_master() {
Some(val) => virtual_masters.push(val),
None => log::warn!("failed to parse virtual master '{value:?}'"),
},
"panose" => panose = value.as_vec_of_ints(),
"openTypeOS2Panose" => panose_old = value.as_vec_of_ints(),
"glyphOrder" => add_and_report_issues!(glyph_order, Plist::as_vec_of_string),
"gasp Table" => add_and_report_issues!(gasp_table, Plist::as_gasp_table),
"Feature for Feature Variations" => {
add_and_report_issues!(feature_for_feature_variations, Plist::as_str, into)
}
"Color Palettes" => {
add_and_report_issues!(color_palettes, Plist::as_color_palettes)
}
"Name Table Entry" => match value
.as_str()
.ok_or_else(|| "should be a string".into())
.and_then(NameTableEntry::from_str)
{
Ok(s) => params.name_table_entries.push(s),
Err(e) => log::warn!("bad 'Name Table Entry': {e}"),
},
_ => log_once_warn!("unknown custom parameter '{name}'"),
}
}
params.panose = panose.or(panose_old);
params.virtual_masters = Some(virtual_masters).filter(|x| !x.is_empty());
Ok(params)
}
fn contains(&self, name: &str) -> bool {
self.0.iter().any(|val| val.name == name)
}
fn take(&mut self, name: &str) -> Option<Plist> {
let pos = self
.0
.iter()
.position(|val| val.name == name && val.disabled != Some(true))?;
let taken = std::mem::take(&mut self.0[pos]);
Some(taken.value)
}
fn take_string(&mut self, name: &str) -> Option<String> {
self.take(name)
.and_then(|p| p.as_str().map(|s| s.to_owned()))
}
fn take_axes(&mut self) -> Option<Vec<Axis>> {
self.take("Axes").and_then(|p| p.as_axes())
}
fn take_axis_mappings(&mut self) -> Option<Vec<AxisMapping>> {
self.take("Axis Mappings")
.and_then(|p| p.as_axis_mappings())
}
fn take_axis_locations(&mut self) -> Option<Vec<AxisLocation>> {
self.take("Axis Location")
.and_then(|p| p.as_axis_locations())
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct AxisLocation {
axis_name: String,
location: OrderedFloat<f64>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct AxisMapping {
tag: String,
user_to_design: Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
struct RawMetric {
type_: String,
}
#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
struct RawName {
key: String,
value: Option<String>,
values: Vec<RawNameValue>,
}
#[derive(Clone, Debug, PartialEq, Hash, Default)]
pub struct NameTableEntry {
pub name_id: u16,
pub platform_id: u16,
pub encoding_id: u16,
pub lang_id: u16,
pub value: String,
}
fn parse_maybe_hex_int(s: &str) -> Result<u16, ParseIntError> {
match s.strip_prefix("0x") {
Some(hex) => u16::from_str_radix(hex, 16),
None => s.parse(),
}
}
impl FromStr for NameTableEntry {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((head, tail)) = s.split_once(';') else {
return Err(format!("expected ';', found '{s}'"));
};
let mut iter = head.split(" ").flat_map(parse_maybe_hex_int);
let value = tail.trim().into();
match (iter.next(), iter.next(), iter.next(), iter.next()) {
(Some(name_id), None | Some(3), None, None) => Ok(NameTableEntry {
name_id,
platform_id: 3,
encoding_id: 1,
lang_id: 0x409,
value,
}),
(Some(name_id), Some(1), None, None) => Ok(NameTableEntry {
name_id,
platform_id: 1,
encoding_id: 0,
lang_id: 0,
value,
}),
(Some(name_id), Some(platform_id), Some(encoding_id), None) => Ok(NameTableEntry {
name_id,
platform_id,
encoding_id,
lang_id: 0,
value,
}),
(Some(a), Some(b), Some(c), Some(d)) => Ok(NameTableEntry {
name_id: a,
platform_id: b,
encoding_id: c,
lang_id: d,
value,
}),
_ => Err(format!("expected four numerical ids, found '{s}'")),
}
}
}
fn default_language_score(lang: &str, index: usize, scale: i32) -> i32 {
match lang {
"dflt" => -scale * 3 - index as i32,
"default" => -scale * 2 - index as i32,
"ENG" => -scale - index as i32,
_ => index as i32,
}
}
impl RawName {
fn is_empty(&self) -> bool {
self.value.is_none() && self.values.is_empty()
}
fn get_value(&self) -> Option<&str> {
if let Some(value) = &self.value {
return Some(value.as_str());
}
let scale = self.values.len() as i32;
self.values
.iter()
.enumerate()
.map(|(i, raw)| {
(
default_language_score(raw.language.as_str(), i, scale),
raw.value.as_str(),
)
})
.min()
.map(|(_, raw)| raw)
}
fn localized_values(&self) -> impl Iterator<Item = (&str, &str)> {
self.values
.iter()
.map(|raw| (raw.language.as_str(), raw.value.as_str()))
}
}
#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
struct RawNameValue {
language: String,
value: String,
}
#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
struct RawFeature {
automatic: Option<i64>,
disabled: Option<i64>,
name: Option<String>,
tag: Option<String>,
notes: Option<String>,
code: String,
labels: Vec<RawNameValue>,
#[fromplist(ignore)]
other_stuff: BTreeMap<String, Plist>,
}
#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
pub struct Axis {
#[fromplist(alt_name = "Name")]
pub name: String,
#[fromplist(alt_name = "Tag")]
pub tag: String,
pub hidden: Option<bool>,
}
#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
struct RawGlyph {
layers: Vec<RawLayer>,
glyphname: SmolStr,
export: Option<bool>,
#[fromplist(alt_name = "leftKerningGroup")]
kern_left: Option<SmolStr>,
#[fromplist(alt_name = "rightKerningGroup")]
kern_right: Option<SmolStr>,
unicode: Option<String>,
category: Option<SmolStr>,
sub_category: Option<SmolStr>,
#[fromplist(alt_name = "production")]
production_name: Option<SmolStr>,
parts_settings: Vec<RawPartSetting>,
#[fromplist(ignore)]
other_stuff: BTreeMap<String, Plist>,
}
#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
struct RawPartSetting {
name: SmolStr,
bottom_name: Option<SmolStr>,
bottom_value: i64,
top_name: Option<SmolStr>,
top_value: i64,
}
#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
struct RawLayer {
name: String,
layer_id: String,
associated_master_id: Option<String>,
width: Option<OrderedFloat<f64>>,
vert_width: Option<OrderedFloat<f64>>,
vert_origin: Option<OrderedFloat<f64>>,
shapes: Vec<RawShape>,
paths: Vec<Path>,
components: Vec<Component>,
anchors: Vec<RawAnchor>,
hints: Vec<RawHint>,
#[fromplist(alt_name = "attr")]
attributes: LayerAttributes,
part_selection: BTreeMap<SmolStr, i64>,
user_data: LayerUserData,
#[fromplist(ignore)]
other_stuff: BTreeMap<String, Plist>,
}
#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
struct LayerUserData {
#[fromplist(alt_name = "PartSelection")]
part_selection: BTreeMap<SmolStr, i64>,
}
impl RawLayer {
fn is_draft(&self) -> bool {
self.associated_master_id.is_some()
&& self.attributes == Default::default()
&& self.part_selection.is_empty() && self.user_data.part_selection.is_empty() }
fn is_bracket_layer(&self, format_version: FormatVersion) -> bool {
self.associated_master_id.is_some()
&& self.associated_master_id.as_ref() != Some(&self.layer_id)
&& match format_version {
FormatVersion::V2 => AxisRule::from_layer_name(&self.name).is_some(),
FormatVersion::V3 => !self.attributes.axis_rules.is_empty(),
}
}
fn v2_to_v3_attributes(&mut self) {
let mut brace_coordinates = Vec::new();
if let (Some(start), Some(end)) = (self.name.find('{'), self.name.find('}')) {
let mut tokenizer = Tokenizer::new(&self.name[start..=end]);
brace_coordinates = tokenizer
.parse_delimited_vec(VecDelimiters::CSV_IN_BRACES)
.unwrap_or_default();
}
if !brace_coordinates.is_empty() {
self.attributes.coordinates = brace_coordinates;
}
}
}
#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
struct RawShape {
closed: Option<bool>,
nodes: Vec<Node>,
#[fromplist(alt_name = "ref", alt_name = "name")]
glyph_name: Option<SmolStr>,
piece: BTreeMap<SmolStr, f64>,
anchor: Option<SmolStr>,
transform: Option<String>, pos: Vec<f64>, angle: Option<f64>, scale: Vec<f64>, slant: Vec<f64>,
#[fromplist(alt_name = "attr")]
attributes: ShapeAttributes,
}
#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
pub struct Path {
pub closed: bool,
pub nodes: Vec<Node>,
pub attributes: ShapeAttributes,
}
#[derive(Default, Clone, Debug, FromPlist)]
pub struct Component {
pub name: SmolStr,
pub transform: Affine,
pub anchor: Option<SmolStr>,
#[fromplist(alt_name = "piece")]
pub smart_component_values: BTreeMap<SmolStr, f64>,
pub attributes: ShapeAttributes,
}
impl PartialEq for Component {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& Into::<AffineForEqAndHash>::into(self.transform) == other.transform.into()
}
}
impl Eq for Component {}
impl Hash for Component {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
Into::<AffineForEqAndHash>::into(self.transform).hash(state);
}
}
#[derive(Clone, Copy, Debug)]
pub struct Node {
pub pt: Point,
pub node_type: NodeType,
}
impl Node {
pub fn is_on_curve(&self) -> bool {
!matches!(self.node_type, NodeType::OffCurve)
}
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
Into::<PointForEqAndHash>::into(self.pt) == other.pt.into()
&& self.node_type == other.node_type
}
}
impl Eq for Node {}
impl Hash for Node {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
PointForEqAndHash::new(self.pt).hash(state);
self.node_type.hash(state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NodeType {
Line,
LineSmooth,
OffCurve,
Curve,
CurveSmooth,
QCurve,
QCurveSmooth,
}
#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
struct RawAnchor {
name: SmolStr,
pos: Option<Point>, position: Option<String>, }
#[derive(Clone, Debug, PartialEq)]
pub struct Anchor {
pub name: SmolStr,
pub pos: Point,
}
impl Hash for Anchor {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
PointForEqAndHash::new(self.pos).hash(state);
}
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct FontMaster {
pub id: String,
pub name: String,
pub axes_values: Vec<OrderedFloat<f64>>,
metric_values: BTreeMap<String, MetricValue>,
pub number_values: BTreeMap<SmolStr, OrderedFloat<f64>>,
pub custom_parameters: CustomParameters,
pub user_data: BTreeMap<SmolStr, Plist>,
pub metrics_source_id: Option<String>,
}
impl FontMaster {
fn read_metric(&self, metric_name: &str) -> Option<f64> {
self.metric_values
.get(metric_name)
.map(|metric| metric.pos.into_inner())
}
pub fn ascender(&self) -> Option<f64> {
self.read_metric("ascender")
}
pub fn descender(&self) -> Option<f64> {
self.read_metric("descender")
}
pub fn x_height(&self) -> Option<f64> {
self.read_metric("x-height")
}
pub fn cap_height(&self) -> Option<f64> {
self.read_metric("cap height")
}
pub fn italic_angle(&self) -> Option<f64> {
self.read_metric("italic angle")
}
}
fn resolve_metrics_source_id(
custom_params: &CustomParameters,
master_ids_to_names: &IndexMap<String, Option<String>>,
) -> Option<String> {
if custom_params.link_metrics_with_first_master == Some(true) {
return master_ids_to_names.first().map(|(id, _)| id.clone());
}
if let Some(ref source_ref) = custom_params.link_metrics_with_master {
if master_ids_to_names.contains_key(source_ref.as_str()) {
return Some(source_ref.to_string());
}
if let Some((id, _)) = master_ids_to_names
.iter()
.find(|(_, name)| name.as_deref() == Some(source_ref.as_str()))
{
return Some(id.clone());
}
log::warn!("Source master for metrics not found: '{source_ref}'");
}
None
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
struct RawFontMaster {
id: String,
name: Option<String>,
weight: Option<String>,
width: Option<String>,
custom: Option<String>,
weight_value: Option<OrderedFloat<f64>>,
interpolation_weight: Option<OrderedFloat<f64>>,
width_value: Option<OrderedFloat<f64>>,
interpolation_width: Option<OrderedFloat<f64>>,
custom_value: Option<OrderedFloat<f64>>,
typo_ascender: Option<i64>,
typo_descender: Option<OrderedFloat<f64>>,
typo_line_gap: Option<OrderedFloat<f64>>,
win_ascender: Option<OrderedFloat<f64>>,
win_descender: Option<OrderedFloat<f64>>,
axes_values: Vec<OrderedFloat<f64>>,
metric_values: Vec<RawMetricValue>,
ascender: Option<OrderedFloat<f64>>, baseline: Option<OrderedFloat<f64>>, descender: Option<OrderedFloat<f64>>, cap_height: Option<OrderedFloat<f64>>, x_height: Option<OrderedFloat<f64>>, #[fromplist(alt_name = "italic angle")]
italic_angle: Option<OrderedFloat<f64>>,
alignment_zones: Vec<String>,
custom_parameters: RawCustomParameters,
number_values: Vec<OrderedFloat<f64>>,
user_data: BTreeMap<SmolStr, Plist>,
#[fromplist(ignore)]
other_stuff: BTreeMap<String, Plist>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
struct RawMetricValue {
pos: Option<OrderedFloat<f64>>,
over: Option<OrderedFloat<f64>>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct MetricValue {
pos: OrderedFloat<f64>,
over: OrderedFloat<f64>,
}
impl From<RawMetricValue> for MetricValue {
fn from(src: RawMetricValue) -> MetricValue {
MetricValue {
pos: src.pos.unwrap_or_default(),
over: src.over.unwrap_or_default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct Instance {
pub name: String,
pub active: bool,
pub type_: InstanceType,
pub axis_mappings: BTreeMap<String, AxisUserToDesignMap>,
pub axes_values: Vec<OrderedFloat<f64>>,
pub custom_parameters: CustomParameters,
properties: Vec<RawName>, }
#[derive(Clone, Debug, PartialEq, Hash)]
pub enum InstanceType {
Single,
Variable,
}
impl From<&str> for InstanceType {
fn from(value: &str) -> Self {
if value.eq_ignore_ascii_case("variable") {
InstanceType::Variable
} else {
InstanceType::Single
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
struct RawInstance {
name: String,
exports: Option<i64>,
active: Option<i64>,
type_: Option<String>,
axes_values: Vec<OrderedFloat<f64>>,
weight_value: Option<OrderedFloat<f64>>,
interpolation_weight: Option<OrderedFloat<f64>>,
width_value: Option<OrderedFloat<f64>>,
interpolation_width: Option<OrderedFloat<f64>>,
custom_value: Option<OrderedFloat<f64>>,
interpolation_custom: Option<OrderedFloat<f64>>,
weight_class: Option<String>,
width_class: Option<String>,
properties: Vec<RawName>,
custom_parameters: RawCustomParameters,
}
impl RawInstance {
fn is_active(&self) -> bool {
self.exports.unwrap_or(1) != 0 && self.active.unwrap_or(1) != 0
}
}
trait GlyphsV2OrderedAxes {
fn weight_value(&self) -> Option<OrderedFloat<f64>>;
fn interpolation_weight(&self) -> Option<OrderedFloat<f64>>;
fn width_value(&self) -> Option<OrderedFloat<f64>>;
fn interpolation_width(&self) -> Option<OrderedFloat<f64>>;
fn custom_value(&self) -> Option<OrderedFloat<f64>>;
fn interpolation_custom(&self) -> Option<OrderedFloat<f64>>;
fn value_for_nth_axis(&self, nth_axis: usize) -> Result<OrderedFloat<f64>, Error> {
Ok(match nth_axis {
0 => self
.weight_value()
.or(self.interpolation_weight())
.unwrap_or(100.0.into()),
1 => self
.width_value()
.or(self.interpolation_width())
.unwrap_or(100.0.into()),
2 => self
.custom_value()
.or(self.interpolation_custom())
.unwrap_or(0.0.into()),
_ => {
return Err(Error::StructuralError(format!(
"We don't know what field to use for axis {nth_axis}"
)));
}
})
}
fn axis_values(&self, axes: &[Axis]) -> Result<Vec<OrderedFloat<f64>>, Error> {
(0..axes.len())
.map(|nth_axis| self.value_for_nth_axis(nth_axis))
.collect::<Result<Vec<OrderedFloat<f64>>, Error>>()
}
}
impl GlyphsV2OrderedAxes for RawFontMaster {
fn weight_value(&self) -> Option<OrderedFloat<f64>> {
self.weight_value
}
fn interpolation_weight(&self) -> Option<OrderedFloat<f64>> {
self.interpolation_weight
}
fn width_value(&self) -> Option<OrderedFloat<f64>> {
self.width_value
}
fn interpolation_width(&self) -> Option<OrderedFloat<f64>> {
self.interpolation_width
}
fn custom_value(&self) -> Option<OrderedFloat<f64>> {
self.custom_value
}
fn interpolation_custom(&self) -> Option<OrderedFloat<f64>> {
None
}
}
impl GlyphsV2OrderedAxes for RawInstance {
fn weight_value(&self) -> Option<OrderedFloat<f64>> {
self.weight_value
}
fn interpolation_weight(&self) -> Option<OrderedFloat<f64>> {
self.interpolation_weight
}
fn width_value(&self) -> Option<OrderedFloat<f64>> {
self.width_value
}
fn interpolation_width(&self) -> Option<OrderedFloat<f64>> {
self.interpolation_width
}
fn custom_value(&self) -> Option<OrderedFloat<f64>> {
self.custom_value
}
fn interpolation_custom(&self) -> Option<OrderedFloat<f64>> {
self.interpolation_custom
}
}
fn parse_node_from_string(value: &str) -> Node {
let mut spl = value.splitn(3, ' ');
let x = spl.next().unwrap().parse().unwrap();
let y = spl.next().unwrap().parse().unwrap();
let pt = Point::new(x, y);
let mut raw_node_type = spl.next().unwrap();
if raw_node_type.contains('{') {
raw_node_type = raw_node_type.split('{').next().unwrap().trim_end();
}
let node_type = raw_node_type.parse().unwrap();
Node { pt, node_type }
}
fn parse_node_from_tokenizer(tokenizer: &mut Tokenizer<'_>) -> Result<Node, crate::plist::Error> {
let x: f64 = tokenizer.parse()?;
tokenizer.eat(b',')?;
let y: f64 = tokenizer.parse()?;
tokenizer.eat(b',')?;
let node_type: String = tokenizer.parse()?;
let node_type = NodeType::from_str(&node_type)
.map_err(|_| crate::plist::Error::Parse(format!("unknown node type '{node_type}'")))?;
if tokenizer.eat(b',').is_ok() {
tokenizer.skip_rec()?;
}
Ok(Node {
pt: Point { x, y },
node_type,
})
}
impl std::str::FromStr for NodeType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"LINE" => Ok(NodeType::Line),
"LINE SMOOTH" => Ok(NodeType::LineSmooth),
"OFFCURVE" => Ok(NodeType::OffCurve),
"CURVE" => Ok(NodeType::Curve),
"CURVE SMOOTH" => Ok(NodeType::CurveSmooth),
"QCURVE" => Ok(NodeType::QCurve),
"QCURVE SMOOTH" => Ok(NodeType::QCurveSmooth),
"l" => Ok(NodeType::Line),
"ls" => Ok(NodeType::LineSmooth),
"o" => Ok(NodeType::OffCurve),
"c" => Ok(NodeType::Curve),
"cs" => Ok(NodeType::CurveSmooth),
"q" => Ok(NodeType::QCurve),
"qs" => Ok(NodeType::QCurveSmooth),
_ => Err(format!("unknown node type {s}")),
}
}
}
impl FromPlist for Node {
fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
use crate::plist::Error;
let tok = tokenizer.lex()?;
let node = match &tok {
Token::Atom(value) => parse_node_from_string(value),
Token::String(value) => parse_node_from_string(value),
Token::OpenParen => {
let node = parse_node_from_tokenizer(tokenizer)?;
tokenizer.eat(b')')?;
node
}
_ => return Err(Error::ExpectedString),
};
Ok(node)
}
}
impl Path {
pub fn new(closed: bool) -> Path {
Path {
nodes: Vec::new(),
closed,
..Default::default()
}
}
pub fn add(&mut self, pt: impl Into<Point>, node_type: NodeType) {
let pt = pt.into();
self.nodes.push(Node { pt, node_type });
}
pub fn rotate_left(&mut self, delta: usize) {
self.nodes.rotate_left(delta);
}
pub fn reverse(&mut self) {
self.nodes.reverse();
}
pub(crate) fn get_previous_segment(&self, idx: usize) -> Option<PathSeg> {
if self.nodes.len() < 2 {
return None;
}
let end = self.nodes.get(idx)?.pt;
let mut idx = self.prev_idx(idx);
let prev = self.nodes.get(idx)?;
if prev.node_type == NodeType::OffCurve {
idx = self.prev_idx(idx);
let control1 = self.nodes.get(idx)?;
if control1.node_type != NodeType::OffCurve {
log::info!("path for corner component contains unexpected quadratic");
return Some(QuadBez::new(control1.pt, prev.pt, end).into());
}
let start = self.nodes.get(self.prev_idx(idx))?;
Some(CubicBez::new(start.pt, control1.pt, prev.pt, end).into())
} else {
Some(Line::new(prev.pt, end).into())
}
}
pub(crate) fn get_next_segment(&self, idx: usize) -> Option<PathSeg> {
if self.nodes.len() < 2 {
return None;
}
let start = self.nodes.get(idx)?.pt;
let mut idx = self.next_idx(idx);
let next = self.nodes.get(idx)?;
if next.node_type == NodeType::OffCurve {
idx = self.next_idx(idx);
let control2 = self.nodes.get(idx)?;
if control2.node_type != NodeType::OffCurve {
log::info!("path for corner component contains unexpected quadratic");
return Some(QuadBez::new(start, next.pt, control2.pt).into());
}
let end = self.nodes.get(self.next_idx(idx))?;
Some(CubicBez::new(start, next.pt, control2.pt, end.pt).into())
} else {
Some(Line::new(start, next.pt).into())
}
}
pub(crate) fn prev_idx(&self, idx: usize) -> usize {
if idx == 0 {
self.nodes.len().saturating_sub(1)
} else {
idx - 1
}
}
pub(crate) fn next_idx(&self, idx: usize) -> usize {
(idx + 1) % self.nodes.len()
}
#[cfg(test)]
pub(crate) fn to_points(&self) -> Vec<Point> {
self.nodes.iter().map(|n| n.pt).collect()
}
}
fn v2_to_v3_name(v2_prop: Option<&str>, v3_name: &str) -> Option<RawName> {
v2_prop.map(|value| {
if v3_name.ends_with('s') {
RawName {
key: v3_name.into(),
value: None,
values: vec![RawNameValue {
language: "dflt".into(),
value: value.to_string(),
}],
}
} else {
RawName {
key: v3_name.into(),
value: Some(value.to_string()),
values: vec![],
}
}
})
}
impl RawFont {
pub fn load_from_string(raw_content: &str) -> Result<Self, crate::plist::Error> {
let raw_content = preprocess_unparsed_plist(raw_content);
Self::parse_plist(&raw_content)
}
pub fn load(glyphs_file: &path::Path) -> Result<Self, Error> {
if glyphs_file.extension() == Some(OsStr::new("glyphspackage")) {
return Self::load_package(glyphs_file);
}
debug!("Read glyphs {glyphs_file:?}");
let raw_content = fs::read_to_string(glyphs_file).map_err(Error::IoError)?;
Self::load_from_string(&raw_content)
.map_err(|e| Error::ParseError(glyphs_file.to_path_buf(), e.to_string()))
}
fn load_package(glyphs_package: &path::Path) -> Result<RawFont, Error> {
if !glyphs_package.is_dir() {
return Err(Error::NotAGlyphsPackage(glyphs_package.to_path_buf()));
}
debug!("Read glyphs package {glyphs_package:?}");
let fontinfo_file = glyphs_package.join("fontinfo.plist");
let fontinfo_data = fs::read_to_string(&fontinfo_file).map_err(Error::IoError)?;
let mut raw_font = RawFont::parse_plist(&fontinfo_data)
.map_err(|e| Error::ParseError(fontinfo_file.to_path_buf(), format!("{e}")))?;
let mut glyphs: HashMap<SmolStr, RawGlyph> = HashMap::new();
let glyphs_dir = glyphs_package.join("glyphs");
if glyphs_dir.is_dir() {
for entry in fs::read_dir(glyphs_dir).map_err(Error::IoError)? {
let entry = entry.map_err(Error::IoError)?;
let path = entry.path();
if path.extension() == Some(OsStr::new("glyph")) {
let glyph_data = fs::read_to_string(&path).map_err(Error::IoError)?;
let glyph_data = preprocess_unparsed_plist(&glyph_data);
let glyph = RawGlyph::parse_plist(&glyph_data)
.map_err(|e| Error::ParseError(path.clone(), e.to_string()))?;
if glyph.glyphname.is_empty() {
return Err(Error::ParseError(
path.clone(),
"Glyph dict must have a 'glyphname' key".to_string(),
));
}
glyphs.insert(glyph.glyphname.clone(), glyph);
}
}
}
let order_file = glyphs_package.join("order.plist");
let mut ordered_glyphs = Vec::new();
if order_file.exists() {
let order_data = fs::read_to_string(&order_file).map_err(Error::IoError)?;
let order_plist = Plist::parse(&order_data)
.map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
let order = order_plist
.expect_array()
.map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
for glyph_name in order {
let glyph_name = glyph_name
.expect_string()
.map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
if let Some(glyph) = glyphs.remove(glyph_name.as_str()) {
ordered_glyphs.push(glyph);
}
}
}
let mut glyph_names: Vec<_> = glyphs.keys().cloned().collect();
glyph_names.sort();
ordered_glyphs.extend(
glyph_names
.into_iter()
.map(|glyph_name| glyphs.remove(&glyph_name).unwrap()),
);
assert!(glyphs.is_empty());
raw_font.glyphs = ordered_glyphs;
Ok(raw_font)
}
fn v2_to_v3_axes(&mut self) -> Result<Vec<String>, Error> {
let mut tags = Vec::new();
if let Some(v2_axes) = self.custom_parameters.take_axes() {
for v2_axis in v2_axes {
tags.push(v2_axis.tag.clone());
self.axes.push(v2_axis.clone());
}
}
if self.axes.is_empty() {
self.axes.push(Axis {
name: "Weight".into(),
tag: "wght".into(),
hidden: None,
});
self.axes.push(Axis {
name: "Width".into(),
tag: "wdth".into(),
hidden: None,
});
self.axes.push(Axis {
name: "Custom".into(),
tag: "XXXX".into(),
hidden: None,
});
}
if self.axes.len() > 3 {
return Err(Error::StructuralError(
"We only understand 0..3 axes for Glyphs v2".into(),
));
}
for master in self.font_master.iter_mut() {
master.axes_values = master.axis_values(&self.axes)?;
}
for instance in self.instances.iter_mut() {
instance.axes_values = instance.axis_values(&self.axes)?;
}
Ok(tags)
}
fn v2_to_v3_metrics(&mut self) -> Result<(), Error> {
self.metrics = V3_METRIC_NAMES
.iter()
.map(|n| RawMetric {
type_: n.to_string(),
})
.collect();
let mut used_metrics = [false; 6];
for m in &self.font_master {
for (i, val) in [
m.ascender,
m.baseline,
m.descender,
m.cap_height,
m.x_height,
m.italic_angle,
]
.iter()
.enumerate()
{
used_metrics[i] |= val.is_some();
}
}
self.metrics = V3_METRIC_NAMES
.into_iter()
.zip(used_metrics)
.filter(|(_, used)| *used)
.map(|(name, _)| RawMetric {
type_: name.to_string(),
})
.collect();
let mut non_metric_alignment_zones = vec![vec![]; self.font_master.len()];
for (i, master) in self.font_master.iter_mut().enumerate() {
let mut metric_values = vec![RawMetricValue::default(); self.metrics.len()];
for (metric_idx, name) in self.metrics.iter().enumerate() {
let value = match name.type_.as_ref() {
"ascender" => master.ascender,
"baseline" => master.baseline,
"descender" => master.descender,
"cap height" => master.cap_height,
"x-height" => master.x_height,
"italic angle" => master.italic_angle,
_ => unreachable!("only these values exist in V3_METRIC_NAMES"),
};
metric_values[metric_idx].pos = value;
}
for alignment_zone in &master.alignment_zones {
let Some((pos, over)) = parse_alignment_zone(alignment_zone) else {
warn!("Confusing alignment zone '{alignment_zone}', skipping");
continue;
};
if over == 0. {
continue;
}
if pos == 0. {
if let Some((idx, _)) = self
.metrics
.iter()
.enumerate()
.find(|(_, name)| name.type_ == "baseline")
{
metric_values[idx].over = Some(over);
}
continue;
}
if let Some(metric) = metric_values.iter_mut().find(|x| x.pos == Some(pos)) {
metric.over = Some(over);
} else {
non_metric_alignment_zones[i].push((pos, over))
}
}
master.metric_values = metric_values;
}
let mut new_metrics = HashMap::new();
for pos in non_metric_alignment_zones
.iter()
.flat_map(|master| master.iter().map(|(pos, _)| *pos))
{
if !new_metrics.contains_key(&pos) {
let next_zone = new_metrics.len() + 1;
let idx = self.metrics.len();
self.metrics.push(RawMetric {
type_: format!("zone {next_zone}"),
});
new_metrics.insert(pos, idx);
}
}
let new_metrics: BTreeMap<_, _> = new_metrics.into_iter().map(|(k, v)| (v, k)).collect();
for (idx, metrics) in non_metric_alignment_zones.into_iter().enumerate() {
for pos_to_add in new_metrics.values().copied() {
let to_add = metrics.iter().copied().find_map(|(pos, over)| {
(pos == pos_to_add).then_some(RawMetricValue {
pos: Some(pos),
over: Some(over),
})
});
self.font_master[idx]
.metric_values
.push(to_add.unwrap_or_default());
}
}
Ok(())
}
fn v2_to_v3_master_names(&mut self) -> Result<(), Error> {
for master in self.font_master.iter_mut() {
if master.name.is_some() {
continue;
}
let mut names = [
master.width.as_deref(),
master.weight.as_deref(),
master.custom.as_deref(),
]
.iter()
.flatten()
.flat_map(|n| n.split_ascii_whitespace())
.filter(|x| *x != "Regular")
.collect::<Vec<_>>();
if let Some(italic_angle) = master.italic_angle
&& italic_angle != 0.0
&& (names.is_empty()
|| !names
.iter()
.any(|name| name.contains("Italic") || name.contains("Oblique")))
{
names.push("Italic");
}
master.name = if names.is_empty() {
Some("Regular".into())
} else {
Some(names.join(" "))
};
}
Ok(())
}
fn v2_to_v3_names(&mut self) -> Result<(), Error> {
let mut properties = std::mem::take(&mut self.properties);
properties.extend(v2_to_v3_name(self.copyright.as_deref(), "copyrights"));
properties.extend(v2_to_v3_name(self.designer.as_deref(), "designers"));
properties.extend(v2_to_v3_name(self.designerURL.as_deref(), "designerURL"));
properties.extend(v2_to_v3_name(self.manufacturer.as_deref(), "manufacturers"));
properties.extend(v2_to_v3_name(
self.manufacturerURL.as_deref(),
"manufacturerURL",
));
let mut v2_to_v3_param = |v2_names: &[&str], v3_name: &str| {
if properties.iter().any(|n| n.key == v3_name && !n.is_empty()) {
return;
}
for v2_name in v2_names {
if let Some(value) = v2_to_v3_name(
self.custom_parameters.take_string(v2_name).as_deref(),
v3_name,
) {
properties.push(value);
return;
}
}
};
v2_to_v3_param(&["description", "openTypeNameDescription"], "descriptions");
v2_to_v3_param(&["licenseURL", "openTypeNameLicenseURL"], "licenseURL");
v2_to_v3_param(&["versionString", "openTypeNameVersion"], "versionString");
v2_to_v3_param(&["compatibleFullName"], "compatibleFullNames");
v2_to_v3_param(&["license", "openTypeNameLicense"], "licenses");
v2_to_v3_param(&["uniqueID", "openTypeNameUniqueID"], "uniqueID");
v2_to_v3_param(&["trademark"], "trademarks");
v2_to_v3_param(&["sampleText", "openTypeNameSampleText"], "sampleTexts");
v2_to_v3_param(&["postscriptFullName"], "postscriptFullName");
v2_to_v3_param(&["postscriptFontName"], "postscriptFontName");
v2_to_v3_param(
&["WWSFamilyName", "openTypeNameWWSFamilyName"],
"WWSFamilyName",
);
v2_to_v3_param(&["vendorID", "openTypeOS2VendorID"], "vendorID");
self.properties = properties;
Ok(())
}
fn v2_to_v3_instances(&mut self) -> Result<(), Error> {
for instance in self.instances.iter_mut() {
if let Some(custom_weight_class) = instance.custom_parameters.take("weightClass") {
instance.weight_class = custom_weight_class.to_string().into();
}
for (tag, opt) in [
("wght", &mut instance.weight_class),
("wdth", &mut instance.width_class),
] {
let Some(value) = opt.as_ref() else {
continue;
};
if f64::from_str(value).is_ok() {
continue;
};
let Some(value) = lookup_class_value(tag, value) else {
return Err(Error::UnknownValueName(value.clone()));
};
let _ = opt.insert(value.to_string());
}
instance.properties.extend(v2_to_v3_name(
instance
.custom_parameters
.take_string("postscriptFontName")
.as_deref(),
"postscriptFontName",
));
}
Ok(())
}
fn v2_to_v3_layer_attributes(&mut self) {
for raw_glyph in self.glyphs.iter_mut() {
for layer in raw_glyph.layers.iter_mut() {
layer.v2_to_v3_attributes();
}
}
}
fn v2_to_v3(&mut self) -> Result<(), Error> {
self.v2_to_v3_master_names()?;
self.v2_to_v3_axes()?;
self.v2_to_v3_metrics()?;
self.v2_to_v3_instances()?;
self.v2_to_v3_names()?; self.v2_to_v3_layer_attributes();
Ok(())
}
}
fn parse_alignment_zone(zone: &str) -> Option<(OrderedFloat<f64>, OrderedFloat<f64>)> {
let (one, two) = zone.split_once(',')?;
let one = one.trim_start_matches(['{', ' ']).parse::<i32>().ok()?;
let two = two.trim_start().trim_end_matches('}').parse::<i32>().ok()?;
Some((OrderedFloat(one as f64), OrderedFloat(two as f64)))
}
fn make_glyph_order(glyphs: &[RawGlyph], custom_order: Option<Vec<SmolStr>>) -> Vec<SmolStr> {
let mut valid_names: HashSet<_> = glyphs.iter().map(|g| &g.glyphname).collect();
let mut glyph_order = Vec::new();
for name in custom_order.into_iter().flatten() {
if valid_names.remove(&name) {
glyph_order.push(name.clone());
}
}
glyph_order.extend(
glyphs
.iter()
.filter(|g| valid_names.contains(&g.glyphname))
.map(|g| g.glyphname.clone()),
);
glyph_order
}
fn parse_codepoint_str(s: &str, radix: u32) -> BTreeSet<u32> {
s.split(',')
.map(|cp| u32::from_str_radix(cp, radix).unwrap())
.collect()
}
fn default_master_idx(raw_font: &mut RawFont) -> usize {
if let Some(master_idx) = raw_font
.custom_parameters
.take_string("Variable Font Origin")
.and_then(|origin| {
raw_font
.font_master
.iter()
.position(|master| master.id == origin)
})
{
return master_idx;
}
let contenders = raw_font
.font_master
.iter()
.enumerate()
.filter_map(|(i, m)| {
m.name
.as_deref()
.map(|name| (i, whitespace_separated_tokens(name)))
})
.collect::<Vec<_>>();
if contenders.is_empty() {
return 0;
}
let mut common_words = contenders[0].1.clone();
for (_, words) in contenders.iter().skip(1) {
common_words.retain(|w| words.contains(w));
}
let mut best_idx = 0;
for (idx, mut words) in contenders {
if *common_words == words {
best_idx = idx;
break;
}
words.retain(|w| *w != "Regular");
if *common_words == words {
best_idx = idx;
}
}
best_idx
}
fn whitespace_separated_tokens(s: &str) -> Vec<&str> {
s.split_whitespace().collect()
}
fn axis_index(axes: &[Axis], pred: impl Fn(&Axis) -> bool) -> Option<usize> {
axes.iter()
.enumerate()
.find_map(|(i, a)| if pred(a) { Some(i) } else { None })
}
fn user_to_design_from_axis_mapping(
from: &mut RawFont,
) -> Option<BTreeMap<String, AxisUserToDesignMap>> {
let mappings = from.custom_parameters.take_axis_mappings()?;
let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
for mapping in mappings {
let Some(axis_index) = axis_index(&from.axes, |a| a.tag == mapping.tag) else {
log::warn!(
"axis mapping includes tag {:?} not included in font",
mapping.tag
);
continue;
};
let axis_name = &from.axes.get(axis_index).unwrap().name;
for (user, design) in mapping.user_to_design.iter() {
axis_mappings
.entry(axis_name.clone())
.or_default()
.add_if_new(*user, *design);
}
}
if axis_mappings.is_empty() {
return None;
}
Some(axis_mappings)
}
fn user_to_design_from_axis_location(
from: &mut RawFont,
) -> Option<BTreeMap<String, AxisUserToDesignMap>> {
let master_locations: Vec<_> = from
.font_master
.iter_mut()
.filter_map(|m| m.custom_parameters.take_axis_locations())
.collect();
if master_locations.len() != from.font_master.len() {
if !master_locations.is_empty() {
warn!(
"{}/{} masters have Axis Location; ignoring",
master_locations.len(),
from.font_master.len()
);
}
return None;
}
let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
for (master, axis_locations) in from.font_master.iter().zip(master_locations) {
for (idx, axis) in from.axes.iter().enumerate() {
let Some(axis_location) = axis_locations.iter().find(|loc| loc.axis_name == axis.name)
else {
continue;
};
let user = axis_location.location;
let design = master.axes_values[idx];
let old = axis_mappings
.entry(axis_location.axis_name.clone())
.or_default()
.add_or_replace(user, design);
if let Some(old) = old {
let name = master.name.as_deref().unwrap_or(&master.id);
warn!(
"Axis {}: master '{}' redefines mapping for user \
location {user} from {old} to {design}",
axis.name, name
);
}
}
}
Some(axis_mappings)
}
impl AxisUserToDesignMap {
fn add_if_new(&mut self, user: OrderedFloat<f64>, design: OrderedFloat<f64>) {
if self.0.iter().any(|(u, _)| *u == user) {
return;
}
self.0.push((user, design));
}
fn add_or_replace(
&mut self,
user: OrderedFloat<f64>,
design: OrderedFloat<f64>,
) -> Option<OrderedFloat<f64>> {
if let Some(entry) = self.0.iter_mut().find(|(u, _)| *u == user) {
if entry.1 != design {
let old = entry.1;
entry.1 = design;
return Some(old);
}
} else {
self.0.push((user, design));
}
None
}
fn add_identity_map(&mut self, value: OrderedFloat<f64>) {
if self.0.iter().any(|(u, d)| *u == value || *d == value) {
return;
}
self.0.push((value, value));
}
pub fn iter(&self) -> impl Iterator<Item = &(OrderedFloat<f64>, OrderedFloat<f64>)> {
self.0.iter()
}
pub fn is_identity(&self) -> bool {
self.0.iter().all(|(u, d)| u == d)
}
}
impl UserToDesignMapping {
fn new(from: &mut RawFont, instances: &[Instance]) -> Self {
let from_axis_mapping = user_to_design_from_axis_mapping(from);
let from_axis_location = user_to_design_from_axis_location(from);
let (result, incomplete_mapping) = match (from_axis_mapping, from_axis_location) {
(Some(from_mapping), Some(..)) => {
warn!("Axis Mapping *and* Axis Location are defined; using Axis Mapping");
(from_mapping, false)
}
(Some(from_mapping), None) => (from_mapping, false),
(None, Some(from_location)) => (from_location, true),
(None, None) => (BTreeMap::new(), true),
};
let mut result = Self(result);
if incomplete_mapping {
result.add_instance_mappings(instances);
if result.0.is_empty() || result.0.values().all(|v| v.is_identity()) {
result.add_master_mappings_if_new(from);
}
}
result
}
pub fn contains(&self, axis_name: &str) -> bool {
self.0.contains_key(axis_name)
}
pub fn get(&self, axis_name: &str) -> Option<&AxisUserToDesignMap> {
self.0.get(axis_name)
}
fn add_instance_mappings(&mut self, instances: &[Instance]) {
for instance in instances
.iter()
.filter(|i| i.active && i.type_ == InstanceType::Single)
{
for (axis_name, inst_mapping) in instance.axis_mappings.iter() {
let entry = self.0.entry(axis_name.clone()).or_default();
for (user, design) in inst_mapping.iter() {
if let Some(old) = entry.add_or_replace(*user, *design) {
warn!(
"Axis {axis_name}: instance '{}' redefines mapping for user \
location {user} from {old} to {design}",
instance.name
);
}
}
}
}
}
fn add_master_mappings_if_new(&mut self, from: &RawFont) {
for master in from.font_master.iter() {
for (axis, value) in from.axes.iter().zip(&master.axes_values) {
self.0
.entry(axis.name.clone())
.or_default()
.add_identity_map(*value);
}
}
}
}
impl TryFrom<RawShape> for Shape {
type Error = Error;
fn try_from(from: RawShape) -> Result<Self, Self::Error> {
let shape = if let Some(glyph_name) = from.glyph_name {
assert!(!glyph_name.is_empty(), "A pointless component");
let mut transform = if let Some(transform) = from.transform {
Affine::parse_plist(&transform)?
} else {
Affine::IDENTITY
};
if !from.pos.is_empty() {
if from.pos.len() != 2 {
return Err(Error::StructuralError(format!("Bad pos: {:?}", from.pos)));
}
transform *= Affine::translate((from.pos[0], from.pos[1]));
}
if !from.slant.is_empty() {
if from.slant.len() != 2 {
return Err(Error::StructuralError(format!(
"Bad slant: {:?}",
from.slant
)));
}
transform *= Affine::skew(
from.slant[0].to_radians().tan(),
from.slant[1].to_radians().tan(),
);
}
if let Some(angle) = from.angle {
transform *= normalized_rotation(angle);
}
if !from.scale.is_empty() {
if from.scale.len() != 2 {
return Err(Error::StructuralError(format!(
"Bad scale: {:?}",
from.scale
)));
}
transform *= Affine::scale_non_uniform(from.scale[0], from.scale[1]);
}
Shape::Component(Component {
name: glyph_name,
transform,
anchor: from.anchor,
attributes: from.attributes,
smart_component_values: from.piece,
})
} else {
Shape::Path(Path {
closed: from.closed.unwrap_or_default(),
nodes: from.nodes.clone(),
attributes: from.attributes,
})
};
Ok(shape)
}
}
fn normalized_rotation(angle_deg: f64) -> Affine {
const ROT_90: Affine = Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]);
const ROT_180: Affine = Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
const ROT_270: Affine = Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]);
let normalized_angle = angle_deg.rem_euclid(360.0);
match normalized_angle {
0.0 => Affine::IDENTITY,
90.0 => ROT_90,
180.0 => ROT_180,
270.0 => ROT_270,
_ => Affine::rotate(angle_deg.to_radians()),
}
}
fn map_and_push_if_present<T, U>(dest: &mut Vec<T>, src: Vec<U>, map: fn(U) -> T) {
src.into_iter().map(map).for_each(|v| dest.push(v));
}
impl RawLayer {
fn build(self, format_version: FormatVersion) -> Result<Layer, Error> {
const DEFAULT_LAYER_WIDTH: f64 = 600.;
let mut shapes = Vec::new();
map_and_push_if_present(&mut shapes, self.paths, Shape::Path);
map_and_push_if_present(&mut shapes, self.components, Shape::Component);
for raw_shape in self.shapes {
shapes.push(raw_shape.try_into()?);
}
let anchors = self
.anchors
.into_iter()
.map(|ra| {
let pos = if let Some(pos) = ra.pos {
pos
} else if let Some(raw) = ra.position {
Point::parse_plist(&raw).unwrap()
} else {
Point::ZERO
};
Anchor { name: ra.name, pos }
})
.collect();
let mut attributes = self.attributes;
if let Some(axis_rule) =
AxisRule::from_layer_name(&self.name).filter(|_| format_version.is_v2())
{
assert!(
attributes.axis_rules.is_empty(),
"glyphs v2 does not use axisRules attr"
);
attributes.axis_rules.push(axis_rule);
}
let smart_component_positions = if format_version.is_v2() {
self.user_data.part_selection
} else {
self.part_selection
}
.into_iter()
.map(|(k, v)| match v {
1 => Ok((k, AxisPole::Min)),
2 => Ok((k, AxisPole::Max)),
other => Err(Error::BadValue(format!(
"expected only 1 or 2 in partSelection, found '{other}'"
))),
})
.collect::<Result<_, _>>()?;
let hints = self
.hints
.iter()
.map(RawHint::to_hint)
.collect::<Result<_, _>>()?;
Ok(Layer {
layer_id: self.layer_id,
associated_master_id: self.associated_master_id,
width: self.width.unwrap_or(DEFAULT_LAYER_WIDTH.into()),
vert_width: self.vert_width,
vert_origin: self.vert_origin,
shapes,
anchors,
attributes,
smart_component_positions,
hints,
})
}
}
impl RawGlyph {
fn build(self, format_version: FormatVersion, glyph_data: &GlyphData) -> Result<Glyph, Error> {
let mut instances = Vec::new();
let mut bracket_layers = Vec::new();
for mut layer in self.layers {
if layer.is_bracket_layer(format_version) {
bracket_layers.push(layer.build(format_version)?);
} else if !layer.is_draft() {
layer.attributes.axis_rules.clear();
instances.push(layer.build(format_version)?);
}
}
fn parse_category<T>(s: Option<&str>, glyph: &SmolStr) -> Option<T>
where
T: FromStr<Err = SmolStr>,
{
match s.filter(|s| !s.is_empty()).map(T::from_str).transpose() {
Ok(x) => x,
Err(err) => {
log::warn!("Unknown category '{err}' for glyph '{glyph}'");
None
}
}
}
let mut category = parse_category(self.category.as_deref(), &self.glyphname);
let mut sub_category = parse_category(self.sub_category.as_deref(), &self.glyphname);
let mut production_name = self.production_name;
let category_was_set = self.category.as_deref().is_some_and(|s| !s.is_empty());
let sub_category_was_set = self.sub_category.as_deref().is_some_and(|s| !s.is_empty());
let codepoints = self
.unicode
.map(|s| parse_codepoint_str(&s, format_version.codepoint_radix()))
.unwrap_or_default();
if (category.is_none() || sub_category.is_none() || production_name.is_none())
&& let Some(result) = glyph_data.query(&self.glyphname, Some(&codepoints))
{
if !category_was_set {
category = category.or(Some(result.category));
}
if !sub_category_was_set {
sub_category = sub_category.or(result.subcategory);
}
production_name = production_name.or(result.production_name.map(Into::into));
}
let part_settings = self
.parts_settings
.iter()
.map(|raw| (raw.name.clone(), raw.bottom_value..=raw.top_value))
.collect();
Ok(Glyph {
name: self.glyphname,
export: self.export.unwrap_or(true),
layers: instances,
bracket_layers,
left_kern: self.kern_left,
right_kern: self.kern_right,
unicode: codepoints,
category,
sub_category,
production_name,
smart_component_axes: part_settings,
})
}
}
#[rustfmt::skip]
static GLYPHS_TO_OPENTYPE_LANGUAGE_ID: &[(&str, i32)] = &[
("AFK", 0x0436), ("ARA", 0x0C01), ("ASM", 0x044D), ("AZE", 0x042C), ("BEL", 0x0423),
("BEN", 0x0845), ("BGR", 0x0402), ("BRE", 0x047E), ("CAT", 0x0403), ("CSY", 0x0405),
("DAN", 0x0406), ("DEU", 0x0407), ("ELL", 0x0408), ("ENG", 0x0409), ("ESP", 0x0C0A),
("ETI", 0x0425), ("EUQ", 0x042D), ("FIN", 0x040B), ("FLE", 0x0813), ("FOS", 0x0438),
("FRA", 0x040C), ("FRI", 0x0462), ("GRN", 0x046F), ("GUJ", 0x0447), ("HAU", 0x0468),
("HIN", 0x0439), ("HRV", 0x041A), ("HUN", 0x040E), ("HVE", 0x042B), ("IRI", 0x083C),
("ISL", 0x040F), ("ITA", 0x0410), ("IWR", 0x040D), ("JPN", 0x0411), ("KAN", 0x044B),
("KAT", 0x0437), ("KAZ", 0x043F), ("KHM", 0x0453), ("KOK", 0x0457), ("KOR", 0x0412), ("LAO", 0x0454),
("LSB", 0x082E), ("LTH", 0x0427), ("LVI", 0x0426), ("MAR", 0x044E), ("MKD", 0x042F),
("MLR", 0x044C), ("MLY", 0x043E), ("MNG", 0x0352), ("MOL", 0x0818), ("MTS", 0x043A), ("NEP", 0x0461),
("NLD", 0x0413), ("NOB", 0x0414), ("ORI", 0x0448), ("PAN", 0x0446), ("PAS", 0x0463),
("PLK", 0x0415), ("PTG", 0x0816), ("PTG-BR", 0x0416), ("RMS", 0x0417), ("ROM", 0x0418),
("RUS", 0x0419), ("SAN", 0x044F), ("SKY", 0x041B), ("SLV", 0x0424), ("SQI", 0x041C),
("SRB", 0x081A), ("SVE", 0x041D), ("TAM", 0x0449), ("TAT", 0x0444), ("TEL", 0x044A),
("THA", 0x041E), ("TIB", 0x0451), ("TRK", 0x041F), ("UKR", 0x0422), ("URD", 0x0420),
("USB", 0x042E), ("UYG", 0x0480), ("UZB", 0x0443), ("VIT", 0x042A), ("WEL", 0x0452),
("ZHH", 0x0C04), ("ZHS", 0x0804), ("ZHT", 0x0404),
("dflt", 0x0409),
];
pub fn glyphs_to_opentype_lang_id(lang: &str) -> Option<u16> {
GLYPHS_TO_OPENTYPE_LANGUAGE_ID
.binary_search_by_key(&lang, |entry| entry.0)
.ok()
.map(|idx| GLYPHS_TO_OPENTYPE_LANGUAGE_ID[idx].1 as u16)
}
impl RawFeature {
fn autostr(&self) -> &str {
match self.automatic {
Some(1) => "# automatic\n",
_ => "",
}
}
fn name(&self) -> Result<&str, Error> {
self.name
.as_deref()
.or(self.tag.as_deref())
.ok_or_else(|| Error::StructuralError(format!("{self:?} missing name and tag")))
}
fn disabled(&self) -> bool {
self.disabled == Some(1)
}
fn legacy_name_record_maybe(&self) -> Option<String> {
let name = self.notes.as_deref()?.strip_prefix("Name:")?.trim();
if name.is_empty() {
None
} else {
Some(format!("name 3 1 0x409 \"{name}\";"))
}
}
fn feature_names(&self) -> String {
let labels = self
.labels
.iter()
.filter_map(|label| label.to_fea())
.chain(self.legacy_name_record_maybe())
.collect::<Vec<_>>()
.join("\n");
if labels.is_empty() {
Default::default()
} else {
format!("featureNames {{\n{labels}\n}};\n")
}
}
fn prefix_to_feature(&self) -> Result<FeatureSnippet, Error> {
let name = self.name.as_deref().unwrap_or_default();
let code = format!("# Prefix: {}\n{}{}", name, self.autostr(), self.code);
Ok(FeatureSnippet::new(code, self.disabled()))
}
fn class_to_feature(&self) -> Result<FeatureSnippet, Error> {
let name = self.name()?;
let code = format!(
"{}{}{name} = [ {}\n];",
self.autostr(),
if name.starts_with('@') { "" } else { "@" },
self.code
);
Ok(FeatureSnippet::new(code, self.disabled()))
}
fn raw_feature_to_feature(&self) -> Result<FeatureSnippet, Error> {
let name = self.name()?;
let insert_mark = self.insert_mark_if_manual_kern_feature();
let code = format!(
"feature {name} {{\n{}{}{}{insert_mark}\n}} {name};",
self.autostr(),
self.feature_names(),
self.code
);
Ok(FeatureSnippet::new(code, self.disabled()))
}
fn insert_mark_if_manual_kern_feature(&self) -> &str {
if self.name().unwrap_or_default() == "kern"
&& self.automatic != Some(1)
&& !self.code.contains("# Automatic Code")
{
"# Automatic Code\n"
} else {
""
}
}
}
impl RawNameValue {
fn to_fea(&self) -> Option<String> {
if self.value.is_empty() {
return None;
}
let Some(language_id) = glyphs_to_opentype_lang_id(&self.language) else {
warn!("Unknown feature label language: {}", self.language);
return None;
};
let name = self.value.replace("\\", "\\005c").replace("\"", "\\0022");
Some(format!(" name 3 1 0x{language_id:04X} \"{name}\";"))
}
}
fn lookup_class_value(axis_tag: &str, user_class: &str) -> Option<u16> {
let user_class = match user_class {
value if !value.is_empty() => {
let mut value = value.to_ascii_lowercase();
value.retain(|c| c != ' ');
value
}
_ => String::from(""),
};
match (axis_tag, user_class.as_str()) {
("wght", "thin") => Some(100),
("wght", "extralight" | "ultralight") => Some(200),
("wght", "light") => Some(300),
("wght", "" | "normal" | "regular") => Some(400),
("wght", "medium") => Some(500),
("wght", "demibold" | "semibold") => Some(600),
("wght", "bold") => Some(700),
("wght", "ultrabold" | "extrabold") => Some(800),
("wght", "black" | "heavy") => Some(900),
("wdth", "ultracondensed") => Some(1),
("wdth", "extracondensed") => Some(2),
("wdth", "condensed") => Some(3),
("wdth", "semicondensed") => Some(4),
("wdth", "" | "Medium (normal)") => Some(5),
("wdth", "semiexpanded") => Some(6),
("wdth", "expanded") => Some(7),
("wdth", "extraexpanded") => Some(8),
("wdth", "ultraexpanded") => Some(9),
_ => {
warn!("Unrecognized ('{axis_tag}', '{user_class}')");
None
}
}
}
fn add_mapping_if_new(
axis_mappings: &mut BTreeMap<String, AxisUserToDesignMap>,
axes: &[Axis],
axis_tag: &str,
axes_values: &[OrderedFloat<f64>],
value: f64,
) {
let Some(idx) = axes.iter().position(|a| a.tag == axis_tag) else {
return;
};
let axis = &axes[idx];
let Some(design) = axes_values.get(idx) else {
return;
};
axis_mappings
.entry(axis.name.clone())
.or_default()
.add_if_new(value.into(), *design);
}
impl Instance {
fn new(
axes: &[Axis],
value: &mut RawInstance,
masters_have_axis_locations: bool,
) -> Result<Self, Error> {
let active = value.is_active();
let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
let mut tags_done = BTreeSet::new();
for axis_location in value
.custom_parameters
.take_axis_locations()
.unwrap_or_default()
{
let Some(axis_index) = axis_index(axes, |a| a.name == axis_location.axis_name) else {
log_once_warn!(
"{} instance's 'Axis Location' includes axis {:?} not included in font",
value.name,
axis_location.axis_name
);
continue;
};
let user = axis_location.location;
let design = value.axes_values[axis_index];
axis_mappings
.entry(axis_location.axis_name.clone())
.or_default()
.add_if_new(user, design);
tags_done.insert(axes[axis_index].tag.as_str());
}
for (i, axis) in axes.iter().enumerate() {
if tags_done.contains(axis.tag.as_str()) {
continue;
}
if axis.tag == "wght" || axis.tag == "wdth" {
continue;
}
if let Some(&design) = value.axes_values.get(i) {
axis_mappings
.entry(axis.name.clone())
.or_default()
.add_if_new(design, design);
}
}
if !masters_have_axis_locations && !tags_done.contains("wght") {
add_mapping_if_new(
&mut axis_mappings,
axes,
"wght",
&value.axes_values,
value
.weight_class
.as_ref()
.map(|v| f64::from_str(v).unwrap())
.unwrap_or(400.0),
);
}
if !masters_have_axis_locations && !tags_done.contains("wdth") {
add_mapping_if_new(
&mut axis_mappings,
axes,
"wdth",
value.axes_values.as_ref(),
value
.width_class
.as_ref()
.map(|v| match WidthClass::try_from(u16::from_str(v).unwrap()) {
Ok(width_class) => width_class.to_percent(),
Err(err) => {
warn!("{err}");
100.0
}
})
.unwrap_or(100.0),
);
}
Ok(Instance {
name: value.name.clone(),
active,
type_: value
.type_
.as_ref()
.map(|v| v.as_str().into())
.unwrap_or(InstanceType::Single),
axis_mappings,
axes_values: value.axes_values.clone(),
properties: value.properties.clone(),
custom_parameters: value.custom_parameters.to_custom_params()?,
})
}
fn family_name(&self) -> Option<&str> {
self.properties
.iter()
.find(|raw| raw.key == "familyNames")
.and_then(RawName::get_value)
}
pub fn postscript_name(&self) -> Option<&str> {
["variablePostscriptFontName", "postscriptFontName"]
.iter()
.find_map(|key| {
self.properties
.iter()
.find(|raw| raw.key == *key)
.and_then(RawName::get_value)
})
}
}
fn codepage_range_bit(codepage: u32) -> Result<u32, Error> {
Ok(match codepage {
1252 => 0, 1250 => 1, 1251 => 2, 1253 => 3, 1254 => 4, 1255 => 5, 1256 => 6, 1257 => 7, 1258 => 8, 874 => 16, 932 => 17, 936 => 18, 949 => 19, 950 => 20, 1361 => 21, 869 => 48, 866 => 49, 865 => 50, 864 => 51, 863 => 52, 862 => 53, 861 => 54, 860 => 55, 857 => 56, 855 => 57, 852 => 58, 775 => 59, 737 => 60, 708 => 61, 850 => 62, 437 => 63,
v if v < 64 => v, _ => return Err(Error::InvalidCodePage(codepage)),
})
}
fn update_names(all_names: &mut BTreeMap<String, Vec<(String, String)>>, raw_names: &[RawName]) {
for name in raw_names {
let mut lang_map: IndexMap<String, String> = IndexMap::new();
if let Some(value) = &name.value {
lang_map.insert("dflt".to_string(), value.clone());
}
for (lang, val) in name.localized_values() {
lang_map.insert(lang.to_string(), val.to_string());
}
if !lang_map.is_empty() {
let values: Vec<_> = lang_map.into_iter().collect();
all_names.insert(name.key.clone(), values);
}
}
}
impl TryFrom<RawFont> for Font {
type Error = Error;
fn try_from(mut from: RawFont) -> Result<Self, Self::Error> {
if from.format_version.is_v2() {
from.v2_to_v3()?;
} else {
from.v2_to_v3_names()?;
}
let glyph_data = GlyphData::default();
let masters_have_axis_locations = from
.font_master
.iter()
.all(|master| master.custom_parameters.contains("Axis Location"));
let instances: Vec<_> = from
.instances
.iter_mut()
.map(|ri| Instance::new(&from.axes, ri, masters_have_axis_locations))
.collect::<Result<Vec<_>, Error>>()?;
let axis_mappings = UserToDesignMapping::new(&mut from, &instances);
let default_master_idx = default_master_idx(&mut from);
let mut custom_parameters = from.custom_parameters.to_custom_params()?;
let glyph_order = make_glyph_order(&from.glyphs, custom_parameters.glyph_order.take());
let mut glyphs = BTreeMap::new();
for raw_glyph in from.glyphs.into_iter() {
let mut glyph = raw_glyph.build(from.format_version, &glyph_data)?;
for layer in glyph.bracket_layers.iter_mut() {
layer
.attributes
.axis_rules
.resize(from.axes.len(), Default::default());
}
glyphs.insert(glyph.name.clone(), glyph);
}
let mut features = Vec::new();
for class in from.classes {
features.push(class.class_to_feature()?);
}
for prefix in from.feature_prefixes {
features.push(prefix.prefix_to_feature()?);
}
for feature in from.features {
features.push(feature.raw_feature_to_feature()?);
}
let units_per_em = from.units_per_em.ok_or(Error::NoUnitsPerEm)?;
let units_per_em = units_per_em.try_into().map_err(Error::InvalidUpem)?;
let mut all_names = BTreeMap::new();
update_names(&mut all_names, &from.properties);
for instance in &instances {
if instance.active
&& instance.type_ == InstanceType::Variable
&& instance.family_name().map(|name| name == from.family_name.as_str()).unwrap_or(true)
{
update_names(&mut all_names, &instance.properties);
}
}
let family_names = all_names
.entry("familyNames".into())
.or_insert_with(Vec::new);
family_names.retain(|(lang, _)| lang != "dflt");
family_names.insert(0, ("dflt".to_string(), from.family_name.clone()));
if let Some(version_values) = all_names.remove("versionString") {
all_names.insert("version".into(), version_values);
}
let metric_names: BTreeMap<usize, String> = from
.metrics
.into_iter()
.enumerate()
.map(|(idx, metric)| (idx, metric.type_))
.collect();
let master_ids_to_names: IndexMap<_, _> = from
.font_master
.iter()
.map(|m| (m.id.clone(), m.name.clone()))
.collect();
let masters = from
.font_master
.into_iter()
.map(|m| {
let custom_parameters = m.custom_parameters.to_custom_params()?;
let metrics_source_id =
resolve_metrics_source_id(&custom_parameters, &master_ids_to_names);
Ok(FontMaster {
id: m.id,
name: m.name.unwrap_or_default(),
axes_values: m.axes_values,
metric_values: m
.metric_values
.into_iter()
.enumerate()
.filter_map(|(idx, value)| {
metric_names.get(&idx).map(|name| (name.clone(), value))
})
.fold(BTreeMap::new(), |mut acc, (name, value)| {
acc.entry(name).or_insert(value.into());
acc
}),
number_values: from
.numbers
.iter()
.zip(m.number_values.iter())
.map(|(k, v)| (k.name.clone(), *v))
.collect(),
custom_parameters,
user_data: m.user_data,
metrics_source_id,
})
})
.collect::<Result<Vec<_>, Error>>()?;
let virtual_masters = custom_parameters.virtual_masters.take().unwrap_or_default();
Ok(Font {
units_per_em,
axes: from.axes,
masters,
default_master_idx,
glyphs,
glyph_order,
axis_mappings,
virtual_masters,
features,
all_names,
instances,
version_major: from.versionMajor.unwrap_or_default() as i32,
version_minor: from.versionMinor.unwrap_or_default() as u32,
date: from.date,
kerning_ltr: from.kerning_LTR,
kerning_rtl: from.kerning_RTL,
custom_parameters,
user_data: from.user_data,
})
}
}
fn preprocess_unparsed_plist(s: &str) -> Cow<'_, str> {
let unicode_re =
Regex::new(r"(?m)^(?P<prefix>\s*unicode\s*=\s*)[(]?(?P<value>[0-9a-zA-Z,]+)[)]?;\s*$")
.unwrap();
unicode_re.replace_all(s, r#"$prefix"$value";"#)
}
fn variable_instance_for<'a>(instances: &'a [Instance], name: &str) -> Option<&'a Instance> {
instances
.iter()
.find(|i| i.active && i.type_ == InstanceType::Variable && i.name == name)
}
impl Font {
pub fn load_from_string(data: &str) -> Result<Font, Error> {
let raw_font = RawFont::load_from_string(data)?;
let mut font = Font::try_from(raw_font)?;
font.preprocess()?;
Ok(font)
}
pub fn load(glyphs_file: &path::Path) -> Result<Font, Error> {
let mut font = Self::load_raw(glyphs_file)?;
font.preprocess()?;
Ok(font)
}
pub fn color_palettes(&self) -> Option<&Vec<Vec<Color>>> {
self.default_master()
.custom_parameters
.color_palettes
.as_ref()
.or(self.custom_parameters.color_palettes.as_ref())
}
pub fn default_palette(&self) -> Option<&[Color]> {
self.color_palettes().map(|p| p[0].as_slice())
}
fn preprocess(&mut self) -> Result<(), Error> {
self.align_bracket_layers();
self.instantiate_all_smart_components()?;
self.insert_all_corner_components()
}
pub(crate) fn depth_sorted_composite_glyphs(&self) -> Vec<SmolStr> {
fontdrasil::util::depth_sorted_composite_glyphs(&self.glyphs)
}
fn align_bracket_layers(&mut self) {
let todo = self.depth_sorted_composite_glyphs();
for name in &todo {
let mut needed = IndexMap::new();
let glyph = self.glyphs.get(name).unwrap();
let my_bracket_layers = glyph
.bracket_layers
.iter()
.map(|l| l.bracket_info(&self.axes))
.collect::<IndexSet<_>>();
for layer in &glyph.layers {
for comp in layer.components() {
let comp_glyph = self.glyphs.get(&comp.name).unwrap();
let comp_bracket_layers = comp_glyph
.bracket_layers
.iter()
.map(|l| l.bracket_info(&self.axes))
.collect::<IndexSet<_>>();
if comp_bracket_layers != my_bracket_layers {
let missing = comp_bracket_layers.difference(&my_bracket_layers);
needed
.entry(layer.layer_id.clone())
.or_insert(IndexSet::new())
.extend(missing.cloned());
}
}
}
if needed.is_empty() {
continue;
}
let mut new_layers = Vec::new();
for (master, needed_brackets) in needed {
let master_name = self
.masters
.iter()
.find_map(|m| (m.id == master).then_some(m.name.as_str()))
.unwrap_or(master.as_str());
if !my_bracket_layers.is_empty() {
log::warn!(
"Glyph {name} in master {master_name} has different bracket layers \
than its components. This is not supported, so some bracket layers \
will not be applied. Consider fixing the source instead."
);
}
let base_layer = glyph.layers.iter().find(|l| l.layer_id == master).unwrap();
for box_ in needed_brackets {
log::debug!("synthesized layer {box_:?} in master {master_name} for '{name}'",);
let new_layer = synthesize_bracket_layer(base_layer, box_, &self.axes);
new_layers.push(new_layer);
}
}
new_layers.sort_by_cached_key(|l| l.axis_rules_sort_key());
self.glyphs
.get_mut(name)
.unwrap()
.bracket_layers
.extend_from_slice(&new_layers);
}
}
fn instantiate_all_smart_components(&mut self) -> Result<(), Error> {
let mut glyphs_with_smart_components = self
.glyphs
.values()
.filter(|glyph| {
glyph
.layers
.iter()
.flat_map(|layer| layer.components())
.any(|comp| {
self.glyphs
.get(&comp.name)
.map(|ref_glyph| !ref_glyph.smart_component_axes.is_empty())
.unwrap_or(false)
})
})
.cloned()
.collect::<Vec<_>>();
if glyphs_with_smart_components.is_empty() {
return Ok(());
}
let depth_ranked = self
.depth_sorted_composite_glyphs()
.into_iter()
.enumerate()
.map(|(rank, name)| (name, rank))
.collect::<HashMap<_, _>>();
glyphs_with_smart_components.sort_by_key(|g| depth_ranked.get(&g.name).unwrap());
for mut glyph in glyphs_with_smart_components {
for layer in glyph.layers.iter_mut() {
let explicit_anchor_names: HashSet<SmolStr> =
layer.anchors.iter().map(|a| a.name.clone()).collect();
let old_shapes = std::mem::take(&mut layer.shapes);
let mut new_shapes = Vec::new();
for shape in old_shapes {
if let Some(comp) = shape.as_component()
&& let Some(ref_glyph) = self
.glyphs
.get(&comp.name)
.filter(|g| !g.smart_component_axes.is_empty())
{
log::debug!(
"instantiating smart component '{}' for '{}'",
comp.name,
glyph.name
);
let instance = crate::smart_components::instantiate_for_layer(
layer.master_id(),
comp,
ref_glyph,
)
.map_err(|issue| Error::BadSmartComponent {
glyph: glyph.name.clone(),
component: comp.name.clone(),
issue,
})?;
new_shapes.extend(instance.shapes);
let new_anchors: Vec<_> = instance
.anchors
.into_iter()
.filter(|a| !explicit_anchor_names.contains(&a.name))
.collect();
let new_names: HashSet<&SmolStr> =
new_anchors.iter().map(|a| &a.name).collect();
layer.anchors.retain(|a| !new_names.contains(&a.name));
layer.anchors.extend(new_anchors);
} else {
layer.shapes.push(shape);
}
}
layer.shapes.extend(new_shapes);
}
self.glyphs.insert(glyph.name.clone(), glyph);
}
Ok(())
}
fn insert_all_corner_components(&mut self) -> Result<(), Error> {
let glyphs_with_corners: Vec<_> = self
.glyphs
.values()
.filter(|glyph| {
glyph
.layers
.iter()
.chain(glyph.bracket_layers.iter())
.any(|layer| {
layer
.hints
.iter()
.any(|hint| hint.type_ == HintType::Corner)
})
})
.cloned()
.collect();
if glyphs_with_corners.is_empty() {
return Ok(());
}
for mut glyph in glyphs_with_corners {
for layer in glyph
.layers
.iter_mut()
.chain(glyph.bracket_layers.iter_mut())
{
crate::corner_components::insert_corner_components_for_layer(layer, &self.glyphs)
.map_err(|issue| Error::BadCornerComponent {
glyph: glyph.name.clone(),
issue,
})?;
}
self.glyphs.insert(glyph.name.clone(), glyph);
}
Ok(())
}
pub(crate) fn load_raw(glyphs_file: impl AsRef<path::Path>) -> Result<Font, Error> {
RawFont::load(glyphs_file.as_ref()).and_then(Font::try_from)
}
pub fn default_master(&self) -> &FontMaster {
&self.masters[self.default_master_idx]
}
pub fn has_kerns_for_master(&self, master_id: &str) -> bool {
self.kerning_ltr
.get(master_id)
.is_some_and(|k| !k.is_empty())
|| self
.kerning_rtl
.get(master_id)
.is_some_and(|k| !k.is_empty())
}
pub fn variable_export_settings(&self, master: &FontMaster) -> Option<&Instance> {
variable_instance_for(&self.instances, &master.name)
}
pub fn get_default_name(&self, key: &str) -> Option<&str> {
let values = self.all_names.get(key)?;
let scale = values.len() as i32;
values
.iter()
.enumerate()
.map(|(i, (lang, val))| {
(
default_language_score(lang.as_str(), i, scale),
val.as_str(),
)
})
.min()
.map(|(_, val)| val)
}
pub fn get_localized_names(&self, key: &str) -> impl Iterator<Item = (&str, &str)> + '_ {
self.all_names
.get(key)
.into_iter()
.flat_map(|v| v.iter())
.filter(|(lang, _)| !matches!(lang.as_str(), "dflt" | "default" | "ENG"))
.map(|(lang, val)| (lang.as_str(), val.as_str()))
}
pub fn default_names(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
self.all_names
.keys()
.filter_map(|key| self.get_default_name(key).map(|val| (key.as_str(), val)))
}
pub fn localized_names(&self) -> impl Iterator<Item = (&str, &str, &str)> + '_ {
self.all_names.iter().flat_map(|(key, values)| {
values
.iter()
.filter(|(lang, _)| !matches!(lang.as_str(), "dflt" | "default" | "ENG"))
.map(move |(lang, val)| (key.as_str(), lang.as_str(), val.as_str()))
})
}
pub fn vendor_id(&self) -> Option<&str> {
self.get_default_name("vendorID")
}
}
fn synthesize_bracket_layer(
old_layer: &Layer,
box_: BTreeMap<String, (Option<i64>, Option<i64>)>,
axes: &[Axis],
) -> Layer {
let mut new_layer = old_layer.clone();
new_layer.associated_master_id = Some(std::mem::take(&mut new_layer.layer_id));
new_layer.attributes.axis_rules = axes
.iter()
.map(|axis| {
if let Some((min, max)) = box_.get(&axis.tag) {
AxisRule {
min: min.map(|x| x as _),
max: max.map(|x| x as _),
}
} else {
Default::default()
}
})
.collect();
new_layer
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct PointForEqAndHash {
x: OrderedFloat<f64>,
y: OrderedFloat<f64>,
}
impl PointForEqAndHash {
fn new(point: Point) -> PointForEqAndHash {
point.into()
}
}
impl From<Point> for PointForEqAndHash {
fn from(value: Point) -> Self {
PointForEqAndHash {
x: value.x.into(),
y: value.y.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct AffineForEqAndHash([OrderedFloat<f64>; 6]);
impl From<Affine> for AffineForEqAndHash {
fn from(value: Affine) -> Self {
Self(value.as_coeffs().map(|coeff| coeff.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Font, FontMaster, Node, Shape, plist::FromPlist, smart_components};
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
path::{Path, PathBuf},
};
use ordered_float::OrderedFloat;
use pretty_assertions::assert_eq;
use kurbo::{Affine, Point, Rect, Vec2};
use rstest::rstest;
use smol_str::ToSmolStr;
fn testdata_dir() -> PathBuf {
let mut dir = Path::new("../resources/testdata");
if !dir.is_dir() {
dir = Path::new("./resources/testdata");
}
assert!(dir.is_dir());
dir.to_path_buf()
}
fn glyphs2_dir() -> PathBuf {
testdata_dir().join("glyphs2")
}
fn glyphs3_dir() -> PathBuf {
testdata_dir().join("glyphs3")
}
fn round(transform: Affine, digits: u8) -> Affine {
let m = 10f64.powi(digits as i32);
let mut coeffs = transform.as_coeffs();
for c in coeffs.iter_mut() {
*c = (*c * m).round() / m;
}
Affine::new(coeffs)
}
#[test]
fn v2_format_version() {
let v2_font = glyphs2_dir().join("Mono.glyphs");
let as_str = std::fs::read_to_string(&v2_font).unwrap();
assert!(!as_str.contains(".formatVersion"), "only exists in v3");
let font = RawFont::load(&v2_font).unwrap();
assert_eq!(font.format_version, FormatVersion::V2);
}
#[test]
fn v3_format_version() {
let v3_font = glyphs3_dir().join("MasterNames.glyphs");
let as_str = std::fs::read_to_string(&v3_font).unwrap();
assert!(as_str.contains(".formatVersion"), "exists in v3");
let font = RawFont::load(&v3_font).unwrap();
assert_eq!(font.format_version, FormatVersion::V3);
}
#[test]
fn test_glyphs3_node() {
let node: Node = Node::parse_plist("(354, 183, l)").unwrap();
assert_eq!(
Node {
node_type: crate::NodeType::Line,
pt: super::Point { x: 354.0, y: 183.0 }
},
node
);
}
#[test]
fn test_glyphs2_node() {
let node: Node = Node::parse_plist("\"354 183 LINE\"").unwrap();
assert_eq!(
Node {
node_type: crate::NodeType::Line,
pt: super::Point { x: 354.0, y: 183.0 }
},
node
);
}
#[test]
fn test_glyphs3_node_userdata() {
let node = Node::parse_plist("(354, 183, l,{name = hr00;})").unwrap();
assert_eq!(
Node {
node_type: crate::NodeType::Line,
pt: super::Point { x: 354.0, y: 183.0 }
},
node
);
}
#[test]
fn test_glyphs2_node_userdata() {
let node = Node::parse_plist("\"354 183 LINE {name=duck}\"").unwrap();
assert_eq!(
Node {
node_type: crate::NodeType::Line,
pt: super::Point { x: 354.0, y: 183.0 }
},
node
);
}
#[test]
fn survive_unquoted_infinity() {
Font::load(&glyphs3_dir().join("infinity.glyphs")).unwrap();
}
fn assert_wght_var_metrics(font: &Font) {
let default_master = font.default_master();
assert_eq!(737.0, default_master.ascender().unwrap());
assert_eq!(-42.0, default_master.descender().unwrap());
}
#[test]
fn read_wght_var_2_metrics() {
assert_wght_var_metrics(&Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap());
}
#[test]
fn read_wght_var_3_metrics() {
assert_wght_var_metrics(&Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap());
}
enum LoadCompare {
Glyphs,
GlyphsAndPackage,
}
fn assert_fonts_equal_sans_localized_names(f1: &Font, f2: &Font, context: &str) {
let mut f1_clone = f1.clone();
let mut f2_clone = f2.clone();
for all_names in [&mut f1_clone.all_names, &mut f2_clone.all_names] {
for values in all_names.values_mut() {
values.retain(|(lang, _)| matches!(lang.as_str(), "dflt" | "default" | "ENG"));
}
}
assert_eq!(f1_clone, f2_clone, "{context}");
}
fn assert_fonts_equal(f1: &Font, f2: &Font, context: &str) {
assert_eq!(f1, f2, "{context}");
}
fn assert_load_v2_matches_load_v3(name: &str, compare: LoadCompare) {
let has_package = matches!(compare, LoadCompare::GlyphsAndPackage);
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let filename = format!("{name}.glyphs");
let pkgname = format!("{name}.glyphspackage");
let g2_file = glyphs2_dir().join(filename.clone());
let g3_file = glyphs3_dir().join(filename.clone());
let g2 = Font::load(&g2_file).unwrap();
let g3 = Font::load(&g3_file).unwrap();
std::fs::write("/tmp/g2.glyphs.txt", format!("{g2:#?}")).unwrap();
std::fs::write("/tmp/g3.glyphs.txt", format!("{g3:#?}")).unwrap();
assert_fonts_equal_sans_localized_names(
&g2,
&g3,
&format!("g2 vs g3: {g2_file:?} vs {g3_file:?}"),
);
if has_package {
let g2_pkg = Font::load(&glyphs2_dir().join(pkgname.clone())).unwrap();
let g3_pkg = Font::load(&glyphs3_dir().join(pkgname.clone())).unwrap();
std::fs::write("/tmp/g2.glyphspackage.txt", format!("{g2_pkg:#?}")).unwrap();
std::fs::write("/tmp/g3.glyphspackage.txt", format!("{g3_pkg:#?}")).unwrap();
assert_fonts_equal_sans_localized_names(&g2_pkg, &g3_pkg, "g2_pkg vs g3_pkg");
assert_fonts_equal(&g3_pkg, &g3, "g3_pkg vs g3");
}
}
#[test]
fn read_wght_var_2_and_3() {
assert_load_v2_matches_load_v3("WghtVar", LoadCompare::GlyphsAndPackage);
}
#[test]
fn read_wght_var_avar_2_and_3() {
assert_load_v2_matches_load_v3("WghtVar_Avar", LoadCompare::GlyphsAndPackage);
}
#[test]
fn read_wght_var_instances_2_and_3() {
assert_load_v2_matches_load_v3("WghtVar_Instances", LoadCompare::GlyphsAndPackage);
}
#[test]
fn read_wght_var_os2_2_and_3() {
assert_load_v2_matches_load_v3("WghtVar_OS2", LoadCompare::GlyphsAndPackage);
}
#[test]
fn read_wght_var_anchors_2_and_3() {
assert_load_v2_matches_load_v3("WghtVar_Anchors", LoadCompare::GlyphsAndPackage);
}
#[test]
fn read_infinity_2_and_3() {
assert_load_v2_matches_load_v3("infinity", LoadCompare::GlyphsAndPackage);
}
#[test]
fn read_wght_var_noexport_2_and_3() {
assert_load_v2_matches_load_v3("WghtVar_NoExport", LoadCompare::Glyphs);
}
#[test]
fn read_master_names_2_and_3() {
assert_load_v2_matches_load_v3("MasterNames", LoadCompare::Glyphs);
}
#[test]
fn read_master_names_with_italic_2_and_3() {
assert_load_v2_matches_load_v3("MasterNames-Italic", LoadCompare::Glyphs);
}
fn only_shape_in_only_layer<'a>(font: &'a Font, glyph_name: &str) -> &'a Shape {
let glyph = font.glyphs.get(glyph_name).unwrap();
assert_eq!(1, glyph.layers.len());
assert_eq!(1, glyph.layers[0].shapes.len());
&glyph.layers[0].shapes[0]
}
fn check_v2_to_v3_transform(glyphs_file: &str, glyph_name: &str, expected: Affine) {
let g2 = Font::load(&glyphs2_dir().join(glyphs_file)).unwrap();
let g3 = Font::load(&glyphs3_dir().join(glyphs_file)).unwrap();
let g2_shape = only_shape_in_only_layer(&g2, glyph_name);
let g3_shape = only_shape_in_only_layer(&g3, glyph_name);
let Shape::Component(g2_shape) = g2_shape else {
panic!("{g2_shape:?} should be a component");
};
let Shape::Component(g3_shape) = g3_shape else {
panic!("{g3_shape:?} should be a component");
};
assert_eq!(expected, round(g2_shape.transform, 4));
assert_eq!(expected, round(g3_shape.transform, 4));
}
#[test]
fn read_transformed_component_2_and_3_uniform_scale() {
let expected = Affine::new([1.6655, 1.1611, -1.1611, 1.6655, -233.0, -129.0]);
check_v2_to_v3_transform("Component.glyphs", "comma", expected);
}
#[test]
fn read_transformed_component_2_and_3_nonuniform_scale() {
let expected = Affine::new([0.8452, 0.5892, -1.1611, 1.6655, -233.0, -129.0]);
check_v2_to_v3_transform("Component.glyphs", "non_uniform_scale", expected);
}
#[test]
fn read_component_slant_2_and_3() {
let expected = Affine::new([0.8, 0.2736, -0.1411, 0.7518, 0.0, 0.0]);
check_v2_to_v3_transform("ComponentSlant.glyphs", "slanted_square", expected);
}
fn component_transform(shape_plist: &str) -> Affine {
let shape: Shape = RawShape::parse_plist(shape_plist)
.unwrap()
.try_into()
.unwrap();
let Shape::Component(component) = shape else {
panic!("{shape:?} should be a component");
};
component.transform
}
#[test]
fn parse_component_slant_key() {
let raw = RawShape::parse_plist(
r#"
{
ref = A;
pos = (5,21);
scale = (1,0.94);
slant = (-0.7,0);
}
"#,
)
.unwrap();
assert_eq!(raw.slant, vec![-0.7, 0.0]);
}
#[test]
fn read_component_slant_x_with_scale() {
let transform = component_transform(
r#"
{
ref = A;
pos = (5,21);
scale = (1,0.94);
slant = (-0.7,0);
}
"#,
);
assert_eq!(
round(transform, 9),
round(
Affine::new([1.0, 0.0, -0.011484837902484654, 0.94, 5.0, 21.0]),
9
)
);
}
#[test]
fn read_component_slant_with_rotation_and_scale() {
let transform = component_transform(
r#"
{
ref = A;
angle = 20;
scale = (0.8,0.8);
slant = (10,0);
}
"#,
);
assert_eq!(
round(transform, 9),
round(
Affine::new([
0.8,
0.273616114660535,
-0.14106158456677195,
0.7517540966287268,
0.0,
0.0
]),
9
)
);
}
#[test]
fn read_component_slant_y() {
let transform = component_transform(
r#"
{
ref = A;
slant = (0,5);
}
"#,
);
assert_eq!(
round(transform, 9),
round(
Affine::new([1.0, 5f64.to_radians().tan(), 0.0, 1.0, 0.0, 0.0]),
9
)
);
}
#[test]
fn reject_malformed_component_slant() {
let raw = RawShape::parse_plist(
r#"
{
ref = A;
slant = (10);
}
"#,
)
.unwrap();
let result = Shape::try_from(raw);
assert!(
matches!(&result, Err(Error::StructuralError(msg)) if msg.starts_with("Bad slant")),
"{result:?}"
);
}
#[test]
fn upgrade_2_to_3_with_implicit_axes() {
let font = Font::load(&glyphs2_dir().join("WghtVar_ImplicitAxes.glyphs")).unwrap();
assert_eq!(
font.axes
.iter()
.map(|a| a.tag.as_str())
.collect::<Vec<&str>>(),
vec!["wght", "wdth", "XXXX"]
);
}
#[test]
fn understand_v2_style_unquoted_hex_unicode() {
let font = Font::load(&glyphs2_dir().join("Unicode-UnquotedHex.glyphs")).unwrap();
assert_eq!(
BTreeSet::from([0x1234]),
font.glyphs.get("name").unwrap().unicode,
);
assert_eq!(1, font.glyphs.len());
}
#[test]
fn understand_v2_style_quoted_hex_unicode_sequence() {
let font = Font::load(&glyphs2_dir().join("Unicode-QuotedHexSequence.glyphs")).unwrap();
assert_eq!(
BTreeSet::from([0x2044, 0x200D, 0x2215]),
font.glyphs.get("name").unwrap().unicode,
);
assert_eq!(1, font.glyphs.len());
}
#[test]
fn understand_v3_style_unquoted_decimal_unicode() {
let font = Font::load(&glyphs3_dir().join("Unicode-UnquotedDec.glyphs")).unwrap();
assert_eq!(
BTreeSet::from([182]),
font.glyphs.get("name").unwrap().unicode
);
assert_eq!(1, font.glyphs.len());
}
#[test]
fn understand_v3_style_unquoted_decimal_unicode_sequence() {
let font = Font::load(&glyphs3_dir().join("Unicode-UnquotedDecSequence.glyphs")).unwrap();
assert_eq!(
BTreeSet::from([1619, 1764]),
font.glyphs.get("name").unwrap().unicode,
);
assert_eq!(1, font.glyphs.len());
}
#[test]
fn axes_not_hidden() {
let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
assert_eq!(
font.axes.iter().map(|a| a.hidden).collect::<Vec<_>>(),
vec![None]
);
}
#[test]
fn axis_hidden() {
let font = Font::load(&glyphs3_dir().join("WghtVar_3master_CustomOrigin.glyphs")).unwrap();
assert_eq!(
font.axes.iter().map(|a| a.hidden).collect::<Vec<_>>(),
vec![Some(true)]
);
}
#[test]
fn vf_origin_single_axis_default() {
let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
assert_eq!(0, font.default_master_idx);
}
#[test]
fn vf_origin_multi_axis_default() {
let font = Font::load(&glyphs2_dir().join("WghtVar_ImplicitAxes.glyphs")).unwrap();
assert_eq!(0, font.default_master_idx);
}
#[test]
fn vf_origin_multi_axis_custom() {
let font = Font::load(&glyphs3_dir().join("WghtVar_3master_CustomOrigin.glyphs")).unwrap();
assert_eq!(2, font.default_master_idx);
}
#[test]
fn vf_origin_unquoted_string() {
let font = Font::load(&glyphs3_dir().join("CustomOrigin.glyphs")).unwrap();
assert_eq!(1, font.default_master_idx);
}
#[rstest]
#[case::base_style_without_regular(
&[
"Expanded Thin Italic",
"Expanded Italic",
"Expanded Bold Italic",
],
"Expanded Italic" // is common and exactly matches [1]
)]
#[case::base_style_contains_regular(
&[
"Regular Foo Bar",
"Regular Foo Baz",
"Regular Foo",
],
"Regular Foo" // is common and exactly matches [2]
)]
#[case::base_style_with_regular_omitted(
&[
"Condensed Thin",
"Condensed Light",
"Condensed Regular",
],
// "Condensed" is common and matches "Condensed Regular" when "Regular" is ignored
"Condensed Regular"
)]
#[case::default_to_regular(
&["Thin", "Light", "Regular", "Medium", "Bold"],
"Regular"
)]
#[case::default_to_first(&["Foo", "Bar", "Baz"], "Foo")]
fn find_default_master(#[case] master_names: &[&str], #[case] expected: &str) {
let mut font = RawFont::default();
for name in master_names {
let master = RawFontMaster {
name: Some(name.to_string()),
..Default::default()
};
font.font_master.push(master);
}
let idx = default_master_idx(&mut font);
assert_eq!(expected, font.font_master[idx].name.as_deref().unwrap());
}
#[test]
fn glyph_order_default_is_file_order() {
let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
assert_eq!(
vec![
"space",
"exclam",
"hyphen",
"bracketleft",
"bracketright",
"manual-component"
],
font.glyph_order
);
}
#[test]
fn glyph_order_override_obeyed() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let font = Font::load(&glyphs3_dir().join("WghtVar_GlyphOrder.glyphs")).unwrap();
assert_eq!(vec!["hyphen", "space", "exclam"], font.glyph_order);
}
#[test]
fn loads_global_axis_mappings_from_glyphs2() {
let font = Font::load(&glyphs2_dir().join("OpszWghtVar_AxisMappings.glyphs")).unwrap();
assert_eq!(
UserToDesignMapping(BTreeMap::from([
(
"Optical Size".into(),
AxisUserToDesignMap(vec![
(OrderedFloat(12.0), OrderedFloat(12.0)),
(OrderedFloat(72.0), OrderedFloat(72.0))
])
),
(
"Weight".into(),
AxisUserToDesignMap(vec![
(OrderedFloat(100.0), OrderedFloat(40.0)),
(OrderedFloat(200.0), OrderedFloat(46.0)),
(OrderedFloat(300.0), OrderedFloat(51.0)),
(OrderedFloat(400.0), OrderedFloat(57.0)),
(OrderedFloat(450.0), OrderedFloat(57.0)), (OrderedFloat(500.0), OrderedFloat(62.0)),
(OrderedFloat(600.0), OrderedFloat(68.0)),
(OrderedFloat(700.0), OrderedFloat(73.0)),
])
),
])),
font.axis_mappings
);
}
#[test]
fn loads_global_axis_locations_from_glyphs3() {
let font = Font::load(&glyphs3_dir().join("WghtVar_AxisLocation.glyphs")).unwrap();
assert_eq!(
UserToDesignMapping(BTreeMap::from([(
"Weight".into(),
AxisUserToDesignMap(vec![
(OrderedFloat(400.0), OrderedFloat(0.0)),
(OrderedFloat(500.0), OrderedFloat(8.0)),
(OrderedFloat(700.0), OrderedFloat(10.0)),
(OrderedFloat(600.0), OrderedFloat(8.5)),
])
),])),
font.axis_mappings
);
}
#[test]
fn loads_global_axis_mappings_from_instances_wght_glyphs3() {
let font = Font::load(&glyphs3_dir().join("WghtVar_Avar_From_Instances.glyphs")).unwrap();
let wght_idx = font.axes.iter().position(|a| a.tag == "wght").unwrap();
assert_eq!(
vec![60.0, 80.0, 132.0],
font.masters
.iter()
.map(|m| m.axes_values[wght_idx].into_inner())
.collect::<Vec<_>>()
);
assert_eq!(
(132.0, 2),
(
font.default_master().axes_values[wght_idx].into_inner(),
font.default_master_idx
)
);
assert_eq!(
UserToDesignMapping(BTreeMap::from([(
"Weight".into(),
AxisUserToDesignMap(vec![
(OrderedFloat(300.0), OrderedFloat(60.0)),
(OrderedFloat(400.0), OrderedFloat(80.0)),
(OrderedFloat(500.0), OrderedFloat(100.0)),
(OrderedFloat(700.0), OrderedFloat(132.0)),
])
),])),
font.axis_mappings
);
}
#[test]
fn empty_axis_mappings_falls_back_to_instances() {
let base_path = glyphs3_dir().join("WghtVar_Avar_From_Instances.glyphs");
let raw = std::fs::read_to_string(&base_path).unwrap();
let raw = raw.replace(
"customParameters = (\n{",
"customParameters = (\n{\nname = \"Axis Mappings\";\nvalue = {};\n},\n{",
);
let font = Font::load_from_string(&raw).unwrap();
let font_without = Font::load(&base_path).unwrap();
assert_eq!(font_without.axis_mappings, font.axis_mappings);
}
#[test]
fn custom_axis_instance_without_axis_location_gets_identity_mapping() {
let font = Font::load(&glyphs3_dir().join("CustomAxisNoAxisLocation.glyphs")).unwrap();
assert_eq!(
UserToDesignMapping(BTreeMap::from([(
"Year".into(),
AxisUserToDesignMap(vec![
(OrderedFloat(1979.0), OrderedFloat(1979.0)),
(OrderedFloat(2020.0), OrderedFloat(2020.0)),
(OrderedFloat(2050.0), OrderedFloat(2050.0)),
])
)])),
font.axis_mappings
);
}
#[test]
fn loads_global_axis_mappings_from_instances_wdth_glyphs3() {
let font = Font::load(&glyphs3_dir().join("WdthVar.glyphs")).unwrap();
assert_eq!(font.axes.len(), 1);
assert_eq!(font.axes[0].tag, "wdth");
assert_eq!(
vec![22.0, 62.0],
font.masters
.iter()
.map(|m| m.axes_values[0].into_inner())
.collect::<Vec<_>>()
);
assert_eq!(
(22.0, 0),
(
font.default_master().axes_values[0].into_inner(),
font.default_master_idx
)
);
assert_eq!(
UserToDesignMapping(BTreeMap::from([(
"Width".into(),
AxisUserToDesignMap(vec![
(OrderedFloat(50.0), OrderedFloat(22.0)),
(OrderedFloat(100.0), OrderedFloat(41.0)),
(OrderedFloat(200.0), OrderedFloat(62.0)),
])
),])),
font.axis_mappings
);
}
#[test]
fn fea_for_class() {
let font = Font::load(&glyphs2_dir().join("Fea_Class.glyphs")).unwrap();
assert_eq!(
vec![
concat!("# automatic\n", "@Uppercase = [ A B C\n", "];",),
concat!("@Lowercase = [ a b c\n", "];",),
],
font.features
.iter()
.filter_map(|f| f.str_if_enabled())
.collect::<Vec<_>>()
)
}
#[test]
fn fea_for_prefix() {
let font = Font::load(&glyphs2_dir().join("Fea_Prefix.glyphs")).unwrap();
assert_eq!(
vec![
"\
# Prefix: Languagesystems
# automatic
languagesystem DFLT dflt;
languagesystem latn dflt;
and more;
",
"# Prefix: \n# automatic\nthanks for all the fish;",
],
font.features
.iter()
.filter_map(|f| f.str_if_enabled())
.collect::<Vec<_>>()
)
}
#[test]
fn fea_for_feature() {
let font = Font::load(&glyphs2_dir().join("Fea_Feature.glyphs")).unwrap();
assert_eq!(
vec![
"\
feature aalt {
feature locl;
feature tnum;
} aalt;",
"\
feature ccmp {
# automatic
lookup ccmp_Other_2 {
sub @Markscomb' @MarkscombCase by @MarkscombCase;
sub @MarkscombCase @Markscomb' by @MarkscombCase;
} ccmp_Other_2;
etc;
} ccmp;",
],
font.features
.iter()
.filter_map(|f| f.str_if_enabled())
.collect::<Vec<_>>()
)
}
#[test]
fn fea_order() {
let font = Font::load(&glyphs2_dir().join("Fea_Order.glyphs")).unwrap();
assert_eq!(
vec![
"@class_first = [ meh\n];",
"# Prefix: second\nmeh",
"feature third {\nmeh\n} third;",
],
font.features
.iter()
.filter_map(|f| f.str_if_enabled())
.collect::<Vec<_>>()
)
}
#[test]
fn fea_labels() {
let font = Font::load(&glyphs3_dir().join("Fea_Labels.glyphs")).unwrap();
assert_eq!(
vec![
concat!(
"feature ss01 {\n",
"# automatic\n",
"featureNames {\n",
" name 3 1 0x0409 \"Test 1\";\n",
" name 3 1 0x0C01 \"اختبار ١\";\n",
"};\n",
"sub a by a.ss01;\n",
"sub b by b.ss01;\n\n",
"} ss01;",
),
concat!(
"feature ss02 {\n",
"featureNames {\n",
" name 3 1 0x0409 \"Test 2\";\n",
"};\n",
"sub c by c.alt;\n",
"} ss02;",
),
],
font.features
.iter()
.filter_map(|f| f.str_if_enabled())
.collect::<Vec<_>>()
)
}
#[test]
fn tags_make_excellent_names() {
let raw = RawFeature {
tag: Some("aalt".to_string()),
code: "blah".to_string(),
..Default::default()
};
assert_eq!("aalt", raw.name().unwrap());
}
#[test]
fn manual_kern_always_gets_insert_mark() {
let feature = RawFeature {
tag: Some("kern".into()),
..Default::default()
};
assert!(
feature
.raw_feature_to_feature()
.unwrap()
.content
.contains("# Automatic Code")
)
}
#[test]
fn but_automatic_does_not() {
let feature = RawFeature {
tag: Some("kern".into()),
automatic: Some(1),
..Default::default()
};
assert!(
!feature
.raw_feature_to_feature()
.unwrap()
.content
.contains("# Automatic Code")
)
}
#[test]
fn v2_to_v3_simple_names() {
let v2 = Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap();
let v3 = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
let v2_defaults: BTreeMap<_, _> = v2.default_names().collect();
let v3_defaults: BTreeMap<_, _> = v3.default_names().collect();
assert_eq!(v3_defaults, v2_defaults);
}
#[test]
fn v2_to_v3_more_names() {
let v2 = Font::load(&glyphs2_dir().join("TheBestNames.glyphs")).unwrap();
let v3 = Font::load(&glyphs3_dir().join("TheBestNames.glyphs")).unwrap();
let v2_defaults: BTreeMap<_, _> = v2.default_names().collect();
let v3_defaults: BTreeMap<_, _> = v3.default_names().collect();
assert_eq!(v2_defaults, v3_defaults);
}
#[test]
fn v2_long_param_names() {
let v2 = Font::load(&glyphs2_dir().join("LongParamNames.glyphs")).unwrap();
assert_eq!(v2.get_default_name("vendorID"), Some("DERP"));
assert_eq!(
v2.get_default_name("descriptions"),
Some("legacy description")
);
assert_eq!(
v2.get_default_name("licenseURL"),
Some("www.example.com/legacy")
);
assert_eq!(v2.get_default_name("version"), Some("legacy version"));
assert_eq!(v2.get_default_name("licenses"), Some("legacy license"));
assert_eq!(v2.get_default_name("uniqueID"), Some("legacy unique id"));
assert_eq!(
v2.get_default_name("sampleTexts"),
Some("legacy sample text")
);
}
#[test]
fn v2_style_names_in_a_v3_file() {
let v3_mixed_with_v2 =
Font::load(&glyphs3_dir().join("TheBestV2NamesInAV3File.glyphs")).unwrap();
let v3 = Font::load(&glyphs3_dir().join("TheBestNames.glyphs")).unwrap();
let v3_mixed_defaults: BTreeMap<_, _> = v3_mixed_with_v2.default_names().collect();
let v3_defaults: BTreeMap<_, _> = v3.default_names().collect();
assert_eq!(v3_mixed_defaults, v3_defaults);
}
#[test]
fn no_english_names_fallback() {
let font = Font::load(&glyphs3_dir().join("NoEnglishNames.glyphs")).unwrap();
assert_eq!(
font.get_default_name("copyrights"),
Some("Droit d'auteur français")
);
assert_eq!(
font.get_default_name("designers"),
Some("Concepteur français")
);
assert_eq!(
font.get_default_name("manufacturers"),
Some("Produttore italiano")
);
let localized: Vec<_> = font.localized_names().collect();
assert!(localized.contains(&("copyrights", "FRA", "Droit d'auteur français")));
assert!(localized.contains(&("copyrights", "DEU", "Deutsches Urheberrecht")));
assert!(localized.contains(&("copyrights", "JPN", "日本の著作権")));
}
fn assert_wghtvar_avar_master_and_axes(glyphs_file: &Path) {
let font = Font::load(glyphs_file).unwrap();
let wght_idx = font.axes.iter().position(|a| a.tag == "wght").unwrap();
assert_eq!(
vec![300.0, 400.0, 700.0],
font.masters
.iter()
.map(|m| m.axes_values[wght_idx].into_inner())
.collect::<Vec<_>>()
);
assert_eq!(
(400.0, 1),
(
font.default_master().axes_values[wght_idx].into_inner(),
font.default_master_idx
)
);
}
#[test]
fn favor_regular_as_origin_glyphs2() {
assert_wghtvar_avar_master_and_axes(&glyphs2_dir().join("WghtVar_Avar.glyphs"));
}
#[test]
fn favor_regular_as_origin_glyphs3() {
assert_wghtvar_avar_master_and_axes(&glyphs3_dir().join("WghtVar_Avar.glyphs"));
}
#[test]
fn have_all_the_best_instances() {
let font = Font::load(&glyphs3_dir().join("WghtVar_Instances.glyphs")).unwrap();
assert_eq!(
vec![
("Regular", vec![("Weight", 400.0)]),
("Bold", vec![("Weight", 700.0)])
],
font.instances
.iter()
.map(|inst| (
inst.name.as_str(),
font.axes
.iter()
.zip(&inst.axes_values)
.map(|(a, v)| (a.name.as_str(), v.0))
.collect::<Vec<_>>()
))
.collect::<Vec<_>>()
);
}
#[test]
fn read_typo_whatsits() {
let font = Font::load(&glyphs2_dir().join("WghtVar_OS2.glyphs")).unwrap();
let master = font.default_master();
assert_eq!(Some(1193), master.custom_parameters.typo_ascender);
assert_eq!(Some(-289), master.custom_parameters.typo_descender);
}
#[test]
fn read_os2_flags_default_set() {
let font = Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap();
assert_eq!(font.custom_parameters.use_typo_metrics, Some(true));
assert_eq!(font.custom_parameters.has_wws_names, Some(true));
}
#[test]
fn read_os2_flags_default_unset() {
let font = Font::load(&glyphs2_dir().join("WghtVar_OS2.glyphs")).unwrap();
assert_eq!(font.custom_parameters.use_typo_metrics, None);
assert_eq!(font.custom_parameters.has_wws_names, None);
}
#[test]
fn read_os2_longform_param_names() {
let font = Font::load(&glyphs2_dir().join("os2_longform_params.glyphs")).unwrap();
assert_eq!(font.custom_parameters.strikeout_position, Some(300));
assert_eq!(font.custom_parameters.strikeout_size, Some(50));
assert_eq!(font.custom_parameters.subscript_x_offset, Some(-80));
assert_eq!(font.custom_parameters.subscript_x_size, Some(700));
assert_eq!(font.custom_parameters.subscript_y_offset, Some(-75));
assert_eq!(font.custom_parameters.subscript_y_size, Some(650));
assert_eq!(font.custom_parameters.superscript_x_offset, Some(50));
assert_eq!(font.custom_parameters.superscript_x_size, Some(600));
assert_eq!(font.custom_parameters.superscript_y_offset, Some(350));
assert_eq!(font.custom_parameters.superscript_y_size, Some(600));
}
#[test]
fn read_simple_kerning() {
let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
assert_eq!(
HashSet::from(["m01", "E09E0C54-128D-4FEA-B209-1B70BEFE300B",]),
font.kerning_ltr
.keys()
.map(|k| k.as_str())
.collect::<HashSet<_>>()
);
let actual_groups: Vec<_> = font
.glyphs
.iter()
.filter_map(|(name, glyph)| {
if glyph.left_kern.is_some() || glyph.right_kern.is_some() {
Some((
name.as_str(),
glyph.left_kern.as_deref(),
glyph.right_kern.as_deref(),
))
} else {
None
}
})
.collect();
let actual_kerning = font
.kerning_ltr
.get("m01")
.unwrap()
.iter()
.map(|((n1, n2), value)| (n1.as_str(), n2.as_str(), value.0))
.collect::<Vec<_>>();
assert_eq!(
(
vec![
("bracketleft", Some("bracketleft_L"), Some("bracketleft_R")),
(
"bracketright",
Some("bracketright_L"),
Some("bracketright_R")
),
],
vec![
("@MMK_L_bracketleft_R", "exclam", -165.),
("bracketleft", "bracketright", -300.),
("exclam", "@MMK_R_bracketright_L", -160.),
("exclam", "exclam", -360.),
("exclam", "hyphen", 20.),
("hyphen", "hyphen", -150.),
],
),
(actual_groups, actual_kerning),
"{:?}",
font.kerning_ltr
);
}
#[test]
fn kern_floats() {
let font = Font::load(&glyphs3_dir().join("KernFloats.glyphs")).unwrap();
let kerns = font.kerning_ltr.get("m01").unwrap();
let key = ("space".to_smolstr(), "space".to_smolstr());
assert_eq!(kerns.get(&key), Some(&OrderedFloat(4.2001)));
}
#[test]
fn read_simple_anchor() {
let font = Font::load(&glyphs3_dir().join("WghtVar_Anchors.glyphs")).unwrap();
assert_eq!(
vec![
("m01", "top", Point::new(300.0, 700.0)),
("l2", "top", Point::new(325.0, 725.0))
],
font.glyphs
.get("A")
.unwrap()
.layers
.iter()
.flat_map(|l| l.anchors.iter().map(|a| (
l.layer_id.as_str(),
a.name.as_str(),
a.pos
)))
.collect::<Vec<_>>()
);
}
#[test]
fn read_export_glyph() {
let font = Font::load(&glyphs3_dir().join("WghtVar_NoExport.glyphs")).unwrap();
assert_eq!(
vec![
("bracketleft", true),
("bracketright", true),
("exclam", true),
("hyphen", false),
("manual-component", true),
("space", true),
],
font.glyphs
.iter()
.map(|(name, glyph)| (name.as_str(), glyph.export))
.collect::<Vec<_>>()
);
}
#[test]
fn read_fstype_none() {
let font = Font::load(&glyphs3_dir().join("infinity.glyphs")).unwrap();
assert!(font.custom_parameters.fs_type.is_none());
}
#[test]
fn read_fstype_zero() {
let font = Font::load(&glyphs3_dir().join("fstype_0x0000.glyphs")).unwrap();
assert_eq!(Some(0), font.custom_parameters.fs_type);
}
#[test]
fn read_fstype_bits() {
let font = Font::load(&glyphs3_dir().join("fstype_0x0104.glyphs")).unwrap();
assert_eq!(Some(0x104), font.custom_parameters.fs_type);
}
#[test]
fn anchor_components() {
let font = Font::load(&glyphs3_dir().join("ComponentAnchor.glyphs")).unwrap();
let glyph = font.glyphs.get("A_Aacute").unwrap();
let acute_comb = glyph.layers[0]
.shapes
.iter()
.find_map(|shape| match shape {
Shape::Component(c) if c.name == "acutecomb" => Some(c),
_ => None,
})
.unwrap();
assert_eq!(acute_comb.anchor.as_deref(), Some("top_2"));
}
#[test]
fn parse_alignment_zone_smoke_test() {
assert_eq!(
super::parse_alignment_zone("{1, -12}").map(|x| (x.0.0, x.1.0)),
Some((1., -12.))
);
assert_eq!(
super::parse_alignment_zone("{-5001, 12}").map(|x| (x.0.0, x.1.0)),
Some((-5001., 12.))
);
}
impl FontMaster {
fn get_metric(&self, name: &str) -> Option<(f64, f64)> {
self.metric_values
.get(name)
.map(|raw| (raw.pos.0, raw.over.0))
}
}
#[test]
fn v2_alignment_zones_to_metrics() {
let font = Font::load(&glyphs2_dir().join("alignment_zones_v2.glyphs")).unwrap();
let master = font.default_master();
assert_eq!(master.get_metric("ascender"), Some((800., 17.)));
assert_eq!(master.get_metric("cap height"), Some((700., 16.)));
assert_eq!(master.get_metric("baseline"), Some((0., -16.)));
assert_eq!(master.get_metric("descender"), Some((-200., -17.)));
assert_eq!(master.get_metric("x-height"), Some((500., 15.)));
assert_eq!(master.get_metric("italic angle"), None);
}
#[test]
fn v3_duplicate_metrics_first_wins() {
let font = Font::load(&glyphs3_dir().join("WghtVar_OS2.glyphs")).unwrap();
let master = font.default_master();
assert_eq!(master.get_metric("x-height"), Some((501., 0.)));
}
#[test]
fn v2_preserve_custom_alignment_zones() {
let font = Font::load(&glyphs2_dir().join("alignment_zones_v2.glyphs")).unwrap();
let master = font.default_master();
assert_eq!(master.get_metric("zone 1"), Some((1000., 20.)));
assert_eq!(master.get_metric("zone 2"), Some((-100., -15.)));
}
#[test]
fn unknown_glyph_category() {
let raw = super::RawGlyph {
glyphname: "A".into(),
category: Some("Fake".into()),
..Default::default()
};
let cooked = raw.build(FormatVersion::V2, &GlyphData::default()).unwrap();
assert_eq!((cooked.category, cooked.sub_category), (None, None));
}
#[test]
fn explicit_unknown_subcategory_does_not_revert_to_ligature_guess() {
let raw = super::RawGlyph {
glyphname: "a_a".into(),
category: Some("Letter".into()),
sub_category: Some("Conjunct-but-fontc-doesnt-know-it".into()),
..Default::default()
};
let cooked = raw.build(FormatVersion::V2, &GlyphData::default()).unwrap();
assert_eq!(cooked.category, Some(Category::Letter));
assert_eq!(cooked.sub_category, None); }
#[test]
fn custom_params_disable() {
let font = Font::load(&glyphs3_dir().join("custom_param_disable.glyphs")).unwrap();
assert!(font.custom_parameters.fs_type.is_none())
}
#[test]
fn parse_numbers() {
let font = Font::load(&glyphs3_dir().join("number_value.glyphs")).unwrap();
assert_eq!(
font.masters[0].number_values.get("foo"),
Some(&OrderedFloat(12.4f64))
);
assert_eq!(
font.masters[1].number_values.get("foo"),
Some(&OrderedFloat(0f64))
);
}
#[test]
fn read_font_metrics() {
let font =
Font::load(&glyphs3_dir().join("GlobalMetrics_font_customParameters.glyphs")).unwrap();
assert_eq!(Some(950), font.custom_parameters.typo_ascender);
assert_eq!(Some(-350), font.custom_parameters.typo_descender);
assert_eq!(Some(0), font.custom_parameters.typo_line_gap);
assert_eq!(Some(950), font.custom_parameters.hhea_ascender);
assert_eq!(Some(-350), font.custom_parameters.hhea_descender);
assert_eq!(Some(0), font.custom_parameters.hhea_line_gap);
assert_eq!(Some(1185), font.custom_parameters.win_ascent);
assert_eq!(Some(420), font.custom_parameters.win_descent);
assert_eq!(
Some(OrderedFloat(42_f64)),
font.custom_parameters.underline_thickness
);
assert_eq!(
Some(OrderedFloat(-300_f64)),
font.custom_parameters.underline_position
);
}
#[test]
fn parse_legacy_stylistic_set_name() {
let font = Font::load(&glyphs2_dir().join("FeaLegacyName.glyphs")).unwrap();
assert_eq!(font.features.len(), 2);
let [ss01, ss02] = font.features.as_slice() else {
panic!("wrong number of features");
};
assert!(
ss01.content
.contains("name 3 1 0x409 \"Alternate placeholder\"")
);
assert!(!ss02.content.contains("name 3 1"))
}
#[test]
fn one_italic_is_enough() {
let font = Font::load(&glyphs2_dir().join("ItalicItalic.glyphs")).unwrap();
for master in font.masters {
let mut fragments = master.name.split_ascii_whitespace().collect::<Vec<_>>();
fragments.sort();
for words in fragments.windows(2) {
assert!(
words[0] != words[1],
"Multiple instances of {} in {}",
words[0],
master.name
);
}
}
}
#[test]
fn master_name_contains_italic() {
let font = Font::load(&glyphs2_dir().join("MasterNameNotExactlyItalic.glyphs")).unwrap();
assert_eq!(font.default_master().name, "ThinItalic");
}
#[test]
fn ignore_masters_if_axis_mapping() {
let font = Font::load(&glyphs2_dir().join("MasterNotMapped.glyphs")).unwrap();
let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
assert_eq!(
vec![
(OrderedFloat(400_f64), OrderedFloat(40.0)),
(OrderedFloat(700_f64), OrderedFloat(70.0))
],
*mapping
);
}
#[rstest]
#[case::rotate_0(0.0, Affine::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]))]
#[case::rotate_360(360.0, Affine::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]))]
#[case::rotate_90(90.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
#[case::rotate_minus_90(-90.0, Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]))]
#[case::rotate_180(180.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
#[case::rotate_minus_180(-180.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
#[case::rotate_270(270.0, Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]))]
#[case::rotate_minus_270(-270.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
#[case::rotate_450(450.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
#[case::rotate_540(540.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
fn cardinal_rotation_contain_exact_zeros_and_ones(
#[case] angle: f64,
#[case] expected: Affine,
) {
assert_eq!(expected, normalized_rotation(angle));
}
#[rstest]
#[case::rotate_30(30.0, 4, Affine::new([0.866, 0.5, -0.5, 0.866, 0.0, 0.0]))]
#[case::rotate_minus_30(-30.0, 4, Affine::new([0.866, -0.5, 0.5, 0.866, 0.0, 0.0]))]
#[case::rotate_almost_90(
90.01, 8, Affine::new([-0.00017453, 0.99999998, -0.99999998, -0.00017453, 0.0, 0.0])
)]
fn non_cardinal_rotation_left_untouched(
#[case] angle: f64,
#[case] precision: u8,
#[case] expected: Affine,
) {
assert_eq!(expected, round(normalized_rotation(angle), precision));
}
#[test]
fn parse_colrv1_identify_colr_glyphs() {
let font = Font::load(&glyphs3_dir().join("COLRv1-gradient.glyphs")).unwrap();
let expected_colr = HashSet::from(["A", "B", "C", "D", "K", "L", "M", "N"]);
assert_eq!(
expected_colr,
font.glyphs
.values()
.filter(|g| g.layers.iter().all(|l| l.attributes.color))
.map(|g| g.name.as_str())
.collect::<HashSet<_>>()
);
}
#[test]
fn parse_colrv1_gradients() {
let font = Font::load(&glyphs3_dir().join("COLRv1-gradient.glyphs")).unwrap();
let expected_colr = HashSet::from([
(
"A",
Gradient {
start: vec![OrderedFloat(0.1), OrderedFloat(0.1)],
end: vec![OrderedFloat(0.9), OrderedFloat(0.9)],
colors: vec![
ColorStop {
color: Color::rgba(255, 0, 0, 255),
stop_offset: 0.into(),
},
ColorStop {
color: Color::rgba(0, 0, 255, 255),
stop_offset: 1.into(),
},
],
..Default::default()
},
),
(
"N",
Gradient {
start: vec![OrderedFloat(1.0), OrderedFloat(1.0)],
end: vec![OrderedFloat(0.0), OrderedFloat(0.0)],
colors: vec![
ColorStop {
color: Color::rgba(255, 0, 0, 255),
stop_offset: 0.into(),
},
ColorStop {
color: Color::rgba(0, 0, 255, 255),
stop_offset: 1.into(),
},
],
style: "circle".to_string(),
},
),
]);
assert_eq!(
expected_colr,
font.glyphs
.values()
.filter(|g| expected_colr.iter().any(|(name, _)| *name == g.name))
.flat_map(|g| g.layers.iter().flat_map(|l| l.shapes.iter()).map(|s| (
g.name.as_str(),
s.attributes().gradient.as_ref().unwrap().clone()
)))
.collect::<HashSet<_>>()
);
}
#[test]
fn parse_grayscale_colors() {
let font = Font::load(&glyphs3_dir().join("COLRv1-grayscale.glyphs")).unwrap();
assert_eq!(
vec![
ColorStop {
color: Color::rgba(64, 64, 64, 255),
stop_offset: 0.into(),
},
ColorStop {
color: Color::rgba(0, 0, 0, 255),
stop_offset: 1.into(),
},
],
font.glyphs
.values()
.flat_map(
|g| g.layers.iter().flat_map(|l| l.shapes.iter()).flat_map(|s| s
.attributes()
.gradient
.iter()
.flat_map(|g| g.colors.iter())
.cloned())
)
.collect::<Vec<_>>()
);
}
#[test]
fn missing_width_no_problem() {
let font = Font::load(&glyphs3_dir().join("MissingWidth.glyphs")).unwrap();
let glyph = font.glyphs.get("widthless").unwrap();
assert_eq!(glyph.layers[0].width, 600.);
}
#[test]
fn read_preferred_names_from_properties() {
let font = Font::load(&glyphs3_dir().join("PreferableNames.glyphs")).unwrap();
assert_eq!(
vec![
Some("Pref Family Name"),
Some("Pref Regular"),
Some("Name 25?!")
],
vec![
font.get_default_name("preferredFamilyNames"),
font.get_default_name("preferredSubfamilyNames"),
font.get_default_name("variationsPostScriptNamePrefix"),
]
);
}
#[test]
fn zero_value_metrics() {
let font = Font::load(&glyphs3_dir().join("ZeroMetrics.glyphs")).unwrap();
let master = font.default_master();
assert_eq!(master.ascender(), Some(789.));
assert_eq!(master.cap_height(), Some(0.));
assert_eq!(master.x_height(), Some(0.));
assert_eq!(master.italic_angle(), None);
}
#[test]
fn names_from_instances() {
let font = Font::load(&glyphs3_dir().join("InstanceNames.glyphs")).unwrap();
assert_eq!(
font.get_default_name("preferredSubfamilyNames").unwrap(),
"Italic"
)
}
#[rstest]
#[case::v2(glyphs2_dir())]
#[case::v3(glyphs3_dir())]
fn glyph_production_names(#[case] glyphs_dir: PathBuf) {
let font = Font::load(&glyphs_dir.join("ProductionNames.glyphs")).unwrap();
let glyphs = font.glyphs;
assert_eq!(glyphs.get("A").unwrap().production_name, None);
assert_eq!(
glyphs.get("idotless").unwrap().production_name,
Some("uni0131".into())
);
assert_eq!(
glyphs.get("nbspace").unwrap().production_name,
Some("uni00A0".into())
);
}
#[test]
fn parse_axis_rules() {
let raw = RawFont::load(&glyphs3_dir().join("AxisRules.glyphs")).unwrap();
let space = &raw.glyphs[0];
let axes = &space.layers[0].attributes.axis_rules;
assert_eq!(
axes,
&[
AxisRule {
min: None,
max: Some(400)
},
AxisRule {
min: Some(100),
max: None,
},
AxisRule {
min: None,
max: None,
},
]
)
}
#[test]
fn drop_axis_rules_if_no_assoc_master() {
let font = Font::load(&glyphs3_dir().join("AxisRules.glyphs")).unwrap();
let space = font.glyphs.get("space").unwrap();
assert!(space.layers[0].attributes.axis_rules.is_empty());
}
#[test]
fn parse_v2_bracket_layers() {
let font = Font::load(&glyphs2_dir().join("AxisRules.glyphs")).unwrap();
let glyph = font.glyphs.get("space").unwrap();
assert_eq!(
glyph.bracket_layers[0].attributes.axis_rules,
[
AxisRule {
min: None,
max: Some(141)
},
AxisRule::default()
]
);
}
#[test]
fn parse_layer_normal() {
for name in &["[60]", "Light Extended [ 60 ]"] {
let rule = AxisRule::from_layer_name(name);
assert_eq!(
rule,
Some(AxisRule {
min: Some(60),
max: None
}),
"{name}"
)
}
}
#[test]
fn parse_layer_reversed() {
for name in &["]60]", "Light Extended ] 60 ]"] {
let rule = AxisRule::from_layer_name(name);
assert_eq!(
rule,
Some(AxisRule {
min: None,
max: Some(60)
})
)
}
}
#[test]
fn parse_layer_fails() {
for name in &["[hi]", "[45opsz]", "Medium [499‹wg]"] {
assert!(AxisRule::from_layer_name(name).is_none(), "{name}")
}
}
#[test]
fn v2_bracket_layers() {
let font = Font::load(&glyphs2_dir().join("WorkSans-minimal-bracketlayer.glyphs")).unwrap();
let glyph = font.glyphs.get("colonsign").unwrap();
assert_eq!(glyph.layers.len(), 3);
assert_eq!(glyph.bracket_layers.len(), 3);
assert!(
glyph
.layers
.iter()
.all(|l| l.attributes.axis_rules.is_empty())
);
assert!(
glyph
.bracket_layers
.iter()
.all(|l| !l.attributes.axis_rules.is_empty())
);
}
#[test]
fn v3_bracket_layers() {
let font = Font::load(&glyphs3_dir().join("LibreFranklin-bracketlayer.glyphs")).unwrap();
let glyph = font.glyphs.get("peso").unwrap();
assert_eq!(glyph.layers.len(), 2);
assert_eq!(glyph.bracket_layers.len(), 2);
assert!(
glyph
.layers
.iter()
.all(|l| l.attributes.axis_rules.is_empty())
);
assert!(
glyph
.bracket_layers
.iter()
.all(|l| !l.attributes.axis_rules.is_empty())
);
}
#[test]
fn glyphs2_weight_class_custom_instance_parameter() {
let font = Font::load(&glyphs2_dir().join("WeightClassCustomParam.glyphs")).unwrap();
let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
assert_eq!(
mapping.as_slice(),
&[
(OrderedFloat(200f64), OrderedFloat(42f64)),
(OrderedFloat(700.), OrderedFloat(125.)),
(OrderedFloat(1000.), OrderedFloat(208.))
]
)
}
#[test]
fn glyphs2_avoid_adding_mapping_from_master() {
let font = Font::load(&glyphs2_dir().join("master_missing_from_instances.glyphs")).unwrap();
let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
assert_eq!(
mapping.as_slice(),
&[
(OrderedFloat(100f64), OrderedFloat(20f64)),
(OrderedFloat(300.), OrderedFloat(50.)),
(OrderedFloat(400.), OrderedFloat(71.)),
(OrderedFloat(700.), OrderedFloat(156.)),
(OrderedFloat(900.), OrderedFloat(226.)),
]
)
}
#[test]
fn does_not_truncate_axis_location() {
let font = Font::load(&glyphs3_dir().join("WghtVar_AxisLocationFloat.glyphs")).unwrap();
let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
assert_eq!(
mapping.as_slice(),
&[(OrderedFloat(400.75), OrderedFloat(0f64)),]
)
}
fn make_axis_location_params(values: &[(&str, i64)]) -> RawCustomParameterValue {
RawCustomParameterValue {
name: "Axis Location".into(),
value: Plist::Array(
values
.iter()
.map(|(name, loc)| {
Plist::Dictionary(BTreeMap::from([
("Axis".into(), Plist::String(name.to_string())),
("Location".into(), Plist::Integer(*loc)),
]))
})
.collect(),
),
disabled: None,
}
}
#[test]
fn user_to_design_with_no_axes() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let mut myfont = RawFont {
font_master: vec![RawFontMaster {
custom_parameters: RawCustomParameters(vec![make_axis_location_params(&[(
"Weight", 400,
)])]),
..Default::default()
}],
..Default::default()
};
let _just_dont_crash_plz = user_to_design_from_axis_location(&mut myfont);
}
#[test]
fn user_to_design_with_unknown_axis_location() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let mut myfont = RawFont {
axes: vec![Axis {
name: "Weight".into(),
tag: "wght".into(),
hidden: None,
}],
font_master: vec![RawFontMaster {
custom_parameters: RawCustomParameters(vec![make_axis_location_params(&[
("Weight", 400),
("Italic", 0),
])]),
axes_values: vec![60.0.into()],
..Default::default()
}],
..Default::default()
};
let _just_dont_crash_plz = user_to_design_from_axis_location(&mut myfont);
}
#[test]
fn axis_location_string_value() {
let raw = Plist::Dictionary(BTreeMap::from([
("Axis".into(), Plist::String("Weight".into())),
("Location".into(), Plist::String("42".into())),
]));
assert_eq!(
Some(AxisLocation {
axis_name: "Weight".into(),
location: 42.0.into(),
}),
raw.as_axis_location()
);
}
#[test]
fn glyphs2_unicode_ranges() {
let font = Font::load(&glyphs2_dir().join("UnicodeRanges.glyphs")).unwrap();
assert_eq!(
Some(BTreeSet::from([8])),
font.custom_parameters.unicode_range_bits
);
}
#[test]
fn parse_smart_components_v3() {
let font = RawFont::load(&glyphs3_dir().join("SmartComponents.glyphs")).unwrap();
let font = Font::try_from(font).unwrap();
let shoulder = font.glyphs.get("_part.shoulder").unwrap();
assert_eq!(
shoulder.smart_component_axes,
BTreeMap::from([
("shoulderWidth".into(), 0..=100),
("crotchDepth".into(), -100..=0)
])
);
let m = font.glyphs.get("m").unwrap();
let expected = [
Component {
name: "_part.stem".into(),
..Default::default()
},
Component {
name: "_part.shoulder".into(),
transform: Affine::translate((17., 0.)),
smart_component_values: BTreeMap::from([
("crotchDepth".into(), -44.28304),
("shoulderWidth".into(), 28.78011),
]),
..Default::default()
},
Component {
name: "_part.shoulder".into(),
transform: Affine::translate((180., 1.)),
smart_component_values: BTreeMap::from([
("crotchDepth".into(), -81.24796),
("shoulderWidth".into(), 0.0),
]),
..Default::default()
},
];
assert_eq!(
m.layers[0].components().cloned().collect::<Vec<_>>(),
expected
);
}
fn instantiate_shoulder_at(location: &[(&str, f64)]) -> Shape {
fn make_component_values(inp: &[(&str, f64)]) -> BTreeMap<SmolStr, f64> {
inp.iter().map(|(k, v)| (k.to_smolstr(), *v)).collect()
}
let font = RawFont::load(&glyphs3_dir().join("SmartComponents.glyphs")).unwrap();
let font = Font::try_from(font).unwrap();
let smart_comp = font.glyphs.get("_part.shoulder").unwrap();
let default_pos = Component {
name: "_part.shoulder".into(),
smart_component_values: make_component_values(location),
..Default::default()
};
let instance =
smart_components::instantiate_for_layer("m01", &default_pos, smart_comp).unwrap();
assert_eq!(instance.shapes.len(), 1);
instance.shapes[0].clone()
}
#[test]
fn instantiate_smart_component_at_default() {
let font = Font::load(&glyphs3_dir().join("SmartComponents.glyphs")).unwrap();
let smart_comp = font.glyphs.get("_part.shoulder").unwrap();
let shape = instantiate_shoulder_at(&[("shoulderWidth", 100.), ("crotchDepth", 0.)]);
assert_eq!(smart_comp.layers[1].layer_id, "m01");
assert_eq!(shape, smart_comp.layers[1].shapes[0]);
}
#[test]
fn instantiate_smart_component_at_default_empty_values() {
let font = Font::load(&glyphs3_dir().join("SmartComponents.glyphs")).unwrap();
let smart_comp = font.glyphs.get("_part.shoulder").unwrap();
let component = Component {
name: "_part.shoulder".into(),
smart_component_values: BTreeMap::new(),
..Default::default()
};
let instance =
smart_components::instantiate_for_layer("m01", &component, smart_comp).unwrap();
assert_eq!(instance.shapes.len(), 1);
let shape_with_explicit_defaults =
instantiate_shoulder_at(&[("shoulderWidth", 100.), ("crotchDepth", 0.)]);
assert_eq!(instance.shapes[0], shape_with_explicit_defaults);
}
#[test]
fn instantiate_smart_component_at_not_default() {
let Shape::Path(path) =
instantiate_shoulder_at(&[("shoulderWidth", 0.), ("crotchDepth", -100.)])
else {
panic!("wat")
};
assert_eq!(path.nodes.last().unwrap().pt.round().y, 267.0);
assert_eq!(
path.nodes.iter().map(|nd| nd.pt.x.round() as i64).max(),
Some(335)
);
}
impl crate::Path {
fn bbox(&self) -> Option<Rect> {
let points = self.to_points();
let (head, rest) = points.as_slice().split_first()?;
Some(
rest.iter()
.fold(Rect::ZERO.with_origin(*head), |bbox, pt| bbox.union_pt(*pt)),
)
}
}
#[test]
fn smart_component_v2() {
let font = Font::load(&glyphs2_dir().join("SmartComponent.glyphs")).unwrap();
let glyph = font.glyphs.get("composite").unwrap();
let shapes = &glyph.layers[0].shapes;
let paths = shapes
.iter()
.map(|s| s.as_path().unwrap().to_points())
.collect::<Vec<_>>();
let expected1 = [(9., 405.), (9., 300.), (276., 300.), (276.0, 405.0)]
.into_iter()
.map(Point::from)
.collect::<Vec<_>>();
let expected2 = [(9., 405.), (9., 383.), (276., 383.), (276.0, 405.0)]
.into_iter()
.map(|pt| Point::from(pt) + Vec2::new(100., 100.))
.collect::<Vec<_>>();
assert_eq!(paths, [expected1, expected2]);
}
#[test]
fn nested_smart_components() {
let font = Font::load(&glyphs2_dir().join("NestedSmartComponent.glyphs")).unwrap();
let glyph = font.glyphs.get("w").unwrap();
let shapes = &glyph.layers[0].shapes;
let rects = shapes
.iter()
.map(|s| s.as_path().unwrap().bbox().unwrap())
.collect::<Vec<_>>();
assert_eq!(
rects,
[
Rect::new(0., 200., 400., 400.),
Rect::new(0., 0., 400., 100.),
Rect::new(500., 200., 550., 400.),
Rect::new(500., 0., 550., 100.),
]
);
}
#[test]
fn smart_component_preserves_explicit_anchors() {
let font = Font::load(&glyphs3_dir().join("SmartComponentExplicitAnchors.glyphs")).unwrap();
let glyph = font.glyphs.get("composite").unwrap();
let layer = &glyph.layers[0];
let anchor_map: std::collections::HashMap<&str, (f64, f64)> = layer
.anchors
.iter()
.map(|a| (a.name.as_str(), (a.pos.x, a.pos.y)))
.collect();
assert_eq!(
anchor_map.get("top"),
Some(&(300.0, 700.0)),
"explicit 'top' anchor was overridden by smart component"
);
assert_eq!(
anchor_map.get("bottom"),
Some(&(300.0, 0.0)),
"explicit 'bottom' anchor was overridden by smart component"
);
}
#[test]
fn smart_component_preserves_underscore_anchors() {
let font =
Font::load(&glyphs3_dir().join("SmartComponentUnderscoreAnchors.glyphs")).unwrap();
let glyph = font.glyphs.get("knmAU").unwrap();
let layer = &glyph.layers[0];
let anchor_map: std::collections::HashMap<&str, (f64, f64)> = layer
.anchors
.iter()
.map(|a| (a.name.as_str(), (a.pos.x, a.pos.y)))
.collect();
assert_eq!(
anchor_map.get("_headline"),
Some(&(135.0, 383.0)),
"_headline anchor was dropped during smart component instantiation"
);
assert_eq!(
anchor_map.get("top"),
Some(&(250.0, 600.0)),
"top anchor was dropped during smart component instantiation"
);
}
#[test]
fn parse_basic_corner_component_hint() {
let font = Font::load_raw(glyphs3_dir().join("CornerComponents.glyphs")).unwrap();
let glyph = font.glyphs.get("aa_simple_angleinstroke").unwrap();
let hints = &glyph.layers[0].hints;
assert_eq!(hints.len(), 1);
assert_eq!(hints[0].name, "_corner.one");
assert_eq!(hints[0].shape_index, 0);
assert_eq!(hints[0].node_index, 0);
assert_eq!(hints[0].type_, HintType::Corner);
assert_eq!(hints[0].scale, Default::default());
assert_eq!(hints[0].alignment, Alignment::OutStroke);
}
#[test]
fn parse_corner_component_hint_with_scale() {
let font = Font::load_raw(glyphs3_dir().join("CornerComponents.glyphs")).unwrap();
let glyph = font.glyphs.get("ac_scale").unwrap();
let hints = &glyph.layers[0].hints;
assert_eq!(hints.len(), 1);
assert_eq!(hints[0].name, "_corner.one");
assert_eq!(hints[0].shape_index, 0);
assert_eq!(hints[0].node_index, 0);
assert_eq!(hints[0].type_, HintType::Corner);
assert_eq!(hints[0].scale, (Scale::new(1.2, 1.5)));
assert_eq!(hints[0].alignment, Alignment::OutStroke);
}
#[test]
fn parse_corner_component_hint_with_alignment() {
let font = Font::load_raw(glyphs3_dir().join("CornerComponents.glyphs")).unwrap();
let glyph = font.glyphs.get("aj_right_alignment").unwrap();
let hints = &glyph.layers[0].hints;
assert_eq!(hints.len(), 1);
assert_eq!(hints[0].name, "_corner.one");
assert_eq!(hints[0].shape_index, 0);
assert_eq!(hints[0].node_index, 1);
assert_eq!(hints[0].type_, HintType::Corner);
assert_eq!(hints[0].scale, Default::default());
assert_eq!(hints[0].alignment, Alignment::InStroke);
}
#[test]
fn parse_v2_corner_component() {
let hint_plist = r#"
{
origin = "{0, 0}";
scale = "{0.7, 1}";
type = Corner;
name = _corner.hi;
}
"#;
let hint = RawHint::parse_plist(hint_plist).unwrap().to_hint().unwrap();
assert_eq!(hint.scale.x, 0.7);
assert_eq!(hint.scale.y, 1.0);
}
#[test]
fn link_metrics_with_first_master_parsing() {
let font = Font::load(&glyphs3_dir().join("LinkMetricsWithFirstMaster.glyphs")).unwrap();
assert_eq!(font.masters.len(), 2);
assert_eq!(
font.masters[0]
.custom_parameters
.link_metrics_with_first_master,
None
);
assert_eq!(
font.masters[1]
.custom_parameters
.link_metrics_with_first_master,
Some(true)
);
}
#[test]
fn link_metrics_with_master_parsing() {
let font = Font::load(&glyphs3_dir().join("LinkMetricsWithMaster.glyphs")).unwrap();
assert_eq!(font.masters.len(), 3);
assert_eq!(
font.masters[0].custom_parameters.link_metrics_with_master,
None
);
assert_eq!(
font.masters[1].custom_parameters.link_metrics_with_master,
None
);
let link_target = font.masters[2]
.custom_parameters
.link_metrics_with_master
.as_deref();
assert_eq!(link_target, Some("m02"));
assert_eq!(font.masters[1].id, "m02");
assert_eq!(font.masters[1].name, "Bold");
}
#[test]
fn metrics_source_id_with_first_master() {
let font = Font::load(&glyphs3_dir().join("LinkMetricsWithFirstMaster.glyphs")).unwrap();
assert_eq!(font.masters[0].metrics_source_id, None);
assert_eq!(
font.masters[1].metrics_source_id.as_ref(),
Some(&font.masters[0].id)
);
}
#[test]
fn metrics_source_id_with_master_by_id() {
let font = Font::load(&glyphs3_dir().join("LinkMetricsWithMaster.glyphs")).unwrap();
assert_eq!(font.masters[0].metrics_source_id, None);
assert_eq!(font.masters[1].metrics_source_id, None);
assert_eq!(
font.masters[2].metrics_source_id.as_ref(),
Some(&font.masters[1].id)
);
}
#[test]
fn metrics_source_id_with_master_by_name() {
let font = Font::load(&glyphs2_dir().join("LinkMetricsWithMasterByName.glyphs")).unwrap();
assert_eq!(font.masters[0].metrics_source_id, None);
assert_eq!(font.masters[1].metrics_source_id, None);
assert_eq!(
font.masters[2]
.custom_parameters
.link_metrics_with_master
.as_deref(),
Some("Bold")
);
assert_eq!(
font.masters[2].metrics_source_id.as_ref(),
Some(&font.masters[1].id)
);
}
#[test]
fn metrics_source_id_with_missing_master() {
let font = Font::load(&glyphs3_dir().join("LinkMetricsWithMissingMaster.glyphs")).unwrap();
assert_eq!(
font.masters[1]
.custom_parameters
.link_metrics_with_master
.as_deref(),
Some("NonExistent")
);
assert_eq!(font.masters[1].metrics_source_id, None);
}
#[test]
fn duplicate_custom_params_uses_first_value() {
let raw_params = RawCustomParameters(vec![
RawCustomParameterValue {
name: "Link Metrics With Master".into(),
value: Plist::String("first_master_id".into()),
disabled: None,
},
RawCustomParameterValue {
name: "Link Metrics With Master".into(),
value: Plist::String("second_master_id".into()),
disabled: None,
},
]);
let params = raw_params.to_custom_params().unwrap();
assert_eq!(
params.link_metrics_with_master.as_deref(),
Some("first_master_id")
);
}
#[test]
fn parsing_name_table_entries() {
assert_eq!(
NameTableEntry::from_str("1024; FOO; BAZ").unwrap(),
NameTableEntry {
name_id: 1024,
platform_id: 3,
encoding_id: 1,
lang_id: 0x409,
value: "FOO; BAZ".into()
}
);
assert_eq!(
NameTableEntry::from_str("2048 1; FOO").unwrap(),
NameTableEntry {
name_id: 2048,
platform_id: 1,
encoding_id: 0,
lang_id: 0,
value: "FOO".into()
}
);
assert_eq!(
NameTableEntry::from_str("4096 1 2; FOO").unwrap(),
NameTableEntry {
name_id: 4096,
platform_id: 1,
encoding_id: 2,
lang_id: 0,
value: "FOO".into()
}
);
assert_eq!(
NameTableEntry::from_str("8192 1 2 3; FOO").unwrap(),
NameTableEntry {
name_id: 8192,
platform_id: 1,
encoding_id: 2,
lang_id: 3,
value: "FOO".into()
}
);
assert_eq!(
NameTableEntry::from_str("27 3 1 0x404; HEX").unwrap(),
NameTableEntry {
name_id: 27,
platform_id: 3,
encoding_id: 1,
lang_id: 0x404,
value: "HEX".into()
}
)
}
#[test]
fn parse_color_palette_numeric() {
let plist = r#"{ colorPalette = 2; }"#;
let attrs = LayerAttributes::parse_plist(plist).unwrap();
assert_eq!(attrs.color_palette, Some(2));
}
#[test]
fn parse_color_palette_star() {
let plist = r#"{ colorPalette = "*"; }"#;
let attrs = LayerAttributes::parse_plist(plist).unwrap();
assert_eq!(attrs.color_palette, Some(0xFFFF));
}
#[test]
fn ensure_lang_map_is_sorted() {
assert!(GLYPHS_TO_OPENTYPE_LANGUAGE_ID.is_sorted())
}
#[test]
fn codepage_bits_from_integers() {
let plist = Plist::Array(vec![Plist::Integer(1252), Plist::Integer(1258)]);
assert_eq!(
plist.as_codepage_bits(),
Some(BTreeSet::from([0, 8])) );
}
#[test]
fn codepage_bits_from_strings() {
let plist = Plist::Array(vec![
Plist::String("1252".into()),
Plist::String("1258".into()),
Plist::String("bit 29".into()),
]);
assert_eq!(plist.as_codepage_bits(), Some(BTreeSet::from([0, 8, 29])));
}
#[test]
fn read_codepage_range_bits_from_file() {
let font = Font::load(&glyphs2_dir().join("WghtVar_OS2.glyphs")).unwrap();
assert_eq!(
font.custom_parameters.codepage_range_bits,
Some(BTreeSet::from([0, 1, 19, 63]))
);
}
#[test]
fn v2_instance_interpolation_custom_third_axis() {
let font =
Font::load(&glyphs2_dir().join("ThreeAxisWithInterpolationCustom.glyphs")).unwrap();
assert_eq!(
vec![
(
"Regular Caption",
vec![("Weight", 400.0), ("Width", 100.0), ("Optical Size", 12.0)]
),
(
"Bold Display",
vec![("Weight", 700.0), ("Width", 100.0), ("Optical Size", 72.0)]
),
],
font.instances
.iter()
.map(|inst| (
inst.name.as_str(),
font.axes
.iter()
.zip(&inst.axes_values)
.map(|(a, v)| (a.name.as_str(), v.0))
.collect::<Vec<_>>()
))
.collect::<Vec<_>>()
);
}
#[test]
fn retains_font_level_user_data() {
let font = Font::load_from_string(
r#"{
.formatVersion = 3;
fontMaster = (
{
id = "m01";
}
);
unitsPerEm = 1000;
userData = {
com.github.googlei18n.ufo2ft.featureWriters = (
{
class = KernFeatureWriter;
options = {
mode = append;
};
}
);
};
}"#,
)
.unwrap();
let writers = font
.user_data
.get("com.github.googlei18n.ufo2ft.featureWriters")
.and_then(Plist::as_array)
.expect("featureWriters retained as an array");
assert_eq!(writers.len(), 1);
assert_eq!(
writers[0].get("class").and_then(Plist::as_str),
Some("KernFeatureWriter")
);
}
}