glyphs_reader/
font.rs

1//! The general strategy is just to use a plist for storage. Also, lots of
2//! unwrapping.
3//!
4//! There are lots of other ways this could go, including something serde-like
5//! where it gets serialized to more Rust-native structures, proc macros, etc.
6
7use std::borrow::Cow;
8use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
9use std::ffi::OsStr;
10use std::hash::Hash;
11use std::str::FromStr;
12use std::{fs, path};
13
14use crate::glyphdata::{Category, GlyphData, Subcategory};
15use ascii_plist_derive::FromPlist;
16use fontdrasil::types::WidthClass;
17use indexmap::{IndexMap, IndexSet};
18use kurbo::{Affine, Point, Vec2};
19use log::{debug, warn};
20use ordered_float::OrderedFloat;
21use regex::Regex;
22use smol_str::SmolStr;
23
24use crate::error::Error;
25use crate::plist::{FromPlist, Plist, Token, Tokenizer, VecDelimiters};
26
27const V3_METRIC_NAMES: [&str; 6] = [
28    "ascender",
29    "baseline",
30    "descender",
31    "cap height",
32    "x-height",
33    "italic angle",
34];
35
36#[derive(Clone, Debug, Default, PartialEq, Hash)]
37pub struct UserToDesignMapping(BTreeMap<String, AxisUserToDesignMap>);
38
39#[derive(Clone, Debug, Default, PartialEq, Hash)]
40pub struct AxisUserToDesignMap(Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>);
41
42/// A tidied up font from a plist.
43///
44/// Normalized representation of Glyphs 2/3 content
45#[derive(Debug, Default, PartialEq, Hash)]
46pub struct Font {
47    pub units_per_em: u16,
48    pub axes: Vec<Axis>,
49    pub masters: Vec<FontMaster>,
50    pub default_master_idx: usize,
51    pub glyphs: BTreeMap<SmolStr, Glyph>,
52    pub glyph_order: Vec<SmolStr>,
53    // tag => (user:design) tuples
54    pub axis_mappings: UserToDesignMapping,
55    pub virtual_masters: Vec<BTreeMap<String, OrderedFloat<f64>>>,
56    pub features: Vec<FeatureSnippet>,
57    pub names: BTreeMap<String, String>,
58    pub instances: Vec<Instance>,
59    pub version_major: i32,
60    pub version_minor: u32,
61    pub date: Option<String>,
62
63    // master id => { (name or class, name or class) => adjustment }
64    pub kerning_ltr: Kerning,
65    pub kerning_rtl: Kerning,
66
67    pub custom_parameters: CustomParameters,
68}
69
70/// Custom parameter options that can be set on a glyphs font
71#[derive(Clone, Debug, PartialEq, Hash, Default)]
72pub struct CustomParameters {
73    pub propagate_anchors: Option<bool>,
74    pub use_typo_metrics: Option<bool>,
75    pub is_fixed_pitch: Option<bool>,
76    pub fs_type: Option<u16>,
77    pub has_wws_names: Option<bool>,
78    pub typo_ascender: Option<i64>,
79    pub typo_descender: Option<i64>,
80    pub typo_line_gap: Option<i64>,
81    pub win_ascent: Option<i64>,
82    pub win_descent: Option<i64>,
83    pub hhea_ascender: Option<i64>,
84    pub hhea_descender: Option<i64>,
85    pub hhea_line_gap: Option<i64>,
86    pub vhea_ascender: Option<i64>,
87    pub vhea_descender: Option<i64>,
88    pub vhea_line_gap: Option<i64>,
89    pub underline_thickness: Option<OrderedFloat<f64>>,
90    pub underline_position: Option<OrderedFloat<f64>>,
91    pub strikeout_position: Option<i64>,
92    pub strikeout_size: Option<i64>,
93    pub subscript_x_offset: Option<i64>,
94    pub subscript_x_size: Option<i64>,
95    pub subscript_y_offset: Option<i64>,
96    pub subscript_y_size: Option<i64>,
97    pub superscript_x_offset: Option<i64>,
98    pub superscript_x_size: Option<i64>,
99    pub superscript_y_offset: Option<i64>,
100    pub superscript_y_size: Option<i64>,
101    pub unicode_range_bits: Option<BTreeSet<u32>>,
102    pub codepage_range_bits: Option<BTreeSet<u32>>,
103    pub panose: Option<Vec<i64>>,
104
105    pub lowest_rec_ppem: Option<i64>,
106    pub hhea_caret_slope_run: Option<i64>,
107    pub hhea_caret_slope_rise: Option<i64>,
108    pub hhea_caret_offset: Option<i64>,
109    pub vhea_caret_slope_run: Option<i64>,
110    pub vhea_caret_slope_rise: Option<i64>,
111    pub vhea_caret_offset: Option<i64>,
112    pub meta_table: Option<MetaTableValues>,
113    pub dont_use_production_names: Option<bool>,
114    // these fields are parsed via the config, but are stored
115    // in the top-level `Font` struct
116    pub virtual_masters: Option<Vec<BTreeMap<String, OrderedFloat<f64>>>>,
117    pub glyph_order: Option<Vec<SmolStr>>,
118    pub gasp_table: Option<BTreeMap<i64, i64>>,
119    pub feature_for_feature_variations: Option<SmolStr>,
120}
121
122/// Values for the 'meta Table' custom parameter
123#[derive(Clone, Debug, PartialEq, Hash, Default)]
124pub struct MetaTableValues {
125    pub dlng: Vec<SmolStr>,
126    pub slng: Vec<SmolStr>,
127}
128
129impl MetaTableValues {
130    fn from_plist(plist: &Plist) -> Option<Self> {
131        let mut ret = MetaTableValues::default();
132        let as_array = plist.as_array()?;
133        for item in as_array {
134            let tag = item.get("tag").and_then(Plist::as_str)?;
135            let data = item.get("data").and_then(Plist::as_str)?;
136            let data = data.split(',').map(SmolStr::new).collect();
137
138            match tag {
139                "dlng" => ret.dlng = data,
140                "slng" => ret.slng = data,
141                _ => log::warn!("Unknown meta table tag '{tag}'"),
142            }
143        }
144
145        if ret.dlng.len() + ret.slng.len() > 0 {
146            Some(ret)
147        } else {
148            None
149        }
150    }
151}
152
153/// master id => { (name or class, name or class) => adjustment }
154#[derive(Clone, Debug, Default, PartialEq, Hash)]
155pub struct Kerning(BTreeMap<String, BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>>);
156
157impl Kerning {
158    pub fn get(&self, master_id: &str) -> Option<&BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>> {
159        self.0.get(master_id)
160    }
161
162    pub fn keys(&self) -> impl Iterator<Item = &String> {
163        self.0.keys()
164    }
165
166    pub fn iter(
167        &self,
168    ) -> impl Iterator<Item = (&String, &BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>)> {
169        self.0.iter()
170    }
171
172    fn insert(
173        &mut self,
174        master_id: String,
175        lhs_class_or_group: SmolStr,
176        rhs_class_or_group: SmolStr,
177        kern: f64,
178    ) {
179        *self
180            .0
181            .entry(master_id)
182            .or_default()
183            .entry((lhs_class_or_group, rhs_class_or_group))
184            .or_default() = kern.into();
185    }
186}
187
188/// Hand-parse because it's a bit weird
189impl FromPlist for Kerning {
190    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
191        let mut kerning = Kerning::default();
192
193        tokenizer.eat(b'{')?;
194
195        loop {
196            if tokenizer.eat(b'}').is_ok() {
197                break;
198            }
199
200            // parse string-that-is-master-id = {
201            let master_id: String = tokenizer.parse()?;
202            tokenizer.eat(b'=')?;
203
204            // The map for the master
205            tokenizer.eat(b'{')?;
206            loop {
207                if tokenizer.eat(b'}').is_ok() {
208                    break;
209                }
210                let lhs_name_or_class: SmolStr = tokenizer.parse()?;
211                tokenizer.eat(b'=')?;
212                tokenizer.eat(b'{')?;
213
214                // rhs name = value pairs
215                loop {
216                    if tokenizer.eat(b'}').is_ok() {
217                        break;
218                    }
219
220                    let rhs_name_or_class: SmolStr = tokenizer.parse()?;
221                    tokenizer.eat(b'=')?;
222                    let value: f64 = tokenizer.parse()?;
223                    tokenizer.eat(b';')?;
224
225                    kerning.insert(
226                        master_id.clone(),
227                        lhs_name_or_class.clone(),
228                        rhs_name_or_class,
229                        value,
230                    );
231                }
232                tokenizer.eat(b';')?;
233            }
234
235            tokenizer.eat(b';')?;
236        }
237
238        Ok(kerning)
239    }
240}
241
242/// A chunk of FEA code
243#[derive(Debug, PartialEq, Eq, Hash)]
244pub struct FeatureSnippet {
245    pub content: String,
246    /// If `true` the content should be ignored.
247    pub disabled: bool,
248}
249
250impl FeatureSnippet {
251    pub fn new(content: String, disabled: bool) -> Self {
252        FeatureSnippet { content, disabled }
253    }
254
255    pub fn str_if_enabled(&self) -> Option<&str> {
256        (!self.disabled).then_some(&self.content)
257    }
258}
259
260#[derive(Clone, Default, Debug, PartialEq, Hash)]
261pub struct Glyph {
262    pub name: SmolStr,
263    pub export: bool,
264    pub layers: Vec<Layer>,
265    pub bracket_layers: Vec<Layer>,
266    pub unicode: BTreeSet<u32>,
267    /// The left kerning group
268    pub left_kern: Option<SmolStr>,
269    /// The right kerning group
270    pub right_kern: Option<SmolStr>,
271    pub category: Option<Category>,
272    pub sub_category: Option<Subcategory>,
273    pub production_name: Option<SmolStr>,
274}
275
276impl Glyph {
277    pub fn is_nonspacing_mark(&self) -> bool {
278        matches!(
279            (self.category, self.sub_category),
280            (Some(Category::Mark), Some(Subcategory::Nonspacing))
281        )
282    }
283
284    pub(crate) fn has_components(&self) -> bool {
285        self.layers
286            .iter()
287            .chain(self.bracket_layers.iter())
288            .flat_map(Layer::components)
289            .next()
290            .is_some()
291    }
292}
293
294#[derive(Debug, Default, Clone, PartialEq, Hash)]
295pub struct Layer {
296    pub layer_id: String,
297    pub associated_master_id: Option<String>,
298    pub width: OrderedFloat<f64>,
299    pub vert_width: Option<OrderedFloat<f64>>,
300    pub vert_origin: Option<OrderedFloat<f64>>,
301    pub shapes: Vec<Shape>,
302    pub anchors: Vec<Anchor>,
303    pub attributes: LayerAttributes,
304}
305
306impl Layer {
307    pub fn is_master(&self) -> bool {
308        self.associated_master_id.is_none()
309    }
310
311    /// associated master id if set, else layer id
312    pub fn master_id(&self) -> &str {
313        self.associated_master_id
314            .as_deref()
315            .unwrap_or(&self.layer_id)
316    }
317
318    /// A key used to identify a bracket layer during anchor propagation
319    pub(crate) fn axis_rules_key(&self) -> Option<String> {
320        (!self.attributes.axis_rules.is_empty())
321            .then(|| format!("{:?} {}", self.attributes.axis_rules, self.master_id()))
322    }
323
324    pub fn is_intermediate(&self) -> bool {
325        self.associated_master_id.is_some() && !self.attributes.coordinates.is_empty()
326    }
327
328    pub(crate) fn components(&self) -> impl Iterator<Item = &Component> + '_ {
329        self.shapes.iter().filter_map(|shape| match shape {
330            Shape::Path(_) => None,
331            Shape::Component(comp) => Some(comp),
332        })
333    }
334
335    /// NOTE: panics if called on a non-bracket layer
336    fn bracket_info(&self, axes: &[Axis]) -> BTreeMap<String, (Option<i64>, Option<i64>)> {
337        assert!(
338            !self.attributes.axis_rules.is_empty(),
339            "all bracket layers have axis rules"
340        );
341        axes.iter()
342            .zip(&self.attributes.axis_rules)
343            .map(|(axis, rule)| (axis.tag.clone(), (rule.min, rule.max)))
344            .collect()
345    }
346
347    // TODO add is_alternate, is_color, etc.
348}
349
350#[derive(Clone, Default, Debug, PartialEq, Hash)]
351pub struct LayerAttributes {
352    pub coordinates: Vec<OrderedFloat<f64>>,
353    pub color: bool,
354    // in the same order that axes are declared for the font
355    pub axis_rules: Vec<AxisRule>,
356}
357
358#[derive(Clone, Default, FromPlist, Debug, PartialEq, Hash)]
359pub struct AxisRule {
360    // if missing, assume default min/max for font
361    pub min: Option<i64>,
362    pub max: Option<i64>,
363}
364
365impl AxisRule {
366    /// Parse an AxisRule from a glyphs v2 layer name.
367    ///
368    /// See <https://glyphsapp.com/learn/alternating-glyph-shapes> for an
369    /// overview of the naming conventions.
370    fn from_layer_name(name: &str) -> Option<Self> {
371        // the pattern can either be "$name [100]" or "$name ]100]"
372        // (python uses a regex for this:)
373        // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d/Lib/glyphsLib/classes.py#L3938
374        let idx = name.find([']', '['])?;
375        let reversed = name.as_bytes()[idx] == b']';
376        let tail = name.get(idx + 1..)?;
377        let (value, _) = tail.split_once(']')?;
378        let value = str::parse::<u32>(value.trim()).ok()?;
379        let (min, max) = if reversed {
380            (None, Some(value as _))
381        } else {
382            (Some(value as _), None)
383        };
384        Some(AxisRule { min, max })
385    }
386}
387
388// hand-parse because they can take multiple shapes
389impl FromPlist for LayerAttributes {
390    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
391        let mut coordinates = Vec::new();
392        let mut color = false;
393        let mut axis_rules = Vec::new();
394
395        tokenizer.eat(b'{')?;
396
397        loop {
398            if tokenizer.eat(b'}').is_ok() {
399                break;
400            }
401
402            let key: String = tokenizer.parse()?;
403            tokenizer.eat(b'=')?;
404            match key.as_str() {
405                "coordinates" => coordinates = tokenizer.parse()?,
406                "color" => color = tokenizer.parse()?,
407                "axisRules" => axis_rules = tokenizer.parse()?,
408                // skip unsupported attributes for now
409                // TODO: match the others
410                _ => tokenizer.skip_rec()?,
411            }
412            tokenizer.eat(b';')?;
413        }
414
415        Ok(LayerAttributes {
416            coordinates,
417            color,
418            axis_rules,
419        })
420    }
421}
422
423#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
424pub struct ShapeAttributes {
425    pub gradient: Gradient,
426}
427
428#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
429pub struct Gradient {
430    pub start: Vec<OrderedFloat<f64>>,
431    pub end: Vec<OrderedFloat<f64>>,
432    pub colors: Vec<Color>,
433    #[fromplist(key = "type")]
434    pub style: String,
435}
436
437#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)]
438pub struct Color {
439    pub r: i64,
440    pub g: i64,
441    pub b: i64,
442    pub a: i64,
443    // The position on the color line, see <https://learn.microsoft.com/en-us/typography/opentype/spec/colr#color-lines>
444    pub stop_offset: OrderedFloat<f64>,
445}
446
447// hand-parse because it's a list of inconsistent types
448impl FromPlist for Color {
449    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
450        tokenizer.eat(b'(')?;
451
452        let colors = tokenizer.parse::<Vec<i64>>()?;
453        tokenizer.eat(b',')?;
454        let stop_offset = tokenizer.parse::<f64>()?;
455        tokenizer.eat(b')')?;
456
457        // See <https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/common.py#L41-L50>
458        match *colors.as_slice() {
459            // Grayscale
460            [black, alpha] => Ok(Color {
461                r: black,
462                g: black,
463                b: black,
464                a: alpha,
465                stop_offset: stop_offset.into(),
466            }),
467            // RGB
468            [r, g, b, a] => Ok(Color {
469                r,
470                g,
471                b,
472                a,
473                stop_offset: stop_offset.into(),
474            }),
475            // 5 is CMYK, match python by not supporting that
476            _ => Err(crate::plist::Error::UnexpectedNumberOfValues {
477                value_type: "grayscale (2) or rgba (4)",
478                actual: colors.len(),
479            }),
480        }
481    }
482}
483
484#[derive(Debug, Clone, PartialEq, Hash)]
485pub enum Shape {
486    Path(Path),
487    Component(Component),
488}
489
490impl Shape {
491    pub fn attributes(&self) -> &ShapeAttributes {
492        match self {
493            Shape::Path(p) => &p.attributes,
494            Shape::Component(c) => &c.attributes,
495        }
496    }
497}
498
499#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
500enum FormatVersion {
501    #[default]
502    V2,
503    V3,
504}
505
506// The font you get directly from a plist, minimally modified
507// Types chosen specifically to accomodate plist translation.
508#[derive(Default, Debug, PartialEq, FromPlist)]
509#[allow(non_snake_case)]
510struct RawFont {
511    #[fromplist(key = ".formatVersion")]
512    format_version: FormatVersion,
513    units_per_em: Option<i64>,
514    metrics: Vec<RawMetric>,
515    family_name: String,
516    date: Option<String>,
517    copyright: Option<String>,
518    designer: Option<String>,
519    designerURL: Option<String>,
520    manufacturer: Option<String>,
521    manufacturerURL: Option<String>,
522    versionMajor: Option<i64>,
523    versionMinor: Option<i64>,
524    axes: Vec<Axis>,
525    glyphs: Vec<RawGlyph>,
526    font_master: Vec<RawFontMaster>,
527    instances: Vec<RawInstance>,
528    feature_prefixes: Vec<RawFeature>,
529    features: Vec<RawFeature>,
530    classes: Vec<RawFeature>,
531    properties: Vec<RawName>,
532    #[fromplist(alt_name = "kerning")]
533    kerning_LTR: Kerning,
534    kerning_RTL: Kerning,
535    custom_parameters: RawCustomParameters,
536    numbers: Vec<NumberName>,
537}
538
539#[derive(Default, Debug, PartialEq, FromPlist)]
540struct NumberName {
541    name: SmolStr,
542}
543
544// we use a vec of tuples instead of a map because there can be multiple
545// values for the same name (e.g. 'Virtual Master')
546#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)]
547pub(crate) struct RawCustomParameters(Vec<RawCustomParameterValue>);
548
549#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
550struct RawCustomParameterValue {
551    name: SmolStr,
552    value: Plist,
553    disabled: Option<bool>,
554}
555
556impl FromPlist for FormatVersion {
557    fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
558        let raw: i64 = FromPlist::parse(tokenizer)?;
559        if raw == 3 {
560            Ok(FormatVersion::V3)
561        } else {
562            // format version 2 is the default value, if no explicit version is set
563            Err(crate::plist::Error::Parse(
564                "'3' is the only known format version".into(),
565            ))
566        }
567    }
568}
569
570impl FormatVersion {
571    fn is_v2(self) -> bool {
572        self == FormatVersion::V2
573    }
574
575    fn codepoint_radix(self) -> u32 {
576        match self {
577            FormatVersion::V2 => 16,
578            FormatVersion::V3 => 10,
579        }
580    }
581}
582
583impl FromPlist for RawCustomParameters {
584    fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
585        Vec::parse(tokenizer).map(RawCustomParameters)
586    }
587}
588
589/// Convenience methods for converting `Plist` structs to concrete custom param types
590trait PlistParamsExt {
591    fn as_codepage_bits(&self) -> Option<BTreeSet<u32>>;
592    fn as_unicode_code_ranges(&self) -> Option<BTreeSet<u32>>;
593    fn as_fs_type(&self) -> Option<u16>;
594    fn as_mapping_values(&self) -> Option<Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>>;
595    fn as_axis_location(&self) -> Option<AxisLocation>;
596    fn as_axis(&self) -> Option<Axis>;
597    fn as_bool(&self) -> Option<bool>;
598    fn as_ordered_f64(&self) -> Option<OrderedFloat<f64>>;
599    fn as_vec_of_ints(&self) -> Option<Vec<i64>>;
600    fn as_axis_locations(&self) -> Option<Vec<AxisLocation>>;
601    fn as_vec_of_string(&self) -> Option<Vec<SmolStr>>;
602    fn as_axes(&self) -> Option<Vec<Axis>>;
603    fn as_axis_mappings(&self) -> Option<Vec<AxisMapping>>;
604    fn as_virtual_master(&self) -> Option<BTreeMap<String, OrderedFloat<f64>>>;
605    fn as_gasp_table(&self) -> Option<BTreeMap<i64, i64>>;
606}
607
608impl PlistParamsExt for Plist {
609    fn as_bool(&self) -> Option<bool> {
610        self.as_i64().map(|val| val == 1)
611    }
612
613    fn as_ordered_f64(&self) -> Option<OrderedFloat<f64>> {
614        self.as_f64().map(OrderedFloat)
615    }
616
617    fn as_vec_of_ints(&self) -> Option<Vec<i64>> {
618        // exciting! apparently glyphs (at least sometimes?) serializes
619        // this as a vec of strings? (this is true in WghtVar_OS2.glyphs)
620        if let Some(thing_that_makes_sense) = self.as_array()?.iter().map(Plist::as_i64).collect() {
621            return Some(thing_that_makes_sense);
622        }
623
624        self.as_array()?
625            .iter()
626            .map(|val| val.as_str().and_then(|s| s.parse::<i64>().ok()))
627            .collect()
628    }
629
630    fn as_axis_location(&self) -> Option<AxisLocation> {
631        let plist = self.as_dict()?;
632        let name = plist.get("Axis").and_then(Plist::as_str)?;
633        let location = plist.get("Location").and_then(Plist::as_f64)?;
634        Some(AxisLocation {
635            axis_name: name.into(),
636            location: location.into(),
637        })
638    }
639
640    fn as_axis_locations(&self) -> Option<Vec<AxisLocation>> {
641        let array = self.as_array()?;
642        array.iter().map(Plist::as_axis_location).collect()
643    }
644
645    fn as_vec_of_string(&self) -> Option<Vec<SmolStr>> {
646        self.as_array()?
647            .iter()
648            .map(|plist| plist.as_str().map(SmolStr::from))
649            .collect()
650    }
651
652    fn as_axis(&self) -> Option<Axis> {
653        let plist = self.as_dict()?;
654        let name = plist.get("Name").and_then(Plist::as_str)?;
655        let tag = plist.get("Tag").and_then(Plist::as_str)?;
656        let hidden = plist.get("hidden").and_then(Plist::as_bool);
657        Some(Axis {
658            name: name.into(),
659            tag: tag.into(),
660            hidden,
661        })
662    }
663
664    fn as_axes(&self) -> Option<Vec<Axis>> {
665        self.as_array()?.iter().map(Plist::as_axis).collect()
666    }
667
668    fn as_mapping_values(&self) -> Option<Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>> {
669        let plist = self.as_dict()?;
670        plist
671            .iter()
672            .map(|(key, value)| {
673                // our keys are strings, but we want to interpret them as floats:
674                let key = key
675                    .parse::<f64>()
676                    .or_else(|_| key.parse::<i64>().map(|i| i as f64))
677                    .ok()?;
678                let val = value.as_f64()?;
679                Some((key.into(), val.into()))
680            })
681            .collect()
682    }
683
684    fn as_axis_mappings(&self) -> Option<Vec<AxisMapping>> {
685        self.as_dict()?
686            .iter()
687            .map(|(tag, mappings)| {
688                let user_to_design = mappings.as_mapping_values()?;
689                Some(AxisMapping {
690                    tag: tag.to_string(),
691                    user_to_design,
692                })
693            })
694            .collect()
695    }
696
697    fn as_virtual_master(&self) -> Option<BTreeMap<String, OrderedFloat<f64>>> {
698        self.as_array()?
699            .iter()
700            .map(Plist::as_axis_location)
701            .map(|loc| (loc.map(|loc| (loc.axis_name, loc.location))))
702            .collect()
703    }
704
705    fn as_fs_type(&self) -> Option<u16> {
706        Some(self.as_vec_of_ints()?.iter().map(|bit| 1 << bit).sum())
707    }
708
709    fn as_unicode_code_ranges(&self) -> Option<BTreeSet<u32>> {
710        let bits = self.as_vec_of_ints()?;
711        Some(bits.iter().map(|bit| *bit as u32).collect())
712    }
713
714    fn as_codepage_bits(&self) -> Option<BTreeSet<u32>> {
715        let bits = self.as_vec_of_ints()?;
716        bits.iter()
717            .map(|b| codepage_range_bit(*b as _))
718            .collect::<Result<_, _>>()
719            .ok()
720    }
721
722    fn as_gasp_table(&self) -> Option<BTreeMap<i64, i64>> {
723        Some(
724            self.as_dict()?
725                .iter()
726                .filter_map(|(k, v)| k.parse::<i64>().ok().zip(v.as_i64()))
727                .collect(),
728        )
729    }
730}
731
732impl RawCustomParameters {
733    ////convert into the parsed params for a top-level font
734    fn to_custom_params(&self) -> Result<CustomParameters, Error> {
735        let mut params = CustomParameters::default();
736        let mut virtual_masters = Vec::<BTreeMap<String, OrderedFloat<f64>>>::new();
737        // PANOSE custom parameter is accessible under a short name and a long name:
738        //     https://github.com/googlefonts/glyphsLib/blob/050ef62c/Lib/glyphsLib/builder/custom_params.py#L322-L323
739        // ...with the value under the short name taking precendence:
740        //     https://github.com/googlefonts/glyphsLib/blob/050ef62c/Lib/glyphsLib/builder/custom_params.py#L258-L269
741        let mut panose = None;
742        let mut panose_old = None;
743
744        for RawCustomParameterValue {
745            name,
746            value,
747            disabled,
748        } in &self.0
749        {
750            // we need to use a macro here because you can't pass the name of a field to a
751            // function.
752            macro_rules! add_and_report_issues {
753                ($field:ident, $converter:path) => {{
754                    add_and_report_issues!($field, $converter(value))
755                }};
756
757                ($field:ident, $converter:path, into) => {{
758                    add_and_report_issues!($field, $converter(value).map(Into::into))
759                }};
760
761                ($field:ident, $value_expr:expr) => {{
762                    let value = $value_expr;
763
764                    if value.is_none() {
765                        log::warn!("failed to parse param for '{}'", stringify!($field));
766                    }
767                    if params.$field.is_some() {
768                        log::warn!("duplicate param value for field '{}'", stringify!($field));
769                    }
770                    params.$field = value;
771                }};
772            }
773
774            if *disabled == Some(true) {
775                log::debug!("skipping disabled custom param '{name}'");
776                continue;
777            }
778            match name.as_str() {
779                "Propagate Anchors" => add_and_report_issues!(propagate_anchors, Plist::as_bool),
780                "Use Typo Metrics" => add_and_report_issues!(use_typo_metrics, Plist::as_bool),
781                "isFixedPitch" => add_and_report_issues!(is_fixed_pitch, Plist::as_bool),
782                "Has WWS Names" => add_and_report_issues!(has_wws_names, Plist::as_bool),
783                "typoAscender" => add_and_report_issues!(typo_ascender, Plist::as_i64),
784                "typoDescender" => add_and_report_issues!(typo_descender, Plist::as_i64),
785                "typoLineGap" => add_and_report_issues!(typo_line_gap, Plist::as_i64),
786                "winAscent" => add_and_report_issues!(win_ascent, Plist::as_i64),
787                "winDescent" => add_and_report_issues!(win_descent, Plist::as_i64),
788                "hheaAscender" => add_and_report_issues!(hhea_ascender, Plist::as_i64),
789                "hheaDescender" => add_and_report_issues!(hhea_descender, Plist::as_i64),
790                "hheaLineGap" => add_and_report_issues!(hhea_line_gap, Plist::as_i64),
791                "vheaVertAscender" => add_and_report_issues!(vhea_ascender, Plist::as_i64),
792                "vheaVertDescender" => add_and_report_issues!(vhea_descender, Plist::as_i64),
793                "vheaVertLineGap" => add_and_report_issues!(vhea_line_gap, Plist::as_i64),
794                "underlineThickness" => {
795                    add_and_report_issues!(underline_thickness, Plist::as_ordered_f64)
796                }
797                "underlinePosition" => {
798                    add_and_report_issues!(underline_position, Plist::as_ordered_f64)
799                }
800                "strikeoutPosition" => add_and_report_issues!(strikeout_position, Plist::as_i64),
801                "strikeoutSize" => add_and_report_issues!(strikeout_size, Plist::as_i64),
802                "subscriptXOffset" => add_and_report_issues!(subscript_x_offset, Plist::as_i64),
803                "subscriptXSize" => add_and_report_issues!(subscript_x_size, Plist::as_i64),
804                "subscriptYOffset" => add_and_report_issues!(subscript_y_offset, Plist::as_i64),
805                "subscriptYSize" => add_and_report_issues!(subscript_y_size, Plist::as_i64),
806                "superscriptXOffset" => add_and_report_issues!(superscript_x_offset, Plist::as_i64),
807                "superscriptXSize" => add_and_report_issues!(superscript_x_size, Plist::as_i64),
808                "superscriptYOffset" => add_and_report_issues!(superscript_y_offset, Plist::as_i64),
809                "superscriptYSize" => add_and_report_issues!(superscript_y_size, Plist::as_i64),
810                "fsType" => add_and_report_issues!(fs_type, Plist::as_fs_type),
811                "unicodeRanges" => {
812                    add_and_report_issues!(unicode_range_bits, Plist::as_unicode_code_ranges)
813                }
814                "codePageRanges" => {
815                    add_and_report_issues!(codepage_range_bits, Plist::as_codepage_bits)
816                }
817                // these values are not listed in the glyphs.app UI, but exist
818                // in some fonts:
819                // https://github.com/googlefonts/fontc/pull/1144#issuecomment-2503134008
820                "openTypeHeadLowestRecPPEM" => {
821                    add_and_report_issues!(lowest_rec_ppem, Plist::as_i64)
822                }
823                "openTypeHheaCaretSlopeRun" => {
824                    add_and_report_issues!(hhea_caret_slope_run, Plist::as_i64)
825                }
826                "openTypeHheaCaretSlopeRise" => {
827                    add_and_report_issues!(hhea_caret_slope_rise, Plist::as_i64)
828                }
829                "openTypeHheaCaretOffset" => {
830                    add_and_report_issues!(hhea_caret_offset, Plist::as_i64)
831                }
832                "openTypeVheaCaretSlopeRun" => {
833                    add_and_report_issues!(vhea_caret_slope_run, Plist::as_i64)
834                }
835                "openTypeVheaCaretSlopeRise" => {
836                    add_and_report_issues!(vhea_caret_slope_rise, Plist::as_i64)
837                }
838                "openTypeVheaCaretOffset" => {
839                    add_and_report_issues!(vhea_caret_offset, Plist::as_i64)
840                }
841                "meta Table" => {
842                    add_and_report_issues!(meta_table, MetaTableValues::from_plist)
843                }
844                "Don't use Production Names" => {
845                    add_and_report_issues!(dont_use_production_names, Plist::as_bool)
846                }
847                // these might need to be handled? they're in the same list as
848                // the items above:
849                // https://github.com/googlefonts/glyphsLib/blob/74c63244fdb/Lib/glyphsLib/builder/custom_params.py#L429
850                "openTypeNameUniqueID"
851                | "openTypeNameVersion"
852                | "openTypeOS2FamilyClass"
853                | "openTypeHeadFlags" => {
854                    log::warn!("unhandled custom param '{name}'")
855                }
856
857                "Virtual Master" => match value.as_virtual_master() {
858                    Some(val) => virtual_masters.push(val),
859                    None => log::warn!("failed to parse virtual master '{value:?}'"),
860                },
861                "panose" => panose = value.as_vec_of_ints(),
862                "openTypeOS2Panose" => panose_old = value.as_vec_of_ints(),
863                "glyphOrder" => add_and_report_issues!(glyph_order, Plist::as_vec_of_string),
864                "gasp Table" => add_and_report_issues!(gasp_table, Plist::as_gasp_table),
865                "Feature for Feature Variations" => {
866                    add_and_report_issues!(feature_for_feature_variations, Plist::as_str, into)
867                }
868                _ => log::warn!("unknown custom parameter '{name}'"),
869            }
870        }
871        params.panose = panose.or(panose_old);
872        params.virtual_masters = Some(virtual_masters).filter(|x| !x.is_empty());
873        Ok(params)
874    }
875
876    /// Get the first parameter with the given name, or `None` if not found.
877    fn get(&self, name: &str) -> Option<&Plist> {
878        let item = self.0.iter().find(|val| (val.name == name))?;
879        (item.disabled != Some(true)).then_some(&item.value)
880    }
881
882    fn contains(&self, name: &str) -> bool {
883        self.0.iter().any(|val| val.name == name)
884    }
885
886    fn string(&self, name: &str) -> Option<&str> {
887        self.get(name).and_then(Plist::as_str)
888    }
889
890    fn axes(&self) -> Option<Vec<Axis>> {
891        self.get("Axes").and_then(Plist::as_axes)
892    }
893
894    fn axis_mappings(&self) -> Option<Vec<AxisMapping>> {
895        self.get("Axis Mappings").and_then(Plist::as_axis_mappings)
896    }
897
898    fn axis_locations(&self) -> Option<Vec<AxisLocation>> {
899        self.get("Axis Location").and_then(Plist::as_axis_locations)
900    }
901}
902
903#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
904pub struct AxisLocation {
905    axis_name: String,
906    location: OrderedFloat<f64>,
907}
908
909#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
910pub struct AxisMapping {
911    //TODO this should really be a `Tag`?
912    tag: String,
913    user_to_design: Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>,
914}
915
916#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
917struct RawMetric {
918    // So named to let FromPlist populate it from a field called "type"
919    type_: String,
920}
921
922#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
923struct RawName {
924    key: String,
925    value: Option<String>,
926    values: Vec<RawNameValue>,
927}
928
929impl RawName {
930    fn is_empty(&self) -> bool {
931        self.value.is_none() && self.values.is_empty()
932    }
933
934    fn get_value(&self) -> Option<&str> {
935        if let Some(value) = &self.value {
936            return Some(value.as_str());
937        }
938
939        // This is a localized (multivalued) name. Pick a winner.
940        // See <https://github.com/googlefonts/fontc/issues/1011>
941        // In order of preference: dflt, default, ENG, whatever is first
942        // <https://github.com/googlefonts/glyphsLib/blob/1cb4fc5ae2cf385df95d2b7768e7ab4eb60a5ac3/Lib/glyphsLib/classes.py#L3155-L3161>
943        self.values
944            .iter()
945            .enumerate()
946            // (score [lower better], index)
947            .map(|(i, raw)| match raw.language.as_str() {
948                "dflt" => (-3, raw.value.as_str()),
949                "default" => (-2, raw.value.as_str()),
950                "ENG" => (-1, raw.value.as_str()),
951                _ => (i as i32, raw.value.as_str()),
952            })
953            .min()
954            .map(|(_, raw)| raw)
955    }
956}
957
958#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
959struct RawNameValue {
960    language: String,
961    value: String,
962}
963
964#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
965struct RawFeature {
966    automatic: Option<i64>,
967    disabled: Option<i64>,
968    name: Option<String>,
969    tag: Option<String>,
970    notes: Option<String>,
971    code: String,
972    labels: Vec<RawNameValue>,
973
974    #[fromplist(ignore)]
975    other_stuff: BTreeMap<String, Plist>,
976}
977
978#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
979pub struct Axis {
980    #[fromplist(alt_name = "Name")]
981    pub name: String,
982    #[fromplist(alt_name = "Tag")]
983    pub tag: String,
984    pub hidden: Option<bool>,
985}
986
987#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
988struct RawGlyph {
989    layers: Vec<RawLayer>,
990    glyphname: SmolStr,
991    export: Option<bool>,
992    #[fromplist(alt_name = "leftKerningGroup")]
993    kern_left: Option<SmolStr>,
994    #[fromplist(alt_name = "rightKerningGroup")]
995    kern_right: Option<SmolStr>,
996    unicode: Option<String>,
997    category: Option<SmolStr>,
998    sub_category: Option<SmolStr>,
999    #[fromplist(alt_name = "production")]
1000    production_name: Option<SmolStr>,
1001    #[fromplist(ignore)]
1002    other_stuff: BTreeMap<String, Plist>,
1003}
1004
1005#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1006struct RawLayer {
1007    name: String,
1008    layer_id: String,
1009    associated_master_id: Option<String>,
1010    width: Option<OrderedFloat<f64>>,
1011    vert_width: Option<OrderedFloat<f64>>,
1012    vert_origin: Option<OrderedFloat<f64>>,
1013    shapes: Vec<RawShape>,
1014    paths: Vec<Path>,
1015    components: Vec<Component>,
1016    anchors: Vec<RawAnchor>,
1017    #[fromplist(alt_name = "attr")]
1018    attributes: LayerAttributes,
1019    #[fromplist(ignore)]
1020    other_stuff: BTreeMap<String, Plist>,
1021}
1022
1023impl RawLayer {
1024    /// Return true if the layer is a draft that is not meant to be compiled.
1025    ///
1026    /// The presence of an associated master indicates this is not a simple 'master' instance.
1027    /// Without 'attributes' that specify whether it's a special intermediate, alternate or
1028    /// color layer, we can assume the non-master layer is a draft.
1029    fn is_draft(&self) -> bool {
1030        self.associated_master_id.is_some() && self.attributes == Default::default()
1031    }
1032
1033    /// Glyphs uses the concept of 'bracket layers' to represent GSUB feature variations.
1034    ///
1035    /// See <https://glyphsapp.com/learn/switching-shapes#g-1-alternate-layers-bracket-layers>
1036    /// for more information.
1037    fn is_bracket_layer(&self, format_version: FormatVersion) -> bool {
1038        // can't be a bracket without an associated_master_id
1039        //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d57/Lib/glyphsLib/builder/builders.py#L270
1040        self.associated_master_id.is_some()
1041            && self.associated_master_id.as_ref() != Some(&self.layer_id)
1042            //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d5/Lib/glyphsLib/classes.py#L3942
1043            && match format_version {
1044                FormatVersion::V2 => AxisRule::from_layer_name(&self.name).is_some(),
1045                FormatVersion::V3 => !self.attributes.axis_rules.is_empty(),
1046            }
1047    }
1048
1049    fn v2_to_v3_attributes(&mut self) {
1050        // In Glyphs v2, 'brace' or intermediate layer coordinates are stored in the
1051        // layer name as comma-separated values inside braces
1052        let mut brace_coordinates = Vec::new();
1053        if let (Some(start), Some(end)) = (self.name.find('{'), self.name.find('}')) {
1054            let mut tokenizer = Tokenizer::new(&self.name[start..=end]);
1055            // we don't want this to fail, technically '{foobar}' is valid inside a
1056            // layer name which is not meant to specify intermediate coordinates.
1057            // Typos are also possible. Perhaps we should warn?
1058            brace_coordinates = tokenizer
1059                .parse_delimited_vec(VecDelimiters::CSV_IN_BRACES)
1060                .unwrap_or_default();
1061        }
1062        if !brace_coordinates.is_empty() {
1063            self.attributes.coordinates = brace_coordinates;
1064        }
1065        // TODO: handle 'bracket' layers and other attributes
1066    }
1067}
1068
1069/// Represents a path OR a component
1070///
1071/// <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#differences-between-version-2>
1072#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1073struct RawShape {
1074    // TODO: add numerous unsupported attributes
1075
1076    // When I'm a path
1077    closed: Option<bool>,
1078    nodes: Vec<Node>,
1079
1080    // When I'm a component I specifically want all my attributes to end up in other_stuff
1081    // My Component'ness can be detected by presence of a ref (Glyphs3) or name(Glyphs2) attribute
1082    // ref is reserved so take advantage of alt names
1083    #[fromplist(alt_name = "ref", alt_name = "name")]
1084    glyph_name: Option<SmolStr>,
1085
1086    // for components, an optional name to rename an anchor
1087    // on the target glyph during anchor propagation
1088    anchor: Option<SmolStr>,
1089    transform: Option<String>, // v2
1090    pos: Vec<f64>,             // v3
1091    angle: Option<f64>,        // v3
1092    scale: Vec<f64>,           // v3
1093
1094    #[fromplist(alt_name = "attr")]
1095    attributes: ShapeAttributes,
1096}
1097
1098/// <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#spec-glyphs-3-path>
1099#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
1100pub struct Path {
1101    pub closed: bool,
1102    pub nodes: Vec<Node>,
1103    pub attributes: ShapeAttributes,
1104}
1105
1106/// <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#spec-glyphs-3-component>
1107#[derive(Default, Clone, Debug, FromPlist)]
1108pub struct Component {
1109    /// The glyph this component references
1110    pub name: SmolStr,
1111    /// meh
1112    pub transform: Affine,
1113    /// An alternative anchor name used during anchor propagation
1114    ///
1115    /// For instance, if an acute accent is a component of a ligature glyph,
1116    /// we might rename its 'top' anchor to 'top_2'
1117    pub anchor: Option<SmolStr>,
1118    pub attributes: ShapeAttributes,
1119}
1120
1121impl PartialEq for Component {
1122    fn eq(&self, other: &Self) -> bool {
1123        self.name == other.name
1124            && Into::<AffineForEqAndHash>::into(self.transform) == other.transform.into()
1125    }
1126}
1127
1128impl Eq for Component {}
1129
1130impl Hash for Component {
1131    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1132        self.name.hash(state);
1133        Into::<AffineForEqAndHash>::into(self.transform).hash(state);
1134    }
1135}
1136
1137#[derive(Clone, Debug)]
1138pub struct Node {
1139    pub pt: Point,
1140    pub node_type: NodeType,
1141}
1142
1143impl Node {
1144    pub fn is_on_curve(&self) -> bool {
1145        !matches!(self.node_type, NodeType::OffCurve)
1146    }
1147}
1148
1149impl PartialEq for Node {
1150    fn eq(&self, other: &Self) -> bool {
1151        Into::<PointForEqAndHash>::into(self.pt) == other.pt.into()
1152            && self.node_type == other.node_type
1153    }
1154}
1155
1156impl Eq for Node {}
1157
1158impl Hash for Node {
1159    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1160        PointForEqAndHash::new(self.pt).hash(state);
1161        self.node_type.hash(state);
1162    }
1163}
1164
1165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1166pub enum NodeType {
1167    Line,
1168    LineSmooth,
1169    OffCurve,
1170    Curve,
1171    CurveSmooth,
1172    QCurve,
1173    QCurveSmooth,
1174}
1175
1176#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1177struct RawAnchor {
1178    name: SmolStr,
1179    pos: Option<Point>,       // v3
1180    position: Option<String>, // v2
1181}
1182
1183#[derive(Clone, Debug, PartialEq)]
1184pub struct Anchor {
1185    pub name: SmolStr,
1186    pub pos: Point,
1187}
1188
1189impl Anchor {
1190    pub(crate) fn is_origin(&self) -> bool {
1191        self.name == "*origin"
1192    }
1193
1194    pub(crate) fn origin_delta(&self) -> Option<Vec2> {
1195        self.is_origin().then_some(self.pos.to_vec2())
1196    }
1197}
1198
1199impl Hash for Anchor {
1200    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1201        self.name.hash(state);
1202        PointForEqAndHash::new(self.pos).hash(state);
1203    }
1204}
1205
1206#[derive(Clone, Debug, PartialEq, Hash)]
1207pub struct FontMaster {
1208    pub id: String,
1209    pub name: String,
1210    pub axes_values: Vec<OrderedFloat<f64>>,
1211    metric_values: BTreeMap<String, MetricValue>,
1212    pub number_values: BTreeMap<SmolStr, OrderedFloat<f64>>,
1213    pub custom_parameters: CustomParameters,
1214}
1215
1216impl FontMaster {
1217    fn read_metric(&self, metric_name: &str) -> Option<f64> {
1218        self.metric_values
1219            .get(metric_name)
1220            .map(|metric| metric.pos.into_inner())
1221    }
1222
1223    pub fn ascender(&self) -> Option<f64> {
1224        self.read_metric("ascender")
1225    }
1226
1227    pub fn descender(&self) -> Option<f64> {
1228        self.read_metric("descender")
1229    }
1230
1231    pub fn x_height(&self) -> Option<f64> {
1232        self.read_metric("x-height")
1233    }
1234
1235    pub fn cap_height(&self) -> Option<f64> {
1236        self.read_metric("cap height")
1237    }
1238
1239    pub fn italic_angle(&self) -> Option<f64> {
1240        self.read_metric("italic angle")
1241    }
1242}
1243
1244#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
1245struct RawFontMaster {
1246    id: String,
1247    name: Option<String>,
1248
1249    weight: Option<String>,
1250    width: Option<String>,
1251    custom: Option<String>,
1252
1253    weight_value: Option<OrderedFloat<f64>>,
1254    interpolation_weight: Option<OrderedFloat<f64>>,
1255
1256    width_value: Option<OrderedFloat<f64>>,
1257    interpolation_width: Option<OrderedFloat<f64>>,
1258
1259    custom_value: Option<OrderedFloat<f64>>,
1260
1261    typo_ascender: Option<i64>,
1262    typo_descender: Option<OrderedFloat<f64>>,
1263    typo_line_gap: Option<OrderedFloat<f64>>,
1264    win_ascender: Option<OrderedFloat<f64>>,
1265    win_descender: Option<OrderedFloat<f64>>,
1266
1267    axes_values: Vec<OrderedFloat<f64>>,
1268    metric_values: Vec<RawMetricValue>, // v3
1269
1270    ascender: Option<OrderedFloat<f64>>,   // v2
1271    baseline: Option<OrderedFloat<f64>>,   // v2
1272    descender: Option<OrderedFloat<f64>>,  // v2
1273    cap_height: Option<OrderedFloat<f64>>, // v2
1274    x_height: Option<OrderedFloat<f64>>,   // v2
1275    #[fromplist(alt_name = "italic angle")]
1276    italic_angle: Option<OrderedFloat<f64>>, // v2
1277
1278    alignment_zones: Vec<String>, // v2
1279
1280    custom_parameters: RawCustomParameters,
1281    number_values: Vec<OrderedFloat<f64>>,
1282
1283    #[fromplist(ignore)]
1284    other_stuff: BTreeMap<String, Plist>,
1285}
1286
1287#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
1288struct RawMetricValue {
1289    pos: Option<OrderedFloat<f64>>,
1290    over: Option<OrderedFloat<f64>>,
1291}
1292
1293#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
1294pub struct MetricValue {
1295    pos: OrderedFloat<f64>,
1296    over: OrderedFloat<f64>,
1297}
1298
1299impl From<RawMetricValue> for MetricValue {
1300    fn from(src: RawMetricValue) -> MetricValue {
1301        MetricValue {
1302            pos: src.pos.unwrap_or_default(),
1303            over: src.over.unwrap_or_default(),
1304        }
1305    }
1306}
1307
1308#[derive(Clone, Debug, PartialEq, Hash)]
1309pub struct Instance {
1310    pub name: String,
1311    pub active: bool,
1312    // So named to let FromPlist populate it from a field called "type"
1313    pub type_: InstanceType,
1314    pub axis_mappings: BTreeMap<String, AxisUserToDesignMap>,
1315    pub axes_values: Vec<OrderedFloat<f64>>,
1316    pub custom_parameters: CustomParameters,
1317    properties: Vec<RawName>, // used for name resolution
1318}
1319
1320/// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/classes.py#L150>
1321#[derive(Clone, Debug, PartialEq, Hash)]
1322pub enum InstanceType {
1323    Single,
1324    Variable,
1325}
1326
1327impl From<&str> for InstanceType {
1328    fn from(value: &str) -> Self {
1329        if value.eq_ignore_ascii_case("variable") {
1330            InstanceType::Variable
1331        } else {
1332            InstanceType::Single
1333        }
1334    }
1335}
1336
1337#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
1338struct RawInstance {
1339    name: String,
1340    exports: Option<i64>,
1341    active: Option<i64>,
1342    type_: Option<String>,
1343    axes_values: Vec<OrderedFloat<f64>>,
1344
1345    weight_value: Option<OrderedFloat<f64>>,
1346    interpolation_weight: Option<OrderedFloat<f64>>,
1347
1348    width_value: Option<OrderedFloat<f64>>,
1349    interpolation_width: Option<OrderedFloat<f64>>,
1350
1351    custom_value: Option<OrderedFloat<f64>>,
1352
1353    weight_class: Option<String>,
1354    width_class: Option<String>,
1355    properties: Vec<RawName>,
1356    custom_parameters: RawCustomParameters,
1357}
1358
1359impl RawInstance {
1360    /// Per glyphsLib both "exports=0" and "active=0" mean inactive
1361    /// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L637>
1362    fn is_active(&self) -> bool {
1363        self.exports.unwrap_or(1) != 0 && self.active.unwrap_or(1) != 0
1364    }
1365}
1366
1367trait GlyphsV2OrderedAxes {
1368    fn weight_value(&self) -> Option<OrderedFloat<f64>>;
1369    fn interpolation_weight(&self) -> Option<OrderedFloat<f64>>;
1370    fn width_value(&self) -> Option<OrderedFloat<f64>>;
1371    fn interpolation_width(&self) -> Option<OrderedFloat<f64>>;
1372    fn custom_value(&self) -> Option<OrderedFloat<f64>>;
1373
1374    fn value_for_nth_axis(&self, nth_axis: usize) -> Result<OrderedFloat<f64>, Error> {
1375        // Per https://github.com/googlefonts/fontmake-rs/pull/42#pullrequestreview-1211619812
1376        // the field to use is based on the order in axes NOT the tag.
1377        // That is, whatever the first axis is, it's value is in the weightValue field. Long sigh.
1378        // Defaults per https://github.com/googlefonts/fontmake-rs/pull/42#discussion_r1044415236.
1379        // v2 instances use novel field names so send back several for linear probing.
1380        Ok(match nth_axis {
1381            0 => self
1382                .weight_value()
1383                .or(self.interpolation_weight())
1384                .unwrap_or(100.0.into()),
1385            1 => self
1386                .width_value()
1387                .or(self.interpolation_width())
1388                .unwrap_or(100.0.into()),
1389            2 => self.custom_value().unwrap_or(0.0.into()),
1390            _ => {
1391                return Err(Error::StructuralError(format!(
1392                    "We don't know what field to use for axis {nth_axis}"
1393                )))
1394            }
1395        })
1396    }
1397
1398    fn axis_values(&self, axes: &[Axis]) -> Result<Vec<OrderedFloat<f64>>, Error> {
1399        (0..axes.len())
1400            .map(|nth_axis| self.value_for_nth_axis(nth_axis))
1401            .collect::<Result<Vec<OrderedFloat<f64>>, Error>>()
1402    }
1403}
1404
1405impl GlyphsV2OrderedAxes for RawFontMaster {
1406    fn weight_value(&self) -> Option<OrderedFloat<f64>> {
1407        self.weight_value
1408    }
1409
1410    fn interpolation_weight(&self) -> Option<OrderedFloat<f64>> {
1411        self.interpolation_weight
1412    }
1413
1414    fn width_value(&self) -> Option<OrderedFloat<f64>> {
1415        self.width_value
1416    }
1417
1418    fn interpolation_width(&self) -> Option<OrderedFloat<f64>> {
1419        self.interpolation_width
1420    }
1421
1422    fn custom_value(&self) -> Option<OrderedFloat<f64>> {
1423        self.custom_value
1424    }
1425}
1426
1427impl GlyphsV2OrderedAxes for RawInstance {
1428    fn weight_value(&self) -> Option<OrderedFloat<f64>> {
1429        self.weight_value
1430    }
1431
1432    fn interpolation_weight(&self) -> Option<OrderedFloat<f64>> {
1433        self.interpolation_weight
1434    }
1435
1436    fn width_value(&self) -> Option<OrderedFloat<f64>> {
1437        self.width_value
1438    }
1439
1440    fn interpolation_width(&self) -> Option<OrderedFloat<f64>> {
1441        self.interpolation_width
1442    }
1443
1444    fn custom_value(&self) -> Option<OrderedFloat<f64>> {
1445        self.custom_value
1446    }
1447}
1448
1449fn parse_node_from_string(value: &str) -> Node {
1450    let mut spl = value.splitn(3, ' ');
1451    let x = spl.next().unwrap().parse().unwrap();
1452    let y = spl.next().unwrap().parse().unwrap();
1453    let pt = Point::new(x, y);
1454    let mut raw_node_type = spl.next().unwrap();
1455    // drop the userData dict, we don't use it for compilation
1456    if raw_node_type.contains('{') {
1457        raw_node_type = raw_node_type.split('{').next().unwrap().trim_end();
1458    }
1459    let node_type = raw_node_type.parse().unwrap();
1460    Node { pt, node_type }
1461}
1462
1463fn parse_node_from_tokenizer(tokenizer: &mut Tokenizer<'_>) -> Result<Node, crate::plist::Error> {
1464    // (x,y,type)
1465    let x: f64 = tokenizer.parse()?;
1466    tokenizer.eat(b',')?;
1467    let y: f64 = tokenizer.parse()?;
1468    tokenizer.eat(b',')?;
1469    let node_type: String = tokenizer.parse()?;
1470    let node_type = NodeType::from_str(&node_type)
1471        .map_err(|_| crate::plist::Error::Parse(format!("unknown node type '{node_type}'")))?;
1472
1473    // Sometimes there is userData; ignore it
1474    if tokenizer.eat(b',').is_ok() {
1475        tokenizer.skip_rec()?;
1476    }
1477
1478    Ok(Node {
1479        pt: Point { x, y },
1480        node_type,
1481    })
1482}
1483
1484impl std::str::FromStr for NodeType {
1485    type Err = String;
1486    fn from_str(s: &str) -> Result<Self, Self::Err> {
1487        match s {
1488            // Glyphs 2 style
1489            "LINE" => Ok(NodeType::Line),
1490            "LINE SMOOTH" => Ok(NodeType::LineSmooth),
1491            "OFFCURVE" => Ok(NodeType::OffCurve),
1492            "CURVE" => Ok(NodeType::Curve),
1493            "CURVE SMOOTH" => Ok(NodeType::CurveSmooth),
1494            "QCURVE" => Ok(NodeType::QCurve),
1495            "QCURVE SMOOTH" => Ok(NodeType::QCurveSmooth),
1496            // Glyphs 3 style
1497            "l" => Ok(NodeType::Line),
1498            "ls" => Ok(NodeType::LineSmooth),
1499            "o" => Ok(NodeType::OffCurve),
1500            "c" => Ok(NodeType::Curve),
1501            "cs" => Ok(NodeType::CurveSmooth),
1502            "q" => Ok(NodeType::QCurve),
1503            "qs" => Ok(NodeType::QCurveSmooth),
1504            _ => Err(format!("unknown node type {s}")),
1505        }
1506    }
1507}
1508
1509// Hand-parse Node because it doesn't follow the normal structure
1510impl FromPlist for Node {
1511    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
1512        use crate::plist::Error;
1513        let tok = tokenizer.lex()?;
1514        let node = match &tok {
1515            Token::Atom(value) => parse_node_from_string(value),
1516            Token::String(value) => parse_node_from_string(value),
1517            Token::OpenParen => {
1518                let node = parse_node_from_tokenizer(tokenizer)?;
1519                tokenizer.eat(b')')?;
1520                node
1521            }
1522            _ => return Err(Error::ExpectedString),
1523        };
1524        Ok(node)
1525    }
1526}
1527
1528impl Path {
1529    pub fn new(closed: bool) -> Path {
1530        Path {
1531            nodes: Vec::new(),
1532            closed,
1533            ..Default::default()
1534        }
1535    }
1536
1537    pub fn add(&mut self, pt: impl Into<Point>, node_type: NodeType) {
1538        let pt = pt.into();
1539        self.nodes.push(Node { pt, node_type });
1540    }
1541
1542    /// Rotate left by one, placing the first point at the end. This is because
1543    /// it's what glyphs seems to expect.
1544    pub fn rotate_left(&mut self, delta: usize) {
1545        self.nodes.rotate_left(delta);
1546    }
1547
1548    pub fn reverse(&mut self) {
1549        self.nodes.reverse();
1550    }
1551}
1552
1553fn v2_to_v3_name(v2_prop: Option<&str>, v3_name: &str) -> Option<RawName> {
1554    // https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#properties
1555    // Keys ending with "s" are localizable that means the second key is values
1556    v2_prop.map(|value| {
1557        if v3_name.ends_with('s') {
1558            RawName {
1559                key: v3_name.into(),
1560                value: None,
1561                values: vec![RawNameValue {
1562                    language: "dflt".into(),
1563                    value: value.to_string(),
1564                }],
1565            }
1566        } else {
1567            RawName {
1568                key: v3_name.into(),
1569                value: Some(value.to_string()),
1570                values: vec![],
1571            }
1572        }
1573    })
1574}
1575
1576impl RawFont {
1577    pub fn load_from_string(raw_content: &str) -> Result<Self, crate::plist::Error> {
1578        let raw_content = preprocess_unparsed_plist(raw_content);
1579        Self::parse_plist(&raw_content)
1580    }
1581
1582    pub fn load(glyphs_file: &path::Path) -> Result<Self, Error> {
1583        if glyphs_file.extension() == Some(OsStr::new("glyphspackage")) {
1584            return Self::load_package(glyphs_file);
1585        }
1586
1587        debug!("Read glyphs {glyphs_file:?}");
1588        let raw_content = fs::read_to_string(glyphs_file).map_err(Error::IoError)?;
1589        Self::load_from_string(&raw_content)
1590            .map_err(|e| Error::ParseError(glyphs_file.to_path_buf(), e.to_string()))
1591    }
1592
1593    /// load from a .glyphspackage
1594    fn load_package(glyphs_package: &path::Path) -> Result<RawFont, Error> {
1595        if !glyphs_package.is_dir() {
1596            return Err(Error::NotAGlyphsPackage(glyphs_package.to_path_buf()));
1597        }
1598        debug!("Read glyphs package {glyphs_package:?}");
1599
1600        let fontinfo_file = glyphs_package.join("fontinfo.plist");
1601        let fontinfo_data = fs::read_to_string(&fontinfo_file).map_err(Error::IoError)?;
1602        let mut raw_font = RawFont::parse_plist(&fontinfo_data)
1603            .map_err(|e| Error::ParseError(fontinfo_file.to_path_buf(), format!("{e}")))?;
1604
1605        let mut glyphs: HashMap<SmolStr, RawGlyph> = HashMap::new();
1606        let glyphs_dir = glyphs_package.join("glyphs");
1607        if glyphs_dir.is_dir() {
1608            for entry in fs::read_dir(glyphs_dir).map_err(Error::IoError)? {
1609                let entry = entry.map_err(Error::IoError)?;
1610                let path = entry.path();
1611                if path.extension() == Some(OsStr::new("glyph")) {
1612                    let glyph_data = fs::read_to_string(&path).map_err(Error::IoError)?;
1613                    let glyph_data = preprocess_unparsed_plist(&glyph_data);
1614                    let glyph = RawGlyph::parse_plist(&glyph_data)
1615                        .map_err(|e| Error::ParseError(path.clone(), e.to_string()))?;
1616                    if glyph.glyphname.is_empty() {
1617                        return Err(Error::ParseError(
1618                            path.clone(),
1619                            "Glyph dict must have a 'glyphname' key".to_string(),
1620                        ));
1621                    }
1622                    glyphs.insert(glyph.glyphname.clone(), glyph);
1623                }
1624            }
1625        }
1626
1627        // if order.plist file exists, read it and sort glyphs in it accordingly
1628        let order_file = glyphs_package.join("order.plist");
1629        let mut ordered_glyphs = Vec::new();
1630        if order_file.exists() {
1631            let order_data = fs::read_to_string(&order_file).map_err(Error::IoError)?;
1632            let order_plist = Plist::parse(&order_data)
1633                .map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
1634            let order = order_plist
1635                .expect_array()
1636                .map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
1637            for glyph_name in order {
1638                let glyph_name = glyph_name
1639                    .expect_string()
1640                    .map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
1641                if let Some(glyph) = glyphs.remove(glyph_name.as_str()) {
1642                    ordered_glyphs.push(glyph);
1643                }
1644            }
1645        }
1646        // sort the glyphs not in order.plist by their name
1647        let mut glyph_names: Vec<_> = glyphs.keys().cloned().collect();
1648        glyph_names.sort();
1649        ordered_glyphs.extend(
1650            glyph_names
1651                .into_iter()
1652                .map(|glyph_name| glyphs.remove(&glyph_name).unwrap()),
1653        );
1654        assert!(glyphs.is_empty());
1655        raw_font.glyphs = ordered_glyphs;
1656
1657        // ignore UIState.plist which stuff like displayStrings that are not used by us
1658
1659        Ok(raw_font)
1660    }
1661
1662    fn v2_to_v3_axes(&mut self) -> Result<Vec<String>, Error> {
1663        let mut tags = Vec::new();
1664        if let Some(v2_axes) = self.custom_parameters.axes() {
1665            for v2_axis in v2_axes {
1666                tags.push(v2_axis.tag.clone());
1667                self.axes.push(v2_axis.clone());
1668            }
1669        }
1670
1671        // Match the defaults from https://github.com/googlefonts/glyphsLib/blob/f6e9c4a29ce764d34c309caef5118c48c156be36/Lib/glyphsLib/builder/axes.py#L526
1672        // if we have nothing
1673        if self.axes.is_empty() {
1674            self.axes.push(Axis {
1675                name: "Weight".into(),
1676                tag: "wght".into(),
1677                hidden: None,
1678            });
1679            self.axes.push(Axis {
1680                name: "Width".into(),
1681                tag: "wdth".into(),
1682                hidden: None,
1683            });
1684            self.axes.push(Axis {
1685                name: "Custom".into(),
1686                tag: "XXXX".into(),
1687                hidden: None,
1688            });
1689        }
1690
1691        if self.axes.len() > 3 {
1692            return Err(Error::StructuralError(
1693                "We only understand 0..3 axes for Glyphs v2".into(),
1694            ));
1695        }
1696
1697        // v2 stores values for axes in specific fields, find them and put them into place
1698        // "Axis position related properties (e.g. weightValue, widthValue, customValue) have been replaced by the axesValues list which is indexed in parallel with the toplevel axes list."
1699        for master in self.font_master.iter_mut() {
1700            master.axes_values = master.axis_values(&self.axes)?;
1701        }
1702        for instance in self.instances.iter_mut() {
1703            instance.axes_values = instance.axis_values(&self.axes)?;
1704        }
1705
1706        Ok(tags)
1707    }
1708
1709    fn v2_to_v3_metrics(&mut self) -> Result<(), Error> {
1710        // setup storage for the basic metrics
1711        self.metrics = V3_METRIC_NAMES
1712            .iter()
1713            .map(|n| RawMetric {
1714                type_: n.to_string(),
1715            })
1716            .collect();
1717
1718        let mut used_metrics = [false; 6];
1719        // first which metric occurs in any master:
1720        for m in &self.font_master {
1721            for (i, val) in [
1722                m.ascender,
1723                m.baseline,
1724                m.descender,
1725                m.cap_height,
1726                m.x_height,
1727                m.italic_angle,
1728            ]
1729            .iter()
1730            .enumerate()
1731            {
1732                used_metrics[i] |= val.is_some();
1733            }
1734        }
1735
1736        // add only used metrics to the metric list
1737        self.metrics = V3_METRIC_NAMES
1738            .into_iter()
1739            .zip(used_metrics)
1740            .filter(|(_, used)| *used)
1741            .map(|(name, _)| RawMetric {
1742                type_: name.to_string(),
1743            })
1744            .collect();
1745
1746        let mut non_metric_alignment_zones = vec![vec![]; self.font_master.len()];
1747
1748        // in each font master setup the parallel array
1749        for (i, master) in self.font_master.iter_mut().enumerate() {
1750            // each master has to have an entry for each defined metric,
1751            // even if it is 'None', and they need to be in the same order
1752            // as in the self.metrics list.
1753            let mut metric_values = vec![RawMetricValue::default(); self.metrics.len()];
1754            for (metric_idx, name) in self.metrics.iter().enumerate() {
1755                let value = match name.type_.as_ref() {
1756                    "ascender" => master.ascender,
1757                    "baseline" => master.baseline,
1758                    "descender" => master.descender,
1759                    "cap height" => master.cap_height,
1760                    "x-height" => master.x_height,
1761                    "italic angle" => master.italic_angle,
1762                    _ => unreachable!("only these values exist in V3_METRIC_NAMES"),
1763                };
1764                metric_values[metric_idx].pos = value;
1765            }
1766
1767            // "alignmentZones is now a set of over (overshoot) properties attached to metrics"
1768            for alignment_zone in &master.alignment_zones {
1769                let Some((pos, over)) = parse_alignment_zone(alignment_zone) else {
1770                    warn!("Confusing alignment zone '{alignment_zone}', skipping");
1771                    continue;
1772                };
1773
1774                // skip zero-height zones
1775                if over == 0. {
1776                    continue;
1777                }
1778
1779                // special handling for this; we assume it's the baseline, and
1780                // we know where that is in our vec (and it has pos: None, set
1781                // above)
1782                if pos == 0. {
1783                    if let Some((idx, _)) = self
1784                        .metrics
1785                        .iter()
1786                        .enumerate()
1787                        .find(|(_, name)| name.type_ == "baseline")
1788                    {
1789                        metric_values[idx].over = Some(over);
1790                    }
1791                    continue;
1792                }
1793
1794                // now look for a metric that has the same position as this zone
1795                // this is quadratic but N is small
1796                if let Some(metric) = metric_values.iter_mut().find(|x| x.pos == Some(pos)) {
1797                    metric.over = Some(over);
1798                } else {
1799                    non_metric_alignment_zones[i].push((pos, over))
1800                }
1801            }
1802            master.metric_values = metric_values;
1803        }
1804
1805        // now handle any non-metric alignment zones, converting them to metrics.
1806        // first we assign a name to each unique position:
1807        let mut new_metrics = HashMap::new();
1808        for pos in non_metric_alignment_zones
1809            .iter()
1810            .flat_map(|master| master.iter().map(|(pos, _)| *pos))
1811        {
1812            if !new_metrics.contains_key(&pos) {
1813                let next_zone = new_metrics.len() + 1;
1814                let idx = self.metrics.len();
1815                self.metrics.push(RawMetric {
1816                    type_: format!("zone {next_zone}"),
1817                });
1818                new_metrics.insert(pos, idx);
1819            }
1820        }
1821
1822        // flip our map, so it's ordered on index:
1823        let new_metrics: BTreeMap<_, _> = new_metrics.into_iter().map(|(k, v)| (v, k)).collect();
1824
1825        // then for each master, add a metric value for each newly named metric
1826        for (idx, metrics) in non_metric_alignment_zones.into_iter().enumerate() {
1827            for pos_to_add in new_metrics.values().copied() {
1828                let to_add = metrics.iter().copied().find_map(|(pos, over)| {
1829                    (pos == pos_to_add).then_some(RawMetricValue {
1830                        pos: Some(pos),
1831                        over: Some(over),
1832                    })
1833                });
1834
1835                self.font_master[idx]
1836                    .metric_values
1837                    .push(to_add.unwrap_or_default());
1838            }
1839        }
1840        Ok(())
1841    }
1842
1843    fn v2_to_v3_master_names(&mut self) -> Result<(), Error> {
1844        // in Glyphs 2, masters don't have a single 'name' attribute, but rather
1845        // a concatenation of three other optional attributes weirdly called
1846        // 'width', 'weight' and 'custom' (in exactly this order).
1847        // The first two can only contain few predefined values, the last one is
1848        // residual and free-form. They default to 'Regular' when omitted in
1849        // the source. See:
1850        // https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv2.md
1851        // https://github.com/googlefonts/glyphsLib/blob/9d5828d/Lib/glyphsLib/classes.py#L1700-L1711
1852        for master in self.font_master.iter_mut() {
1853            // Even though glyphs2 masters don't officially have a 'name' attribute,
1854            // some glyphs2 sources produced by more recent versions of Glyphs
1855            // sometimes have it (unclear exactly when or from which version on).
1856            // We keep the 'name' attribute as is, instead of generating a one.
1857            if master.name.is_some() {
1858                continue;
1859            }
1860
1861            // Remove Nones, empty strings and redundant occurrences of 'Regular'
1862            let mut names = [
1863                master.width.as_deref(),
1864                master.weight.as_deref(),
1865                master.custom.as_deref(),
1866            ]
1867            .iter()
1868            .flatten()
1869            .flat_map(|n| n.split_ascii_whitespace())
1870            .filter(|x| *x != "Regular")
1871            .collect::<Vec<_>>();
1872
1873            // append "Italic" if italic angle != 0
1874            if let Some(italic_angle) = master.italic_angle {
1875                if italic_angle != 0.0
1876                    && (names.is_empty()
1877                        || !names
1878                            .iter()
1879                            .any(|name| *name == "Italic" || *name == "Oblique"))
1880                {
1881                    names.push("Italic");
1882                }
1883            }
1884            // if all are empty, default to "Regular"
1885            master.name = if names.is_empty() {
1886                Some("Regular".into())
1887            } else {
1888                Some(names.join(" "))
1889            };
1890        }
1891        Ok(())
1892    }
1893
1894    fn v2_to_v3_names(&mut self) -> Result<(), Error> {
1895        // The copyright, designer, designerURL, manufacturer, manufacturerURL top-level entries
1896        // have been moved into new top-level properties dictionary and made localizable.
1897        // Take properties to avoid incompatible borrowing against self
1898        let mut properties = std::mem::take(&mut self.properties);
1899
1900        properties.extend(v2_to_v3_name(self.copyright.as_deref(), "copyrights"));
1901        properties.extend(v2_to_v3_name(self.designer.as_deref(), "designers"));
1902        properties.extend(v2_to_v3_name(self.designerURL.as_deref(), "designerURL"));
1903        properties.extend(v2_to_v3_name(self.manufacturer.as_deref(), "manufacturers"));
1904        properties.extend(v2_to_v3_name(
1905            self.manufacturerURL.as_deref(),
1906            "manufacturerURL",
1907        ));
1908
1909        // glyphsLib tries both long and short names, with short names taking precedence
1910        //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L258
1911        let mut v2_to_v3_param = |v2_names: &[&str], v3_name: &str| {
1912            if properties.iter().any(|n| n.key == v3_name && !n.is_empty()) {
1913                return;
1914            }
1915            for v2_name in v2_names {
1916                if let Some(value) = v2_to_v3_name(self.custom_parameters.string(v2_name), v3_name)
1917                {
1918                    properties.push(value);
1919                    return;
1920                }
1921            }
1922        };
1923
1924        v2_to_v3_param(&["description", "openTypeNameDescription"], "descriptions");
1925        v2_to_v3_param(&["licenseURL", "openTypeNameLicenseURL"], "licenseURL");
1926        v2_to_v3_param(&["versionString", "openTypeNameVersion"], "versionString");
1927        v2_to_v3_param(&["compatibleFullName"], "compatibleFullNames");
1928        v2_to_v3_param(&["license", "openTypeNameLicense"], "licenses");
1929        v2_to_v3_param(&["uniqueID", "openTypeNameUniqueID"], "uniqueID");
1930        v2_to_v3_param(&["trademark"], "trademarks");
1931        v2_to_v3_param(&["sampleText", "openTypeNameSampleText"], "sampleTexts");
1932        v2_to_v3_param(&["postscriptFullName"], "postscriptFullName");
1933        v2_to_v3_param(&["postscriptFontName"], "postscriptFontName");
1934        v2_to_v3_param(
1935            &["WWSFamilyName", "openTypeNameWWSFamilyName"],
1936            "WWSFamilyName",
1937        );
1938        v2_to_v3_param(&["vendorID", "openTypeOS2VendorID"], "vendorID");
1939
1940        self.properties = properties;
1941
1942        Ok(())
1943    }
1944
1945    fn v2_to_v3_instances(&mut self) -> Result<(), Error> {
1946        for instance in self.instances.iter_mut() {
1947            if let Some(custom_weight_class) = instance.custom_parameters.get("weightClass") {
1948                instance.weight_class = custom_weight_class.to_string().into();
1949            }
1950            // named clases become #s in v3
1951            for (tag, opt) in [
1952                ("wght", &mut instance.weight_class),
1953                ("wdth", &mut instance.width_class),
1954            ] {
1955                let Some(value) = opt.as_ref() else {
1956                    continue;
1957                };
1958                if f64::from_str(value).is_ok() {
1959                    continue;
1960                };
1961                let Some(value) = lookup_class_value(tag, value) else {
1962                    return Err(Error::UnknownValueName(value.clone()));
1963                };
1964                let _ = opt.insert(value.to_string());
1965            }
1966
1967            instance.properties.extend(v2_to_v3_name(
1968                instance.custom_parameters.string("postscriptFontName"),
1969                "postscriptFontName",
1970            ));
1971        }
1972
1973        Ok(())
1974    }
1975
1976    fn v2_to_v3_layer_attributes(&mut self) {
1977        for raw_glyph in self.glyphs.iter_mut() {
1978            for layer in raw_glyph.layers.iter_mut() {
1979                layer.v2_to_v3_attributes();
1980            }
1981        }
1982    }
1983
1984    /// `<See https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#differences-between-version-2>`
1985    fn v2_to_v3(&mut self) -> Result<(), Error> {
1986        self.v2_to_v3_master_names()?;
1987        self.v2_to_v3_axes()?;
1988        self.v2_to_v3_metrics()?;
1989        self.v2_to_v3_instances()?;
1990        self.v2_to_v3_names()?; // uses instances
1991        self.v2_to_v3_layer_attributes();
1992        Ok(())
1993    }
1994}
1995
1996// in the form '{INT, INT}'
1997fn parse_alignment_zone(zone: &str) -> Option<(OrderedFloat<f64>, OrderedFloat<f64>)> {
1998    let (one, two) = zone.split_once(',')?;
1999    let one = one.trim_start_matches(['{', ' ']).parse::<i32>().ok()?;
2000    let two = two.trim_start().trim_end_matches('}').parse::<i32>().ok()?;
2001    Some((OrderedFloat(one as f64), OrderedFloat(two as f64)))
2002}
2003
2004fn make_glyph_order(glyphs: &[RawGlyph], custom_order: Option<Vec<SmolStr>>) -> Vec<SmolStr> {
2005    let mut valid_names: HashSet<_> = glyphs.iter().map(|g| &g.glyphname).collect();
2006    let mut glyph_order = Vec::new();
2007
2008    // Add all valid glyphOrder entries in order
2009    // See https://github.com/googlefonts/fontmake-rs/pull/43/files#r1044627972
2010    for name in custom_order.into_iter().flatten() {
2011        if valid_names.remove(&name) {
2012            glyph_order.push(name.clone());
2013        }
2014    }
2015
2016    // Add anything left over in file order
2017    glyph_order.extend(
2018        glyphs
2019            .iter()
2020            .filter(|g| valid_names.contains(&g.glyphname))
2021            .map(|g| g.glyphname.clone()),
2022    );
2023
2024    glyph_order
2025}
2026
2027// glyphs2 uses hex, glyphs3 uses base10
2028fn parse_codepoint_str(s: &str, radix: u32) -> BTreeSet<u32> {
2029    s.split(',')
2030        .map(|cp| u32::from_str_radix(cp, radix).unwrap())
2031        .collect()
2032}
2033
2034/// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L578>
2035fn default_master_idx(raw_font: &RawFont) -> usize {
2036    // Prefer an explicit origin
2037    // https://github.com/googlefonts/fontmake-rs/issues/44
2038    if let Some(master_idx) = raw_font
2039        .custom_parameters
2040        .string("Variable Font Origin")
2041        .and_then(|origin| {
2042            raw_font
2043                .font_master
2044                .iter()
2045                .position(|master| master.id == origin)
2046        })
2047    {
2048        return master_idx;
2049    }
2050
2051    // No explicit origin, try to pick a winner
2052
2053    // Contenders: (ordinal, words in name) for all masters that have names
2054    let contenders = raw_font
2055        .font_master
2056        .iter()
2057        .enumerate()
2058        .filter_map(|(i, m)| {
2059            m.name
2060                .as_deref()
2061                .map(|name| (i, whitespace_separated_tokens(name)))
2062        })
2063        .collect::<Vec<_>>();
2064
2065    // EARLY EXIT: no contenders, just pick 0
2066    if contenders.is_empty() {
2067        return 0;
2068    }
2069
2070    // In Python find_base_style <https://github.com/googlefonts/glyphsLib/blob/9d5828d874110c42dfc5f542db8eb84f88641eb5/Lib/glyphsLib/builder/axes.py#L652-L663>
2071    let mut common_words = contenders[0].1.clone();
2072    for (_, words) in contenders.iter().skip(1) {
2073        common_words.retain(|w| words.contains(w));
2074    }
2075
2076    // Find the best match:
2077    //   Find the common words in the master names
2078    //   If any master is named exactly that, it wins
2079    //      "Foo Bar" is the best match for {Foo Bar Donkey, Foo Bar Cat, Foo Bar}
2080    //   Otherwise, a master whose name matches the common words if we delete "Regular" wins
2081    //      "Foo Bar Regular" is the best match for {Foo Bar Italic, Foo Bar Majestic, Foo Bar Regular}
2082    let mut best_idx = 0;
2083    for (idx, mut words) in contenders {
2084        // if name exactly matches common words you just win
2085        if *common_words == words {
2086            best_idx = idx;
2087            break;
2088        }
2089
2090        // if our words excluding "Regular" match we're the best
2091        // a subsequent contender could match exactly so we don't win yet
2092        words.retain(|w| *w != "Regular");
2093        if *common_words == words {
2094            best_idx = idx;
2095        }
2096    }
2097    best_idx
2098}
2099
2100fn whitespace_separated_tokens(s: &str) -> Vec<&str> {
2101    s.split_whitespace().collect()
2102}
2103
2104fn axis_index(axes: &[Axis], pred: impl Fn(&Axis) -> bool) -> Option<usize> {
2105    axes.iter()
2106        .enumerate()
2107        .find_map(|(i, a)| if pred(a) { Some(i) } else { None })
2108}
2109
2110fn user_to_design_from_axis_mapping(
2111    from: &RawFont,
2112) -> Option<BTreeMap<String, AxisUserToDesignMap>> {
2113    let mappings = from.custom_parameters.axis_mappings()?;
2114    let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
2115    for mapping in mappings {
2116        let Some(axis_index) = axis_index(&from.axes, |a| a.tag == mapping.tag) else {
2117            log::warn!(
2118                "axis mapping includes tag {:?} not included in font",
2119                mapping.tag
2120            );
2121            continue;
2122        };
2123        let axis_name = &from.axes.get(axis_index).unwrap().name;
2124        for (user, design) in mapping.user_to_design.iter() {
2125            axis_mappings
2126                .entry(axis_name.clone())
2127                .or_default()
2128                .add_if_new(*user, *design);
2129        }
2130    }
2131    Some(axis_mappings)
2132}
2133
2134fn user_to_design_from_axis_location(
2135    from: &RawFont,
2136) -> Option<BTreeMap<String, AxisUserToDesignMap>> {
2137    // glyphsLib only trusts Axis Location when all masters have it, match that
2138    // https://github.com/googlefonts/fontmake-rs/pull/83#discussion_r1065814670
2139    let master_locations: Vec<_> = from
2140        .font_master
2141        .iter()
2142        .filter_map(|m| m.custom_parameters.axis_locations())
2143        .collect();
2144    if master_locations.len() != from.font_master.len() {
2145        if !master_locations.is_empty() {
2146            warn!(
2147                "{}/{} masters have Axis Location; ignoring",
2148                master_locations.len(),
2149                from.font_master.len()
2150            );
2151        }
2152        return None;
2153    }
2154
2155    let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
2156    for (master, axis_locations) in from.font_master.iter().zip(master_locations) {
2157        for axis_location in axis_locations {
2158            let Some(axis_index) = axis_index(&from.axes, |a| a.name == axis_location.axis_name)
2159            else {
2160                panic!("Axis has no index {axis_location:?}");
2161            };
2162            let user = axis_location.location;
2163            let design = master.axes_values[axis_index];
2164
2165            axis_mappings
2166                .entry(axis_location.axis_name.clone())
2167                .or_default()
2168                .add_if_new(user, design);
2169        }
2170    }
2171    Some(axis_mappings)
2172}
2173
2174impl AxisUserToDesignMap {
2175    fn add_any_new(&mut self, incoming: &AxisUserToDesignMap) {
2176        for (user, design) in incoming.0.iter() {
2177            self.add_if_new(*user, *design);
2178        }
2179    }
2180
2181    fn add_if_new(&mut self, user: OrderedFloat<f64>, design: OrderedFloat<f64>) {
2182        // only keys (input/user-space) should be unique, values (output/design-space) can be duplicated
2183        if self.0.iter().any(|(u, _)| *u == user) {
2184            return;
2185        }
2186        self.0.push((user, design));
2187    }
2188
2189    fn add_identity_map(&mut self, value: OrderedFloat<f64>) {
2190        // no duplicate keys nor values allowed
2191        if self.0.iter().any(|(u, d)| *u == value || *d == value) {
2192            return;
2193        }
2194        self.0.push((value, value));
2195    }
2196
2197    pub fn iter(&self) -> impl Iterator<Item = &(OrderedFloat<f64>, OrderedFloat<f64>)> {
2198        self.0.iter()
2199    }
2200
2201    pub fn is_identity(&self) -> bool {
2202        self.0.iter().all(|(u, d)| u == d)
2203    }
2204}
2205
2206impl UserToDesignMapping {
2207    /// From most to least preferred: Axis Mappings, Axis Location, mappings from instances, assume user == design
2208    ///
2209    /// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L155>
2210    fn new(from: &RawFont, instances: &[Instance]) -> Self {
2211        let from_axis_mapping = user_to_design_from_axis_mapping(from);
2212        let from_axis_location = user_to_design_from_axis_location(from);
2213        let (result, incomplete_mapping) = match (from_axis_mapping, from_axis_location) {
2214            (Some(from_mapping), Some(..)) => {
2215                warn!("Axis Mapping *and* Axis Location are defined; using Axis Mapping");
2216                (from_mapping, false)
2217            }
2218            (Some(from_mapping), None) => (from_mapping, false),
2219            (None, Some(from_location)) => (from_location, true),
2220            (None, None) => (BTreeMap::new(), true),
2221        };
2222        let mut result = Self(result);
2223        if incomplete_mapping {
2224            //https://github.com/googlefonts/glyphsLib/blob/682ff4b17/Lib/glyphsLib/builder/axes.py#L251
2225            result.add_instance_mappings_if_new(instances);
2226            if result.0.is_empty() || result.0.values().all(|v| v.is_identity()) {
2227                result.add_master_mappings_if_new(from);
2228            }
2229        }
2230        result
2231    }
2232
2233    pub fn contains(&self, axis_name: &str) -> bool {
2234        self.0.contains_key(axis_name)
2235    }
2236
2237    pub fn get(&self, axis_name: &str) -> Option<&AxisUserToDesignMap> {
2238        self.0.get(axis_name)
2239    }
2240
2241    /// * <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L128>
2242    /// * <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L353>
2243    fn add_instance_mappings_if_new(&mut self, instances: &[Instance]) {
2244        for instance in instances
2245            .iter()
2246            .filter(|i| i.active && i.type_ == InstanceType::Single)
2247        {
2248            for (axis_name, inst_mapping) in instance.axis_mappings.iter() {
2249                self.0
2250                    .entry(axis_name.clone())
2251                    .or_default()
2252                    .add_any_new(inst_mapping);
2253            }
2254        }
2255    }
2256
2257    fn add_master_mappings_if_new(&mut self, from: &RawFont) {
2258        for master in from.font_master.iter() {
2259            for (axis, value) in from.axes.iter().zip(&master.axes_values) {
2260                self.0
2261                    .entry(axis.name.clone())
2262                    .or_default()
2263                    .add_identity_map(*value);
2264            }
2265        }
2266    }
2267}
2268
2269impl TryFrom<RawShape> for Shape {
2270    type Error = Error;
2271
2272    fn try_from(from: RawShape) -> Result<Self, Self::Error> {
2273        // TODO: handle numerous unsupported attributes
2274        // See <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#differences-between-version-2>
2275
2276        let shape = if let Some(glyph_name) = from.glyph_name {
2277            assert!(!glyph_name.is_empty(), "A pointless component");
2278
2279            // V3 vs v2: The transform entry has been replaced by angle, pos and scale entries.
2280            let mut transform = if let Some(transform) = from.transform {
2281                Affine::parse_plist(&transform)?
2282            } else {
2283                Affine::IDENTITY
2284            };
2285
2286            // Glyphs 3 gives us {angle, pos, scale}. Glyphs 2 gives us the standard 2x3 matrix.
2287            // The matrix is more general and less ambiguous (what order do you apply the angle, pos, scale?)
2288            // so convert Glyphs 3 to that. Order based on saving the same transformed comonent as
2289            // Glyphs 2 and Glyphs 3 then trying to convert one to the other.
2290            if !from.pos.is_empty() {
2291                if from.pos.len() != 2 {
2292                    return Err(Error::StructuralError(format!("Bad pos: {:?}", from.pos)));
2293                }
2294                transform *= Affine::translate((from.pos[0], from.pos[1]));
2295            }
2296            if let Some(angle) = from.angle {
2297                transform *= normalized_rotation(angle);
2298            }
2299            if !from.scale.is_empty() {
2300                if from.scale.len() != 2 {
2301                    return Err(Error::StructuralError(format!(
2302                        "Bad scale: {:?}",
2303                        from.scale
2304                    )));
2305                }
2306                transform *= Affine::scale_non_uniform(from.scale[0], from.scale[1]);
2307            }
2308
2309            Shape::Component(Component {
2310                name: glyph_name,
2311                transform,
2312                anchor: from.anchor,
2313                attributes: from.attributes,
2314            })
2315        } else {
2316            // no ref; presume it's a path
2317            Shape::Path(Path {
2318                closed: from.closed.unwrap_or_default(),
2319                nodes: from.nodes.clone(),
2320                attributes: from.attributes,
2321            })
2322        };
2323        Ok(shape)
2324    }
2325}
2326
2327/// Return [kurbo::Affine] rotation around angle (in degrees) with normalized sin/cos.
2328///
2329/// This ensures that for the four "cardinal" rotations (0, 90, 180, 270), the values
2330/// of sin(angle) and cos(angle) are rounded to *exactly* 0.0, +1.0 or -1.0, thus avoiding
2331/// very small values (e.g. 1.2246467991473532e-16) whose effect may be amplified as some
2332/// transformed point coordinates end up close to the 0.5 threshold before being rounded
2333/// to integer later in the build.
2334/// It matches the output of the fontTools' Transform.rotate() used by glyphsLib.
2335/// <https://github.com/fonttools/fonttools/blob/b7509b2/Lib/fontTools/misc/transform.py#L246-L258>
2336fn normalized_rotation(angle_deg: f64) -> Affine {
2337    const ROT_90: Affine = Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]);
2338    const ROT_180: Affine = Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
2339    const ROT_270: Affine = Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]);
2340    // Normalize angle to [0, 360)
2341    let normalized_angle = angle_deg.rem_euclid(360.0);
2342
2343    match normalized_angle {
2344        0.0 => Affine::IDENTITY,
2345        90.0 => ROT_90,
2346        180.0 => ROT_180,
2347        270.0 => ROT_270,
2348        _ => Affine::rotate(angle_deg.to_radians()),
2349    }
2350}
2351
2352fn map_and_push_if_present<T, U>(dest: &mut Vec<T>, src: Vec<U>, map: fn(U) -> T) {
2353    src.into_iter().map(map).for_each(|v| dest.push(v));
2354}
2355
2356impl RawLayer {
2357    fn build(self, format_version: FormatVersion) -> Result<Layer, Error> {
2358        // we do what glyphsLib does:
2359        // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f4/Lib/glyphsLib/classes.py#L3662
2360        // which is apparently standard, in that if a field is missing it has
2361        // some implied default value? Although I don't know where these are
2362        // all documented, outside of glyphsLib.
2363        const DEFAULT_LAYER_WIDTH: f64 = 600.;
2364        let mut shapes = Vec::new();
2365
2366        // Glyphs v2 uses paths and components
2367        map_and_push_if_present(&mut shapes, self.paths, Shape::Path);
2368        map_and_push_if_present(&mut shapes, self.components, Shape::Component);
2369
2370        // Glyphs v3 uses shapes for both
2371        for raw_shape in self.shapes {
2372            shapes.push(raw_shape.try_into()?);
2373        }
2374
2375        let anchors = self
2376            .anchors
2377            .into_iter()
2378            .map(|ra| {
2379                let pos = if let Some(pos) = ra.pos {
2380                    pos
2381                } else if let Some(raw) = ra.position {
2382                    Point::parse_plist(&raw).unwrap()
2383                } else {
2384                    Point::ZERO
2385                };
2386                Anchor { name: ra.name, pos }
2387            })
2388            .collect();
2389
2390        let mut attributes = self.attributes;
2391        // convert v2 bracket layers (based on name) into AxisRule attrs
2392        if let Some(axis_rule) =
2393            AxisRule::from_layer_name(&self.name).filter(|_| format_version.is_v2())
2394        {
2395            assert!(
2396                attributes.axis_rules.is_empty(),
2397                "glyphs v2 does not use axisRules attr"
2398            );
2399            attributes.axis_rules.push(axis_rule);
2400        }
2401        Ok(Layer {
2402            layer_id: self.layer_id,
2403            associated_master_id: self.associated_master_id,
2404            width: self.width.unwrap_or(DEFAULT_LAYER_WIDTH.into()),
2405            vert_width: self.vert_width,
2406            vert_origin: self.vert_origin,
2407            shapes,
2408            anchors,
2409            attributes,
2410        })
2411    }
2412}
2413
2414impl RawGlyph {
2415    // we pass in the radix because it depends on the version, stored in the font struct
2416    fn build(self, format_version: FormatVersion, glyph_data: &GlyphData) -> Result<Glyph, Error> {
2417        let mut instances = Vec::new();
2418        let mut bracket_layers = Vec::new();
2419        for layer in self.layers {
2420            if layer.is_bracket_layer(format_version) {
2421                bracket_layers.push(layer.build(format_version)?);
2422            } else if !layer.is_draft() {
2423                instances.push(layer.build(format_version)?);
2424            }
2425        }
2426        // if category/subcategory were set in the source, we keep them;
2427        // otherwise we look them up based on the bundled GlyphData.
2428        // (we use this info later to determine GDEF categories, zero the width
2429        // on non-spacing marks, etc)
2430        fn parse_category<T>(s: Option<&str>, glyph: &SmolStr) -> Option<T>
2431        where
2432            T: FromStr<Err = SmolStr>,
2433        {
2434            match s.filter(|s| !s.is_empty()).map(T::from_str).transpose() {
2435                Ok(x) => x,
2436                // if we don't know a category ignore it and we'll compute it later
2437                Err(err) => {
2438                    log::warn!("Unknown category '{err}' for glyph '{glyph}'");
2439                    None
2440                }
2441            }
2442        }
2443
2444        let mut category = parse_category(self.category.as_deref(), &self.glyphname);
2445        let mut sub_category = parse_category(self.sub_category.as_deref(), &self.glyphname);
2446        let mut production_name = self.production_name;
2447
2448        let codepoints = self
2449            .unicode
2450            .map(|s| parse_codepoint_str(&s, format_version.codepoint_radix()))
2451            .unwrap_or_default();
2452
2453        if category.is_none() || sub_category.is_none() || production_name.is_none() {
2454            if let Some(result) = glyph_data.query(&self.glyphname, Some(&codepoints)) {
2455                // if they were manually set don't change them, otherwise do
2456                category = category.or(Some(result.category));
2457                sub_category = sub_category.or(result.subcategory);
2458                production_name = production_name.or(result.production_name.map(Into::into));
2459            }
2460        }
2461
2462        Ok(Glyph {
2463            name: self.glyphname,
2464            export: self.export.unwrap_or(true),
2465            layers: instances,
2466            bracket_layers,
2467            left_kern: self.kern_left,
2468            right_kern: self.kern_right,
2469            unicode: codepoints,
2470            category,
2471            sub_category,
2472            production_name,
2473        })
2474    }
2475}
2476
2477// https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/constants.py#L186
2478#[rustfmt::skip]
2479static GLYPHS_TO_OPENTYPE_LANGUAGE_ID: &[(&str, i32)] = &[
2480    ("AFK", 0x0436), ("ARA", 0x0C01), ("ASM", 0x044D), ("AZE", 0x042C), ("BEL", 0x0423),
2481    ("BEN", 0x0845), ("BGR", 0x0402), ("BRE", 0x047E), ("CAT", 0x0403), ("CSY", 0x0405),
2482    ("DAN", 0x0406), ("DEU", 0x0407), ("ELL", 0x0408), ("ENG", 0x0409), ("ESP", 0x0C0A),
2483    ("ETI", 0x0425), ("EUQ", 0x042D), ("FIN", 0x040B), ("FLE", 0x0813), ("FOS", 0x0438),
2484    ("FRA", 0x040C), ("FRI", 0x0462), ("GRN", 0x046F), ("GUJ", 0x0447), ("HAU", 0x0468),
2485    ("HIN", 0x0439), ("HRV", 0x041A), ("HUN", 0x040E), ("HVE", 0x042B), ("IRI", 0x083C),
2486    ("ISL", 0x040F), ("ITA", 0x0410), ("IWR", 0x040D), ("JPN", 0x0411), ("KAN", 0x044B),
2487    ("KAT", 0x0437), ("KAZ", 0x043F), ("KHM", 0x0453), ("KOK", 0x0457), ("LAO", 0x0454),
2488    ("LSB", 0x082E), ("LTH", 0x0427), ("LVI", 0x0426), ("MAR", 0x044E), ("MKD", 0x042F),
2489    ("MLR", 0x044C), ("MLY", 0x043E), ("MNG", 0x0352), ("MTS", 0x043A), ("NEP", 0x0461),
2490    ("NLD", 0x0413), ("NOB", 0x0414), ("ORI", 0x0448), ("PAN", 0x0446), ("PAS", 0x0463),
2491    ("PLK", 0x0415), ("PTG", 0x0816), ("PTG-BR", 0x0416), ("RMS", 0x0417), ("ROM", 0x0418),
2492    ("RUS", 0x0419), ("SAN", 0x044F), ("SKY", 0x041B), ("SLV", 0x0424), ("SQI", 0x041C),
2493    ("SRB", 0x081A), ("SVE", 0x041D), ("TAM", 0x0449), ("TAT", 0x0444), ("TEL", 0x044A),
2494    ("THA", 0x041E), ("TIB", 0x0451), ("TRK", 0x041F), ("UKR", 0x0422), ("URD", 0x0420),
2495    ("USB", 0x042E), ("UYG", 0x0480), ("UZB", 0x0443), ("VIT", 0x042A), ("WEL", 0x0452),
2496    ("ZHH", 0x0C04), ("ZHS", 0x0804), ("ZHT", 0x0404),
2497    ("dflt", 0x0409),
2498];
2499
2500impl RawFeature {
2501    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L43
2502    fn autostr(&self) -> &str {
2503        match self.automatic {
2504            Some(1) => "# automatic\n",
2505            _ => "",
2506        }
2507    }
2508
2509    fn name(&self) -> Result<&str, Error> {
2510        self.name
2511            .as_deref()
2512            .or(self.tag.as_deref())
2513            .ok_or_else(|| Error::StructuralError(format!("{self:?} missing name and tag")))
2514    }
2515
2516    fn disabled(&self) -> bool {
2517        self.disabled == Some(1)
2518    }
2519
2520    /// Some glyphs sources store stylistic set names in the 'note' field
2521    ///
2522    /// See the little sidebar item here:
2523    /// <https://glyphsapp.com/learn/stylistic-sets#g-names-for-stylistic-sets>
2524    fn legacy_name_record_maybe(&self) -> Option<String> {
2525        let name = self.notes.as_deref()?.strip_prefix("Name:")?.trim();
2526        if name.is_empty() {
2527            None
2528        } else {
2529            Some(format!("name 3 1 0x409 \"{name}\";"))
2530        }
2531    }
2532
2533    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L134
2534    fn feature_names(&self) -> String {
2535        let labels = self
2536            .labels
2537            .iter()
2538            .filter_map(|label| label.to_fea())
2539            .chain(self.legacy_name_record_maybe())
2540            .collect::<Vec<_>>()
2541            .join("\n");
2542        if labels.is_empty() {
2543            Default::default()
2544        } else {
2545            format!("featureNames {{\n{labels}\n}};\n")
2546        }
2547    }
2548
2549    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L90
2550    fn prefix_to_feature(&self) -> Result<FeatureSnippet, Error> {
2551        let name = self.name.as_deref().unwrap_or_default();
2552        let code = format!("# Prefix: {}\n{}{}", name, self.autostr(), self.code);
2553        Ok(FeatureSnippet::new(code, self.disabled()))
2554    }
2555
2556    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L101
2557    fn class_to_feature(&self) -> Result<FeatureSnippet, Error> {
2558        let name = self.name()?;
2559        let code = format!(
2560            "{}{}{name} = [ {}\n];",
2561            self.autostr(),
2562            if name.starts_with('@') { "" } else { "@" },
2563            self.code
2564        );
2565        Ok(FeatureSnippet::new(code, self.disabled()))
2566    }
2567
2568    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L113
2569    fn raw_feature_to_feature(&self) -> Result<FeatureSnippet, Error> {
2570        let name = self.name()?;
2571        let insert_mark = self.insert_mark_if_manual_kern_feature();
2572        let code = format!(
2573            "feature {name} {{\n{}{}{}{insert_mark}\n}} {name};",
2574            self.autostr(),
2575            self.feature_names(),
2576            self.code
2577        );
2578        Ok(FeatureSnippet::new(code, self.disabled()))
2579    }
2580
2581    //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f4/Lib/glyphsLib/builder/features.py#L180
2582    fn insert_mark_if_manual_kern_feature(&self) -> &str {
2583        if self.name().unwrap_or_default() == "kern"
2584            && self.automatic != Some(1)
2585            && !self.code.contains("# Automatic Code")
2586        {
2587            "# Automatic Code\n"
2588        } else {
2589            ""
2590        }
2591    }
2592}
2593
2594impl RawNameValue {
2595    fn to_fea(&self) -> Option<String> {
2596        if self.value.is_empty() {
2597            // skip empty names:
2598            // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f/Lib/glyphsLib/builder/features.py#L155
2599            return None;
2600        }
2601
2602        match GLYPHS_TO_OPENTYPE_LANGUAGE_ID
2603            .binary_search_by_key(&self.language.as_str(), |entry| entry.0)
2604        {
2605            Ok(idx) => {
2606                let language_id = GLYPHS_TO_OPENTYPE_LANGUAGE_ID[idx].1;
2607                let name = self.value.replace("\\", "\\005c").replace("\"", "\\0022");
2608                Some(format!("  name 3 1 0x{language_id:04X} \"{name}\";"))
2609            }
2610            Err(_) => {
2611                warn!("Unknown feature label language: {}", self.language);
2612                None
2613            }
2614        }
2615    }
2616}
2617
2618/// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/classes.py#L220-L249>
2619fn lookup_class_value(axis_tag: &str, user_class: &str) -> Option<u16> {
2620    let user_class = match user_class {
2621        value if !value.is_empty() => {
2622            let mut value = value.to_ascii_lowercase();
2623            value.retain(|c| c != ' ');
2624            value
2625        }
2626        _ => String::from(""),
2627    };
2628    match (axis_tag, user_class.as_str()) {
2629        ("wght", "thin") => Some(100),
2630        ("wght", "extralight" | "ultralight") => Some(200),
2631        ("wght", "light") => Some(300),
2632        ("wght", "" | "normal" | "regular") => Some(400),
2633        ("wght", "medium") => Some(500),
2634        ("wght", "demibold" | "semibold") => Some(600),
2635        ("wght", "bold") => Some(700),
2636        ("wght", "ultrabold" | "extrabold") => Some(800),
2637        ("wght", "black" | "heavy") => Some(900),
2638        ("wdth", "ultracondensed") => Some(1),
2639        ("wdth", "extracondensed") => Some(2),
2640        ("wdth", "condensed") => Some(3),
2641        ("wdth", "semicondensed") => Some(4),
2642        ("wdth", "" | "Medium (normal)") => Some(5),
2643        ("wdth", "semiexpanded") => Some(6),
2644        ("wdth", "expanded") => Some(7),
2645        ("wdth", "extraexpanded") => Some(8),
2646        ("wdth", "ultraexpanded") => Some(9),
2647        _ => {
2648            warn!("Unrecognized ('{axis_tag}', '{user_class}')");
2649            None
2650        }
2651    }
2652}
2653
2654fn add_mapping_if_new(
2655    axis_mappings: &mut BTreeMap<String, AxisUserToDesignMap>,
2656    axes: &[Axis],
2657    axis_tag: &str,
2658    axes_values: &[OrderedFloat<f64>],
2659    value: f64,
2660) {
2661    let Some(idx) = axes.iter().position(|a| a.tag == axis_tag) else {
2662        return;
2663    };
2664    let axis = &axes[idx];
2665    let Some(design) = axes_values.get(idx) else {
2666        return;
2667    };
2668
2669    axis_mappings
2670        .entry(axis.name.clone())
2671        .or_default()
2672        .add_if_new(value.into(), *design);
2673}
2674
2675impl Instance {
2676    /// Glyphs 2 instances have fun fields.
2677    ///
2678    /// Mappings based on
2679    /// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/classes.py#L3451>
2680    fn new(
2681        axes: &[Axis],
2682        value: &RawInstance,
2683        masters_have_axis_locations: bool,
2684    ) -> Result<Self, Error> {
2685        let active = value.is_active();
2686        let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
2687
2688        // Instances can also have "Axis Location" custom parameters, complementing the ones
2689        // from the masters: https://github.com/googlefonts/fontc/issues/918
2690        let mut tags_done = BTreeSet::new();
2691        for axis_location in value.custom_parameters.axis_locations().unwrap_or_default() {
2692            let Some(axis_index) = axis_index(axes, |a| a.name == axis_location.axis_name) else {
2693                log::warn!(
2694                    "{} instance's 'Axis Location' includes axis {:?} not included in font",
2695                    value.name,
2696                    axis_location.axis_name
2697                );
2698                continue;
2699            };
2700            let user = axis_location.location;
2701            let design = value.axes_values[axis_index];
2702
2703            axis_mappings
2704                .entry(axis_location.axis_name.clone())
2705                .or_default()
2706                .add_if_new(user, design);
2707
2708            tags_done.insert(axes[axis_index].tag.as_str());
2709        }
2710
2711        // only infer legacy mappings when Axis Locations aren't defined
2712        if !masters_have_axis_locations && !tags_done.contains("wght") {
2713            // OS/2 weight_class corresponds to 'wght' axis user-space value
2714            add_mapping_if_new(
2715                &mut axis_mappings,
2716                axes,
2717                "wght",
2718                &value.axes_values,
2719                value
2720                    .weight_class
2721                    .as_ref()
2722                    .map(|v| f64::from_str(v).unwrap())
2723                    .unwrap_or(400.0),
2724            );
2725        }
2726
2727        if !masters_have_axis_locations && !tags_done.contains("wdth") {
2728            // OS/2 width_class gets mapped to 'wdth' percent scale, see:
2729            // https://github.com/googlefonts/glyphsLib/blob/7041311e/Lib/glyphsLib/builder/constants.py#L222
2730            add_mapping_if_new(
2731                &mut axis_mappings,
2732                axes,
2733                "wdth",
2734                value.axes_values.as_ref(),
2735                value
2736                    .width_class
2737                    .as_ref()
2738                    .map(|v| match WidthClass::try_from(u16::from_str(v).unwrap()) {
2739                        Ok(width_class) => width_class.to_percent(),
2740                        Err(err) => {
2741                            warn!("{err}");
2742                            100.0
2743                        }
2744                    })
2745                    .unwrap_or(100.0),
2746            );
2747        }
2748
2749        Ok(Instance {
2750            name: value.name.clone(),
2751            active,
2752            type_: value
2753                .type_
2754                .as_ref()
2755                .map(|v| v.as_str().into())
2756                .unwrap_or(InstanceType::Single),
2757            axis_mappings,
2758            axes_values: value.axes_values.clone(),
2759            properties: value.properties.clone(),
2760            custom_parameters: value.custom_parameters.to_custom_params()?,
2761        })
2762    }
2763
2764    fn family_name(&self) -> Option<&str> {
2765        self.properties
2766            .iter()
2767            .find(|raw| raw.key == "familyNames")
2768            .and_then(RawName::get_value)
2769        // glyphsLib here also checks for 'familyName' in customParams, but we
2770        // don't have that key? Possible that we're ignoring it in the raw params
2771        // conversion..
2772        // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577/Lib/glyphsLib/classes.py#L3271
2773    }
2774
2775    /// Get the optional postscript name to use for the `fvar` named instance.
2776    pub fn postscript_name(&self) -> Option<&str> {
2777        // https://handbook.glyphsapp.com/custom-parameter-descriptions/
2778        // https://github.com/googlefonts/glyphsLib/blob/7a8f643a711ffb9d351d037425a7eb60759e6219/Lib/glyphsLib/builder/instances.py#L118-L121
2779        ["variablePostscriptFontName", "postscriptFontName"]
2780            .iter()
2781            .find_map(|key| {
2782                self.properties
2783                    .iter()
2784                    .find(|raw| raw.key == *key)
2785                    .and_then(RawName::get_value)
2786            })
2787    }
2788}
2789
2790/// Glyphs appears to use code page identifiers rather than bits
2791///
2792/// <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#ulcodepagerange>
2793fn codepage_range_bit(codepage: u32) -> Result<u32, Error> {
2794    Ok(match codepage {
2795        1252 => 0,  // Latin 1
2796        1250 => 1,  // Latin 2: Eastern Europe
2797        1251 => 2,  // Cyrillic
2798        1253 => 3,  // Greek
2799        1254 => 4,  // Turkish
2800        1255 => 5,  // Hebrew
2801        1256 => 6,  // Arabic
2802        1257 => 7,  // Windows Baltic
2803        1258 => 8,  // Vietnamese
2804        874 => 16,  // Thai
2805        932 => 17,  // JIS/Japan
2806        936 => 18,  // Chinese: Simplified PRC and Singapore
2807        949 => 19,  // Korean Wansung
2808        950 => 20,  // Chinese: Traditional Taiwan and Hong Kong SAR
2809        1361 => 21, // Korean Johab
2810        869 => 48,  // IBM Greek
2811        866 => 49,  // MS-DOS Russian
2812        865 => 50,  // MS-DOS Nordic
2813        864 => 51,  // Arabic
2814        863 => 52,  //	MS-DOS Canadian French
2815        862 => 53,  //		Hebrew
2816        861 => 54,  //		MS-DOS Icelandic
2817        860 => 55,  //		MS-DOS Portuguese
2818        857 => 56,  //		IBM Turkish
2819        855 => 57,  //	IBM Cyrillic; primarily Russian
2820        852 => 58,  //		Latin 2
2821        775 => 59,  //		MS-DOS Baltic
2822        737 => 60,  //	Greek; former 437 G
2823        708 => 61,  //	Arabic; ASMO 708
2824        850 => 62,  //	WE/Latin 1
2825        437 => 63,  //	US
2826
2827        v if v < 64 => v, // an actual bit
2828        _ => return Err(Error::InvalidCodePage(codepage)),
2829    })
2830}
2831
2832fn update_names(names: &mut BTreeMap<String, String>, raw_names: &[RawName]) {
2833    for name in raw_names {
2834        // Name may have one value, in which case use it, or lots, in which case try to pick a winner
2835        if let Some(value) = name.get_value() {
2836            names.insert(name.key.clone(), value.to_string());
2837        }
2838    }
2839}
2840
2841impl TryFrom<RawFont> for Font {
2842    type Error = Error;
2843
2844    fn try_from(mut from: RawFont) -> Result<Self, Self::Error> {
2845        if from.format_version.is_v2() {
2846            from.v2_to_v3()?;
2847        } else {
2848            // <https://github.com/googlefonts/fontc/issues/1029>
2849            from.v2_to_v3_names()?;
2850        }
2851
2852        // TODO: this should be provided in a manner that allows for overrides
2853        let glyph_data = GlyphData::default();
2854
2855        let mut custom_parameters = from.custom_parameters.to_custom_params()?;
2856        let glyph_order = make_glyph_order(&from.glyphs, custom_parameters.glyph_order.take());
2857
2858        let default_master_idx = default_master_idx(&from);
2859
2860        // originally glyphsLib would infer wght/wdth mappings from the instance weight/width classes;
2861        // then, a new "Axis Location" custom parameter was defined for both masters and instances;
2862        // when (all) masters use the latter, only explicit "Axis Location" parameters get used from
2863        // instances, and the legacy heuristic for wght/wdth is disabled, so we need to keep track
2864        // of this when building instance axis mappings:
2865        // https://github.com/googlefonts/glyphsLib/blob/7a8f643a/Lib/glyphsLib/builder/axes.py#L225-L227
2866        let masters_have_axis_locations = from
2867            .font_master
2868            .iter()
2869            .all(|master| master.custom_parameters.contains("Axis Location"));
2870
2871        let instances: Vec<_> = from
2872            .instances
2873            .iter()
2874            .map(|ri| Instance::new(&from.axes, ri, masters_have_axis_locations))
2875            .collect::<Result<Vec<_>, Error>>()?;
2876
2877        let axis_mappings = UserToDesignMapping::new(&from, &instances);
2878
2879        let mut glyphs = BTreeMap::new();
2880        for raw_glyph in from.glyphs.into_iter() {
2881            glyphs.insert(
2882                raw_glyph.glyphname.clone(),
2883                raw_glyph.build(from.format_version, &glyph_data)?,
2884            );
2885        }
2886
2887        let mut features = Vec::new();
2888        for class in from.classes {
2889            features.push(class.class_to_feature()?);
2890        }
2891        for prefix in from.feature_prefixes {
2892            features.push(prefix.prefix_to_feature()?);
2893        }
2894        for feature in from.features {
2895            features.push(feature.raw_feature_to_feature()?);
2896        }
2897
2898        let units_per_em = from.units_per_em.ok_or(Error::NoUnitsPerEm)?;
2899        let units_per_em = units_per_em.try_into().map_err(Error::InvalidUpem)?;
2900
2901        let mut names = BTreeMap::new();
2902        update_names(&mut names, &from.properties);
2903        // Evidently names can come from instances too! They are higher priority so do them last (overwriting prior values)
2904        for instance in &instances {
2905            if instance.active
2906                && instance.type_ == InstanceType::Variable
2907                    // glyphsLib has instance.familyName return the root familyName
2908                    // if it has no other value; we just unwrap_or_true here
2909                    // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577/Lib/glyphsLib/classes.py#L3272
2910                && instance.family_name().map(|name| name ==  from.family_name.as_str()).unwrap_or(true)
2911            {
2912                update_names(&mut names, &instance.properties);
2913            }
2914        }
2915        // And a final few delicate butterflies
2916        names.insert("familyNames".into(), from.family_name);
2917        if let Some(version) = names.remove("versionString") {
2918            names.insert("version".into(), version);
2919        }
2920
2921        let metric_names: BTreeMap<usize, String> = from
2922            .metrics
2923            .into_iter()
2924            .enumerate()
2925            .map(|(idx, metric)| (idx, metric.type_))
2926            .collect();
2927
2928        let masters = from
2929            .font_master
2930            .into_iter()
2931            .map(|m| {
2932                let custom_parameters = m.custom_parameters.to_custom_params()?;
2933                Ok(FontMaster {
2934                    id: m.id,
2935                    name: m.name.unwrap_or_default(),
2936                    axes_values: m.axes_values,
2937                    metric_values: m
2938                        .metric_values
2939                        .into_iter()
2940                        .enumerate()
2941                        .filter_map(|(idx, value)| {
2942                            metric_names.get(&idx).map(|name| (name.clone(), value))
2943                        })
2944                        .fold(BTreeMap::new(), |mut acc, (name, value)| {
2945                            // only insert a metric if one with the same name hasn't been added
2946                            // yet; matches glyphsLib's behavior where the first duplicate wins
2947                            // https://github.com/googlefonts/fontc/issues/1269
2948                            acc.entry(name).or_insert(value.into());
2949                            acc
2950                        }),
2951                    number_values: from
2952                        .numbers
2953                        .iter()
2954                        .zip(m.number_values.iter())
2955                        .map(|(k, v)| (k.name.clone(), *v))
2956                        .collect(),
2957                    custom_parameters,
2958                })
2959            })
2960            .collect::<Result<_, Error>>()?;
2961
2962        let virtual_masters = custom_parameters.virtual_masters.take().unwrap_or_default();
2963        Ok(Font {
2964            units_per_em,
2965            axes: from.axes,
2966            masters,
2967            default_master_idx,
2968            glyphs,
2969            glyph_order,
2970            axis_mappings,
2971            virtual_masters,
2972            features,
2973            names,
2974            instances,
2975            version_major: from.versionMajor.unwrap_or_default() as i32,
2976            version_minor: from.versionMinor.unwrap_or_default() as u32,
2977            date: from.date,
2978            kerning_ltr: from.kerning_LTR,
2979            kerning_rtl: from.kerning_RTL,
2980            custom_parameters,
2981        })
2982    }
2983}
2984
2985fn preprocess_unparsed_plist(s: &str) -> Cow<'_, str> {
2986    // Glyphs has a wide variety of unicode definitions, not all of them parser friendly
2987    // Make unicode always a string, without any wrapping () so we can parse as csv, radix based on format version
2988    let unicode_re =
2989        Regex::new(r"(?m)^(?P<prefix>\s*unicode\s*=\s*)[(]?(?P<value>[0-9a-zA-Z,]+)[)]?;\s*$")
2990            .unwrap();
2991    unicode_re.replace_all(s, r#"$prefix"$value";"#)
2992}
2993
2994fn variable_instance_for<'a>(instances: &'a [Instance], name: &str) -> Option<&'a Instance> {
2995    instances
2996        .iter()
2997        .find(|i| i.active && i.type_ == InstanceType::Variable && i.name == name)
2998}
2999
3000impl Font {
3001    pub fn load_from_string(data: &str) -> Result<Font, Error> {
3002        let raw_font = RawFont::load_from_string(data)?;
3003        let mut font = Font::try_from(raw_font)?;
3004        font.preprocess();
3005        Ok(font)
3006    }
3007
3008    pub fn load(glyphs_file: &path::Path) -> Result<Font, Error> {
3009        let mut font = Self::load_raw(glyphs_file)?;
3010        font.preprocess();
3011        Ok(font)
3012    }
3013
3014    fn preprocess(&mut self) {
3015        // ensure that glyphs with components that have bracket layers
3016        // also have bracket layers.
3017        self.align_bracket_layers();
3018
3019        // propagate anchors by default unless explicitly set to false
3020        if self.custom_parameters.propagate_anchors.unwrap_or(true) {
3021            self.propagate_all_anchors();
3022        }
3023    }
3024
3025    /// if a glyph has components that have alternate layers, copy the layer
3026    /// locations into the glyph.
3027    ///
3028    /// This is required to correctly handle bracket glyphs.
3029    // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d/Lib/glyphsLib/builder/bracket_layers.py#L176
3030    fn align_bracket_layers(&mut self) {
3031        // python does this recursively but we'll presort instead
3032        let todo = self.depth_sorted_composite_glyphs();
3033
3034        for name in &todo {
3035            let mut needed = IndexMap::new();
3036            let glyph = self.glyphs.get(name).unwrap();
3037            let my_bracket_layers = glyph
3038                .bracket_layers
3039                .iter()
3040                .map(|l| l.bracket_info(&self.axes))
3041                .collect::<IndexSet<_>>();
3042            for layer in &glyph.layers {
3043                for comp in layer.components() {
3044                    let comp_glyph = self.glyphs.get(&comp.name).unwrap();
3045
3046                    let comp_bracket_layers = comp_glyph
3047                        .bracket_layers
3048                        .iter()
3049                        .map(|l| l.bracket_info(&self.axes))
3050                        .collect::<IndexSet<_>>();
3051                    if comp_bracket_layers != my_bracket_layers {
3052                        let missing = comp_bracket_layers.difference(&my_bracket_layers);
3053                        needed
3054                            .entry(layer.layer_id.clone())
3055                            .or_insert(IndexSet::new())
3056                            .extend(missing.cloned());
3057                    }
3058                }
3059            }
3060
3061            if needed.is_empty() {
3062                // all good, next glyph
3063                continue;
3064            }
3065
3066            let mut new_layers = Vec::new();
3067            for (master, needed_brackets) in needed {
3068                let master_name = self
3069                    .masters
3070                    .iter()
3071                    .find_map(|m| (m.id == master).then_some(m.name.as_str()))
3072                    .unwrap_or(master.as_str());
3073                if !my_bracket_layers.is_empty() {
3074                    log::warn!(
3075                        "Glyph {name} in master {master_name} has different bracket layers \
3076                        than its components. This is not supported, so some bracket layers \
3077                        will not be applied. Consider fixing the source instead."
3078                    );
3079                }
3080
3081                let base_layer = glyph.layers.iter().find(|l| l.layer_id == master).unwrap();
3082                for box_ in needed_brackets {
3083                    log::debug!("synthesized layer {box_:?} in master {master_name} for '{name}'",);
3084                    let new_layer = synthesize_bracket_layer(base_layer, box_, &self.axes);
3085                    new_layers.push(new_layer);
3086                }
3087            }
3088
3089            self.glyphs
3090                .get_mut(name)
3091                .unwrap()
3092                .bracket_layers
3093                .extend_from_slice(&new_layers);
3094        }
3095    }
3096
3097    // load without propagating anchors or components
3098    pub(crate) fn load_raw(glyphs_file: impl AsRef<path::Path>) -> Result<Font, Error> {
3099        RawFont::load(glyphs_file.as_ref()).and_then(Font::try_from)
3100    }
3101
3102    pub fn default_master(&self) -> &FontMaster {
3103        &self.masters[self.default_master_idx]
3104    }
3105
3106    /// <https://handbook.glyphsapp.com/exports/>
3107    pub fn variable_export_settings(&self, master: &FontMaster) -> Option<&Instance> {
3108        variable_instance_for(&self.instances, &master.name)
3109    }
3110
3111    pub fn vendor_id(&self) -> Option<&String> {
3112        self.names.get("vendorID")
3113    }
3114}
3115
3116//https://github.com/googlefonts/glyphsLib/blob/c4db6b981d/Lib/glyphsLib/builder/bracket_layers.py#L258
3117fn synthesize_bracket_layer(
3118    old_layer: &Layer,
3119    box_: BTreeMap<String, (Option<i64>, Option<i64>)>,
3120    axes: &[Axis],
3121) -> Layer {
3122    let mut new_layer = old_layer.clone();
3123    new_layer.associated_master_id = Some(std::mem::take(&mut new_layer.layer_id));
3124    new_layer.attributes.axis_rules = axes
3125        .iter()
3126        .map(|axis| {
3127            if let Some((min, max)) = box_.get(&axis.tag) {
3128                AxisRule {
3129                    min: min.map(|x| x as _),
3130                    max: max.map(|x| x as _),
3131                }
3132            } else {
3133                Default::default()
3134            }
3135        })
3136        .collect();
3137
3138    new_layer
3139}
3140
3141/// Convert [kurbo::Point] to this for eq and hash/
3142#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3143struct PointForEqAndHash {
3144    x: OrderedFloat<f64>,
3145    y: OrderedFloat<f64>,
3146}
3147
3148impl PointForEqAndHash {
3149    fn new(point: Point) -> PointForEqAndHash {
3150        point.into()
3151    }
3152}
3153
3154impl From<Point> for PointForEqAndHash {
3155    fn from(value: Point) -> Self {
3156        PointForEqAndHash {
3157            x: value.x.into(),
3158            y: value.y.into(),
3159        }
3160    }
3161}
3162
3163/// Convert [kurbo::Affine] to this for eq and hash/
3164#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3165struct AffineForEqAndHash([OrderedFloat<f64>; 6]);
3166
3167impl From<Affine> for AffineForEqAndHash {
3168    fn from(value: Affine) -> Self {
3169        Self(value.as_coeffs().map(|coeff| coeff.into()))
3170    }
3171}
3172
3173#[cfg(test)]
3174mod tests {
3175    use super::*;
3176    use crate::{plist::FromPlist, Font, FontMaster, Node, Shape};
3177    use std::{
3178        collections::{BTreeMap, BTreeSet, HashSet},
3179        path::{Path, PathBuf},
3180    };
3181
3182    use ordered_float::OrderedFloat;
3183
3184    use pretty_assertions::assert_eq;
3185
3186    use kurbo::{Affine, Point};
3187
3188    use rstest::rstest;
3189    use smol_str::ToSmolStr;
3190
3191    fn testdata_dir() -> PathBuf {
3192        // working dir varies CLI vs VSCode
3193        let mut dir = Path::new("../resources/testdata");
3194        if !dir.is_dir() {
3195            dir = Path::new("./resources/testdata");
3196        }
3197        assert!(dir.is_dir());
3198        dir.to_path_buf()
3199    }
3200
3201    fn glyphs2_dir() -> PathBuf {
3202        testdata_dir().join("glyphs2")
3203    }
3204
3205    fn glyphs3_dir() -> PathBuf {
3206        testdata_dir().join("glyphs3")
3207    }
3208
3209    fn round(transform: Affine, digits: u8) -> Affine {
3210        let m = 10f64.powi(digits as i32);
3211        let mut coeffs = transform.as_coeffs();
3212        for c in coeffs.iter_mut() {
3213            *c = (*c * m).round() / m;
3214        }
3215        Affine::new(coeffs)
3216    }
3217
3218    #[test]
3219    fn v2_format_version() {
3220        let v2_font = glyphs2_dir().join("Mono.glyphs");
3221        let as_str = std::fs::read_to_string(&v2_font).unwrap();
3222        assert!(!as_str.contains(".formatVersion"), "only exists in v3");
3223        let font = RawFont::load(&v2_font).unwrap();
3224        // falls back to default
3225        assert_eq!(font.format_version, FormatVersion::V2);
3226    }
3227
3228    #[test]
3229    fn v3_format_version() {
3230        let v3_font = glyphs3_dir().join("MasterNames.glyphs");
3231        let as_str = std::fs::read_to_string(&v3_font).unwrap();
3232        assert!(as_str.contains(".formatVersion"), "exists in v3");
3233        let font = RawFont::load(&v3_font).unwrap();
3234        // falls back to default
3235        assert_eq!(font.format_version, FormatVersion::V3);
3236    }
3237
3238    #[test]
3239    fn test_glyphs3_node() {
3240        let node: Node = Node::parse_plist("(354, 183, l)").unwrap();
3241        assert_eq!(
3242            Node {
3243                node_type: crate::NodeType::Line,
3244                pt: super::Point { x: 354.0, y: 183.0 }
3245            },
3246            node
3247        );
3248    }
3249
3250    #[test]
3251    fn test_glyphs2_node() {
3252        let node: Node = Node::parse_plist("\"354 183 LINE\"").unwrap();
3253        assert_eq!(
3254            Node {
3255                node_type: crate::NodeType::Line,
3256                pt: super::Point { x: 354.0, y: 183.0 }
3257            },
3258            node
3259        );
3260    }
3261
3262    #[test]
3263    fn test_glyphs3_node_userdata() {
3264        let node = Node::parse_plist("(354, 183, l,{name = hr00;})").unwrap();
3265        assert_eq!(
3266            Node {
3267                node_type: crate::NodeType::Line,
3268                pt: super::Point { x: 354.0, y: 183.0 }
3269            },
3270            node
3271        );
3272    }
3273
3274    #[test]
3275    fn test_glyphs2_node_userdata() {
3276        let node = Node::parse_plist("\"354 183 LINE {name=duck}\"").unwrap();
3277        assert_eq!(
3278            Node {
3279                node_type: crate::NodeType::Line,
3280                pt: super::Point { x: 354.0, y: 183.0 }
3281            },
3282            node
3283        );
3284    }
3285
3286    // unquoted infinity likes to parse as a float which is suboptimal for glyph names. Survive.
3287    // Observed on Work Sans and Lexend.
3288    #[test]
3289    fn survive_unquoted_infinity() {
3290        // Read a minimal glyphs file that reproduces the error
3291        Font::load(&glyphs3_dir().join("infinity.glyphs")).unwrap();
3292    }
3293
3294    fn assert_wght_var_metrics(font: &Font) {
3295        let default_master = font.default_master();
3296        assert_eq!(737.0, default_master.ascender().unwrap());
3297        assert_eq!(-42.0, default_master.descender().unwrap());
3298    }
3299
3300    #[test]
3301    fn read_wght_var_2_metrics() {
3302        assert_wght_var_metrics(&Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap());
3303    }
3304
3305    #[test]
3306    fn read_wght_var_3_metrics() {
3307        assert_wght_var_metrics(&Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap());
3308    }
3309
3310    /// So far we don't have any package-only examples
3311    enum LoadCompare {
3312        Glyphs,
3313        GlyphsAndPackage,
3314    }
3315
3316    fn assert_load_v2_matches_load_v3(name: &str, compare: LoadCompare) {
3317        let has_package = matches!(compare, LoadCompare::GlyphsAndPackage);
3318        let _ = env_logger::builder().is_test(true).try_init();
3319        let filename = format!("{name}.glyphs");
3320        let pkgname = format!("{name}.glyphspackage");
3321        let g2_file = glyphs2_dir().join(filename.clone());
3322        let g3_file = glyphs3_dir().join(filename.clone());
3323        let g2 = Font::load(&g2_file).unwrap();
3324        let g3 = Font::load(&g3_file).unwrap();
3325
3326        // Handy if troubleshooting
3327        std::fs::write("/tmp/g2.glyphs.txt", format!("{g2:#?}")).unwrap();
3328        std::fs::write("/tmp/g3.glyphs.txt", format!("{g3:#?}")).unwrap();
3329
3330        // Assert fields that often don't match individually before doing the whole struct for nicer diffs
3331        assert_eq!(g2.axes, g3.axes, "axes mismatch {g2_file:?} vs {g3_file:?}");
3332        for (g2m, g3m) in g2.masters.iter().zip(g3.masters.iter()) {
3333            assert_eq!(g2m, g3m, "master mismatch {g2_file:?} vs {g3_file:?}");
3334        }
3335        assert_eq!(g2, g3, "g2 should match g3 {g2_file:?} vs {g3_file:?}");
3336
3337        if has_package {
3338            let g2_pkg = Font::load(&glyphs2_dir().join(pkgname.clone())).unwrap();
3339            let g3_pkg = Font::load(&glyphs3_dir().join(pkgname.clone())).unwrap();
3340
3341            std::fs::write("/tmp/g2.glyphspackage.txt", format!("{g2_pkg:#?}")).unwrap();
3342            std::fs::write("/tmp/g3.glyphspackage.txt", format!("{g3_pkg:#?}")).unwrap();
3343
3344            assert_eq!(g2_pkg, g3_pkg, "g2_pkg should match g3_pkg");
3345            assert_eq!(g3_pkg, g3, "g3_pkg should match g3");
3346        }
3347    }
3348
3349    #[test]
3350    fn read_wght_var_2_and_3() {
3351        assert_load_v2_matches_load_v3("WghtVar", LoadCompare::GlyphsAndPackage);
3352    }
3353
3354    #[test]
3355    fn read_wght_var_avar_2_and_3() {
3356        assert_load_v2_matches_load_v3("WghtVar_Avar", LoadCompare::GlyphsAndPackage);
3357    }
3358
3359    #[test]
3360    fn read_wght_var_instances_2_and_3() {
3361        assert_load_v2_matches_load_v3("WghtVar_Instances", LoadCompare::GlyphsAndPackage);
3362    }
3363
3364    #[test]
3365    fn read_wght_var_os2_2_and_3() {
3366        assert_load_v2_matches_load_v3("WghtVar_OS2", LoadCompare::GlyphsAndPackage);
3367    }
3368
3369    #[test]
3370    fn read_wght_var_anchors_2_and_3() {
3371        assert_load_v2_matches_load_v3("WghtVar_Anchors", LoadCompare::GlyphsAndPackage);
3372    }
3373
3374    #[test]
3375    fn read_infinity_2_and_3() {
3376        assert_load_v2_matches_load_v3("infinity", LoadCompare::GlyphsAndPackage);
3377    }
3378
3379    #[test]
3380    fn read_wght_var_noexport_2_and_3() {
3381        assert_load_v2_matches_load_v3("WghtVar_NoExport", LoadCompare::Glyphs);
3382    }
3383
3384    #[test]
3385    fn read_master_names_2_and_3() {
3386        assert_load_v2_matches_load_v3("MasterNames", LoadCompare::Glyphs);
3387    }
3388
3389    #[test]
3390    fn read_master_names_with_italic_2_and_3() {
3391        assert_load_v2_matches_load_v3("MasterNames-Italic", LoadCompare::Glyphs);
3392    }
3393
3394    fn only_shape_in_only_layer<'a>(font: &'a Font, glyph_name: &str) -> &'a Shape {
3395        let glyph = font.glyphs.get(glyph_name).unwrap();
3396        assert_eq!(1, glyph.layers.len());
3397        assert_eq!(1, glyph.layers[0].shapes.len());
3398        &glyph.layers[0].shapes[0]
3399    }
3400
3401    fn check_v2_to_v3_transform(glyphs_file: &str, glyph_name: &str, expected: Affine) {
3402        let g2 = Font::load(&glyphs2_dir().join(glyphs_file)).unwrap();
3403        let g3 = Font::load(&glyphs3_dir().join(glyphs_file)).unwrap();
3404
3405        // We're exclusively interested in the transform
3406        let g2_shape = only_shape_in_only_layer(&g2, glyph_name);
3407        let g3_shape = only_shape_in_only_layer(&g3, glyph_name);
3408
3409        let Shape::Component(g2_shape) = g2_shape else {
3410            panic!("{g2_shape:?} should be a component");
3411        };
3412        let Shape::Component(g3_shape) = g3_shape else {
3413            panic!("{g3_shape:?} should be a component");
3414        };
3415
3416        assert_eq!(expected, round(g2_shape.transform, 4));
3417        assert_eq!(expected, round(g3_shape.transform, 4));
3418    }
3419
3420    #[test]
3421    fn read_transformed_component_2_and_3_uniform_scale() {
3422        let expected = Affine::new([1.6655, 1.1611, -1.1611, 1.6655, -233.0, -129.0]);
3423        check_v2_to_v3_transform("Component.glyphs", "comma", expected);
3424    }
3425
3426    #[test]
3427    fn read_transformed_component_2_and_3_nonuniform_scale() {
3428        let expected = Affine::new([0.8452, 0.5892, -1.1611, 1.6655, -233.0, -129.0]);
3429        check_v2_to_v3_transform("Component.glyphs", "non_uniform_scale", expected);
3430    }
3431
3432    #[test]
3433    fn upgrade_2_to_3_with_implicit_axes() {
3434        let font = Font::load(&glyphs2_dir().join("WghtVar_ImplicitAxes.glyphs")).unwrap();
3435        assert_eq!(
3436            font.axes
3437                .iter()
3438                .map(|a| a.tag.as_str())
3439                .collect::<Vec<&str>>(),
3440            vec!["wght", "wdth", "XXXX"]
3441        );
3442    }
3443
3444    #[test]
3445    fn understand_v2_style_unquoted_hex_unicode() {
3446        let font = Font::load(&glyphs2_dir().join("Unicode-UnquotedHex.glyphs")).unwrap();
3447        assert_eq!(
3448            BTreeSet::from([0x1234]),
3449            font.glyphs.get("name").unwrap().unicode,
3450        );
3451        assert_eq!(1, font.glyphs.len());
3452    }
3453
3454    #[test]
3455    fn understand_v2_style_quoted_hex_unicode_sequence() {
3456        let font = Font::load(&glyphs2_dir().join("Unicode-QuotedHexSequence.glyphs")).unwrap();
3457        assert_eq!(
3458            BTreeSet::from([0x2044, 0x200D, 0x2215]),
3459            font.glyphs.get("name").unwrap().unicode,
3460        );
3461        assert_eq!(1, font.glyphs.len());
3462    }
3463
3464    #[test]
3465    fn understand_v3_style_unquoted_decimal_unicode() {
3466        let font = Font::load(&glyphs3_dir().join("Unicode-UnquotedDec.glyphs")).unwrap();
3467        assert_eq!(
3468            BTreeSet::from([182]),
3469            font.glyphs.get("name").unwrap().unicode
3470        );
3471        assert_eq!(1, font.glyphs.len());
3472    }
3473
3474    #[test]
3475    fn understand_v3_style_unquoted_decimal_unicode_sequence() {
3476        let font = Font::load(&glyphs3_dir().join("Unicode-UnquotedDecSequence.glyphs")).unwrap();
3477        assert_eq!(
3478            BTreeSet::from([1619, 1764]),
3479            font.glyphs.get("name").unwrap().unicode,
3480        );
3481        assert_eq!(1, font.glyphs.len());
3482    }
3483
3484    #[test]
3485    fn axes_not_hidden() {
3486        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
3487        assert_eq!(
3488            font.axes.iter().map(|a| a.hidden).collect::<Vec<_>>(),
3489            vec![None]
3490        );
3491    }
3492
3493    #[test]
3494    fn axis_hidden() {
3495        let font = Font::load(&glyphs3_dir().join("WghtVar_3master_CustomOrigin.glyphs")).unwrap();
3496        assert_eq!(
3497            font.axes.iter().map(|a| a.hidden).collect::<Vec<_>>(),
3498            vec![Some(true)]
3499        );
3500    }
3501
3502    #[test]
3503    fn vf_origin_single_axis_default() {
3504        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
3505        assert_eq!(0, font.default_master_idx);
3506    }
3507
3508    #[test]
3509    fn vf_origin_multi_axis_default() {
3510        let font = Font::load(&glyphs2_dir().join("WghtVar_ImplicitAxes.glyphs")).unwrap();
3511        assert_eq!(0, font.default_master_idx);
3512    }
3513
3514    #[test]
3515    fn vf_origin_multi_axis_custom() {
3516        let font = Font::load(&glyphs3_dir().join("WghtVar_3master_CustomOrigin.glyphs")).unwrap();
3517        assert_eq!(2, font.default_master_idx);
3518    }
3519
3520    #[test]
3521    fn vf_origin_unquoted_string() {
3522        // the 'Variable Font Origin' custom parameter has the value `m01`,
3523        // un un-quoted plist string, which happens to be the default master.id
3524        // that Glyphs.app assigns to the predefined 'Regular' master that any
3525        // "New Font" comes with when it is first crated.
3526        // We just test that we do not crash attempting to parse the unquoted
3527        // string as an integer.
3528        let font = Font::load(&glyphs3_dir().join("CustomOrigin.glyphs")).unwrap();
3529        assert_eq!(1, font.default_master_idx);
3530    }
3531
3532    #[rstest]
3533    #[case::base_style_without_regular(
3534        &[
3535            "Expanded Thin Italic",
3536            "Expanded Italic",
3537            "Expanded Bold Italic",
3538        ],
3539        "Expanded Italic"  // is common and exactly matches [1]
3540    )]
3541    #[case::base_style_contains_regular(
3542        &[
3543            "Regular Foo Bar",
3544            "Regular Foo Baz",
3545            "Regular Foo",
3546        ],
3547        "Regular Foo" // is common and exactly matches [2]
3548    )]
3549    #[case::base_style_with_regular_omitted(
3550        &[
3551            "Condensed Thin",
3552            "Condensed Light",
3553            "Condensed Regular",
3554        ],
3555        // "Condensed" is common and matches "Condensed Regular" when "Regular" is ignored
3556        "Condensed Regular"
3557    )]
3558    // "" is common and matches "Regular when "Regular" is ignored
3559    #[case::default_to_regular(
3560        &["Thin", "Light", "Regular", "Medium", "Bold"],
3561        "Regular"
3562    )]
3563    // "" is common, nothing matches, just take the first
3564    #[case::default_to_first(&["Foo", "Bar", "Baz"], "Foo")]
3565    fn find_default_master(#[case] master_names: &[&str], #[case] expected: &str) {
3566        let mut font = RawFont::default();
3567        for name in master_names {
3568            let master = RawFontMaster {
3569                name: Some(name.to_string()),
3570                ..Default::default()
3571            };
3572            font.font_master.push(master);
3573        }
3574
3575        let idx = default_master_idx(&font);
3576
3577        assert_eq!(expected, font.font_master[idx].name.as_deref().unwrap());
3578    }
3579
3580    #[test]
3581    fn glyph_order_default_is_file_order() {
3582        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
3583        assert_eq!(
3584            vec![
3585                "space",
3586                "exclam",
3587                "hyphen",
3588                "bracketleft",
3589                "bracketright",
3590                "manual-component"
3591            ],
3592            font.glyph_order
3593        );
3594    }
3595
3596    #[test]
3597    fn glyph_order_override_obeyed() {
3598        let _ = env_logger::builder().is_test(true).try_init();
3599        let font = Font::load(&glyphs3_dir().join("WghtVar_GlyphOrder.glyphs")).unwrap();
3600        assert_eq!(vec!["hyphen", "space", "exclam"], font.glyph_order);
3601    }
3602
3603    #[test]
3604    fn loads_global_axis_mappings_from_glyphs2() {
3605        let font = Font::load(&glyphs2_dir().join("OpszWghtVar_AxisMappings.glyphs")).unwrap();
3606
3607        // Did you load the mappings? DID YOU?!
3608        assert_eq!(
3609            UserToDesignMapping(BTreeMap::from([
3610                (
3611                    "Optical Size".into(),
3612                    AxisUserToDesignMap(vec![
3613                        (OrderedFloat(12.0), OrderedFloat(12.0)),
3614                        (OrderedFloat(72.0), OrderedFloat(72.0))
3615                    ])
3616                ),
3617                (
3618                    "Weight".into(),
3619                    AxisUserToDesignMap(vec![
3620                        (OrderedFloat(100.0), OrderedFloat(40.0)),
3621                        (OrderedFloat(200.0), OrderedFloat(46.0)),
3622                        (OrderedFloat(300.0), OrderedFloat(51.0)),
3623                        (OrderedFloat(400.0), OrderedFloat(57.0)),
3624                        (OrderedFloat(450.0), OrderedFloat(57.0)), // duplicate value is valid
3625                        (OrderedFloat(500.0), OrderedFloat(62.0)),
3626                        (OrderedFloat(600.0), OrderedFloat(68.0)),
3627                        (OrderedFloat(700.0), OrderedFloat(73.0)),
3628                    ])
3629                ),
3630            ])),
3631            font.axis_mappings
3632        );
3633    }
3634
3635    #[test]
3636    fn loads_global_axis_locations_from_glyphs3() {
3637        let font = Font::load(&glyphs3_dir().join("WghtVar_AxisLocation.glyphs")).unwrap();
3638
3639        // Did you load the mappings? DID YOU?!
3640        assert_eq!(
3641            UserToDesignMapping(BTreeMap::from([(
3642                "Weight".into(),
3643                AxisUserToDesignMap(vec![
3644                    (OrderedFloat(400.0), OrderedFloat(0.0)),
3645                    (OrderedFloat(500.0), OrderedFloat(8.0)),
3646                    (OrderedFloat(700.0), OrderedFloat(10.0)),
3647                    // this comes from the 'SemiBold' instance's 'Axis Location'
3648                    (OrderedFloat(600.0), OrderedFloat(8.5)),
3649                    // the 'SemiMedium' instance contains no explict 'Axis Location' so
3650                    // it doesn't show up (implicit mappings are ignored in this case)
3651                    // (OrderedFloat(450.0), OrderedFloat(4.0)),
3652                ])
3653            ),])),
3654            font.axis_mappings
3655        );
3656    }
3657
3658    #[test]
3659    fn loads_global_axis_mappings_from_instances_wght_glyphs3() {
3660        let font = Font::load(&glyphs3_dir().join("WghtVar_Avar_From_Instances.glyphs")).unwrap();
3661
3662        let wght_idx = font.axes.iter().position(|a| a.tag == "wght").unwrap();
3663        assert_eq!(
3664            vec![60.0, 80.0, 132.0],
3665            font.masters
3666                .iter()
3667                .map(|m| m.axes_values[wght_idx].into_inner())
3668                .collect::<Vec<_>>()
3669        );
3670        // the default master is the 'Bold' in this test font
3671        assert_eq!(
3672            (132.0, 2),
3673            (
3674                font.default_master().axes_values[wght_idx].into_inner(),
3675                font.default_master_idx
3676            )
3677        );
3678
3679        // Did you load the mappings? DID YOU?!
3680        assert_eq!(
3681            UserToDesignMapping(BTreeMap::from([(
3682                "Weight".into(),
3683                AxisUserToDesignMap(vec![
3684                    (OrderedFloat(300.0), OrderedFloat(60.0)),
3685                    // we expect a map 400:80 here, even though the 'Regular' instance's
3686                    // Weight Class property is omitted in the .glyphs source because it
3687                    // is equal to its default value (400):
3688                    // https://github.com/googlefonts/fontc/issues/905
3689                    (OrderedFloat(400.0), OrderedFloat(80.0)),
3690                    (OrderedFloat(500.0), OrderedFloat(100.0)),
3691                    (OrderedFloat(700.0), OrderedFloat(132.0)),
3692                ])
3693            ),])),
3694            font.axis_mappings
3695        );
3696    }
3697
3698    #[test]
3699    fn loads_global_axis_mappings_from_instances_wdth_glyphs3() {
3700        let font = Font::load(&glyphs3_dir().join("WdthVar.glyphs")).unwrap();
3701
3702        assert_eq!(font.axes.len(), 1);
3703        assert_eq!(font.axes[0].tag, "wdth");
3704        assert_eq!(
3705            vec![22.0, 62.0],
3706            font.masters
3707                .iter()
3708                .map(|m| m.axes_values[0].into_inner())
3709                .collect::<Vec<_>>()
3710        );
3711        // the default master is the 'Condensed' in this test font
3712        assert_eq!(
3713            (22.0, 0),
3714            (
3715                font.default_master().axes_values[0].into_inner(),
3716                font.default_master_idx
3717            )
3718        );
3719        // Did you load the mappings? DID YOU?!
3720        assert_eq!(
3721            UserToDesignMapping(BTreeMap::from([(
3722                "Width".into(),
3723                AxisUserToDesignMap(vec![
3724                    // The "1: Ultra-condensed" instance width class corresponds to a
3725                    // `wdth` of 50 (user-space), in turn mapped to 22 (design-space).
3726                    (OrderedFloat(50.0), OrderedFloat(22.0)),
3727                    // We expect a map 100:41 here, even though the 'Regular' instance's
3728                    // Width Class property is omitted in the .glyphs source because it
3729                    // is equal to its default value "5: Medium (normal)" (or wdth=100):
3730                    // https://github.com/googlefonts/fontc/issues/905
3731                    (OrderedFloat(100.0), OrderedFloat(41.0)),
3732                    // The "9: Ultra-expanded" instance width class corresponds to a
3733                    // `wdth` of 200 (user-space), in turn mapped to 62 (design-space).
3734                    (OrderedFloat(200.0), OrderedFloat(62.0)),
3735                ])
3736            ),])),
3737            font.axis_mappings
3738        );
3739    }
3740
3741    #[test]
3742    fn fea_for_class() {
3743        let font = Font::load(&glyphs2_dir().join("Fea_Class.glyphs")).unwrap();
3744        assert_eq!(
3745            vec![
3746                concat!("# automatic\n", "@Uppercase = [ A B C\n", "];",),
3747                concat!("@Lowercase = [ a b c\n", "];",),
3748            ],
3749            font.features
3750                .iter()
3751                .filter_map(|f| f.str_if_enabled())
3752                .collect::<Vec<_>>()
3753        )
3754    }
3755
3756    #[test]
3757    fn fea_for_prefix() {
3758        let font = Font::load(&glyphs2_dir().join("Fea_Prefix.glyphs")).unwrap();
3759        assert_eq!(
3760            vec![
3761                "\
3762# Prefix: Languagesystems
3763# automatic
3764languagesystem DFLT dflt;
3765
3766languagesystem latn dflt;
3767and more;
3768",
3769                "# Prefix: \n# automatic\nthanks for all the fish;",
3770            ],
3771            font.features
3772                .iter()
3773                .filter_map(|f| f.str_if_enabled())
3774                .collect::<Vec<_>>()
3775        )
3776    }
3777
3778    #[test]
3779    fn fea_for_feature() {
3780        let font = Font::load(&glyphs2_dir().join("Fea_Feature.glyphs")).unwrap();
3781        assert_eq!(
3782            vec![
3783                "\
3784feature aalt {
3785feature locl;
3786feature tnum;
3787} aalt;",
3788                "\
3789feature ccmp {
3790# automatic
3791lookup ccmp_Other_2 {
3792  sub @Markscomb' @MarkscombCase by @MarkscombCase;
3793  sub @MarkscombCase @Markscomb' by @MarkscombCase;
3794} ccmp_Other_2;
3795
3796etc;
3797} ccmp;",
3798            ],
3799            font.features
3800                .iter()
3801                .filter_map(|f| f.str_if_enabled())
3802                .collect::<Vec<_>>()
3803        )
3804    }
3805
3806    #[test]
3807    fn fea_order() {
3808        let font = Font::load(&glyphs2_dir().join("Fea_Order.glyphs")).unwrap();
3809        assert_eq!(
3810            vec![
3811                "@class_first = [ meh\n];",
3812                "# Prefix: second\nmeh",
3813                "feature third {\nmeh\n} third;",
3814            ],
3815            font.features
3816                .iter()
3817                .filter_map(|f| f.str_if_enabled())
3818                .collect::<Vec<_>>()
3819        )
3820    }
3821
3822    #[test]
3823    fn fea_labels() {
3824        let font = Font::load(&glyphs3_dir().join("Fea_Labels.glyphs")).unwrap();
3825        assert_eq!(
3826            vec![
3827                concat!(
3828                    "feature ss01 {\n",
3829                    "# automatic\n",
3830                    "featureNames {\n",
3831                    "  name 3 1 0x0409 \"Test 1\";\n",
3832                    "  name 3 1 0x0C01 \"اختبار ١\";\n",
3833                    "};\n",
3834                    "sub a by a.ss01;\n",
3835                    "sub b by b.ss01;\n\n",
3836                    "} ss01;",
3837                ),
3838                concat!(
3839                    "feature ss02 {\n",
3840                    "featureNames {\n",
3841                    "  name 3 1 0x0409 \"Test 2\";\n",
3842                    "};\n",
3843                    "sub c by c.alt;\n",
3844                    "} ss02;",
3845                ),
3846            ],
3847            font.features
3848                .iter()
3849                .filter_map(|f| f.str_if_enabled())
3850                .collect::<Vec<_>>()
3851        )
3852    }
3853
3854    #[test]
3855    fn tags_make_excellent_names() {
3856        let raw = RawFeature {
3857            tag: Some("aalt".to_string()),
3858            code: "blah".to_string(),
3859            ..Default::default()
3860        };
3861        assert_eq!("aalt", raw.name().unwrap());
3862    }
3863
3864    #[test]
3865    fn manual_kern_always_gets_insert_mark() {
3866        let feature = RawFeature {
3867            tag: Some("kern".into()),
3868            ..Default::default()
3869        };
3870
3871        assert!(feature
3872            .raw_feature_to_feature()
3873            .unwrap()
3874            .content
3875            .contains("# Automatic Code"))
3876    }
3877
3878    #[test]
3879    fn but_automatic_does_not() {
3880        let feature = RawFeature {
3881            tag: Some("kern".into()),
3882            automatic: Some(1),
3883            ..Default::default()
3884        };
3885
3886        assert!(!feature
3887            .raw_feature_to_feature()
3888            .unwrap()
3889            .content
3890            .contains("# Automatic Code"))
3891    }
3892
3893    #[test]
3894    fn v2_to_v3_simple_names() {
3895        let v2 = Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap();
3896        let v3 = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
3897        assert_eq!(v3.names, v2.names);
3898    }
3899
3900    #[test]
3901    fn v2_to_v3_more_names() {
3902        let v2 = Font::load(&glyphs2_dir().join("TheBestNames.glyphs")).unwrap();
3903        let v3 = Font::load(&glyphs3_dir().join("TheBestNames.glyphs")).unwrap();
3904        assert_eq!(v3.names, v2.names);
3905    }
3906
3907    #[test]
3908    fn v2_long_param_names() {
3909        let v2 = Font::load(&glyphs2_dir().join("LongParamNames.glyphs")).unwrap();
3910        assert_eq!(v2.names.get("vendorID").cloned().as_deref(), Some("DERP"));
3911        assert_eq!(
3912            v2.names.get("descriptions").cloned().as_deref(),
3913            Some("legacy description")
3914        );
3915        assert_eq!(
3916            v2.names.get("licenseURL").cloned().as_deref(),
3917            Some("www.example.com/legacy")
3918        );
3919        assert_eq!(
3920            v2.names.get("version").cloned().as_deref(),
3921            Some("legacy version")
3922        );
3923        assert_eq!(
3924            v2.names.get("licenses").cloned().as_deref(),
3925            Some("legacy license")
3926        );
3927        assert_eq!(
3928            v2.names.get("uniqueID").cloned().as_deref(),
3929            Some("legacy unique id")
3930        );
3931        assert_eq!(
3932            v2.names.get("sampleTexts").cloned().as_deref(),
3933            Some("legacy sample text")
3934        );
3935    }
3936
3937    #[test]
3938    fn v2_style_names_in_a_v3_file() {
3939        let v3_mixed_with_v2 =
3940            Font::load(&glyphs3_dir().join("TheBestV2NamesInAV3File.glyphs")).unwrap();
3941        let v3 = Font::load(&glyphs3_dir().join("TheBestNames.glyphs")).unwrap();
3942        assert_eq!(v3.names, v3_mixed_with_v2.names);
3943    }
3944
3945    fn assert_wghtvar_avar_master_and_axes(glyphs_file: &Path) {
3946        let font = Font::load(glyphs_file).unwrap();
3947        let wght_idx = font.axes.iter().position(|a| a.tag == "wght").unwrap();
3948        assert_eq!(
3949            vec![300.0, 400.0, 700.0],
3950            font.masters
3951                .iter()
3952                .map(|m| m.axes_values[wght_idx].into_inner())
3953                .collect::<Vec<_>>()
3954        );
3955        assert_eq!(
3956            (400.0, 1),
3957            (
3958                font.default_master().axes_values[wght_idx].into_inner(),
3959                font.default_master_idx
3960            )
3961        );
3962    }
3963
3964    #[test]
3965    fn favor_regular_as_origin_glyphs2() {
3966        assert_wghtvar_avar_master_and_axes(&glyphs2_dir().join("WghtVar_Avar.glyphs"));
3967    }
3968
3969    #[test]
3970    fn favor_regular_as_origin_glyphs3() {
3971        assert_wghtvar_avar_master_and_axes(&glyphs3_dir().join("WghtVar_Avar.glyphs"));
3972    }
3973
3974    #[test]
3975    fn have_all_the_best_instances() {
3976        let font = Font::load(&glyphs3_dir().join("WghtVar_Instances.glyphs")).unwrap();
3977        assert_eq!(
3978            vec![
3979                ("Regular", vec![("Weight", 400.0)]),
3980                ("Bold", vec![("Weight", 700.0)])
3981            ],
3982            font.instances
3983                .iter()
3984                .map(|inst| (
3985                    inst.name.as_str(),
3986                    font.axes
3987                        .iter()
3988                        .zip(&inst.axes_values)
3989                        .map(|(a, v)| (a.name.as_str(), v.0))
3990                        .collect::<Vec<_>>()
3991                ))
3992                .collect::<Vec<_>>()
3993        );
3994    }
3995
3996    #[test]
3997    fn read_typo_whatsits() {
3998        let font = Font::load(&glyphs2_dir().join("WghtVar_OS2.glyphs")).unwrap();
3999        let master = font.default_master();
4000        assert_eq!(Some(1193), master.custom_parameters.typo_ascender);
4001        assert_eq!(Some(-289), master.custom_parameters.typo_descender);
4002    }
4003
4004    #[test]
4005    fn read_os2_flags_default_set() {
4006        let font = Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap();
4007        assert_eq!(font.custom_parameters.use_typo_metrics, Some(true));
4008        assert_eq!(font.custom_parameters.has_wws_names, Some(true));
4009    }
4010
4011    #[test]
4012    fn read_os2_flags_default_unset() {
4013        let font = Font::load(&glyphs2_dir().join("WghtVar_OS2.glyphs")).unwrap();
4014        assert_eq!(font.custom_parameters.use_typo_metrics, None);
4015        assert_eq!(font.custom_parameters.has_wws_names, None);
4016    }
4017
4018    #[test]
4019    fn read_simple_kerning() {
4020        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
4021        assert_eq!(
4022            HashSet::from(["m01", "E09E0C54-128D-4FEA-B209-1B70BEFE300B",]),
4023            font.kerning_ltr
4024                .keys()
4025                .map(|k| k.as_str())
4026                .collect::<HashSet<_>>()
4027        );
4028
4029        let actual_groups: Vec<_> = font
4030            .glyphs
4031            .iter()
4032            .filter_map(|(name, glyph)| {
4033                if glyph.left_kern.is_some() || glyph.right_kern.is_some() {
4034                    Some((
4035                        name.as_str(),
4036                        glyph.left_kern.as_deref(),
4037                        glyph.right_kern.as_deref(),
4038                    ))
4039                } else {
4040                    None
4041                }
4042            })
4043            .collect();
4044
4045        let actual_kerning = font
4046            .kerning_ltr
4047            .get("m01")
4048            .unwrap()
4049            .iter()
4050            .map(|((n1, n2), value)| (n1.as_str(), n2.as_str(), value.0))
4051            .collect::<Vec<_>>();
4052
4053        assert_eq!(
4054            (
4055                vec![
4056                    ("bracketleft", Some("bracketleft_L"), Some("bracketleft_R")),
4057                    (
4058                        "bracketright",
4059                        Some("bracketright_L"),
4060                        Some("bracketright_R")
4061                    ),
4062                ],
4063                vec![
4064                    ("@MMK_L_bracketleft_R", "exclam", -165.),
4065                    ("bracketleft", "bracketright", -300.),
4066                    ("exclam", "@MMK_R_bracketright_L", -160.),
4067                    ("exclam", "exclam", -360.),
4068                    ("exclam", "hyphen", 20.),
4069                    ("hyphen", "hyphen", -150.),
4070                ],
4071            ),
4072            (actual_groups, actual_kerning),
4073            "{:?}",
4074            font.kerning_ltr
4075        );
4076    }
4077
4078    #[test]
4079    fn kern_floats() {
4080        let font = Font::load(&glyphs3_dir().join("KernFloats.glyphs")).unwrap();
4081
4082        let kerns = font.kerning_ltr.get("m01").unwrap();
4083        let key = ("space".to_smolstr(), "space".to_smolstr());
4084        assert_eq!(kerns.get(&key), Some(&OrderedFloat(4.2001)));
4085    }
4086
4087    #[test]
4088    fn read_simple_anchor() {
4089        let font = Font::load(&glyphs3_dir().join("WghtVar_Anchors.glyphs")).unwrap();
4090        assert_eq!(
4091            vec![
4092                ("m01", "top", Point::new(300.0, 700.0)),
4093                ("l2", "top", Point::new(325.0, 725.0))
4094            ],
4095            font.glyphs
4096                .get("A")
4097                .unwrap()
4098                .layers
4099                .iter()
4100                .flat_map(|l| l.anchors.iter().map(|a| (
4101                    l.layer_id.as_str(),
4102                    a.name.as_str(),
4103                    a.pos
4104                )))
4105                .collect::<Vec<_>>()
4106        );
4107    }
4108
4109    #[test]
4110    fn read_export_glyph() {
4111        let font = Font::load(&glyphs3_dir().join("WghtVar_NoExport.glyphs")).unwrap();
4112        assert_eq!(
4113            vec![
4114                ("bracketleft", true),
4115                ("bracketright", true),
4116                ("exclam", true),
4117                ("hyphen", false),
4118                ("manual-component", true),
4119                ("space", true),
4120            ],
4121            font.glyphs
4122                .iter()
4123                .map(|(name, glyph)| (name.as_str(), glyph.export))
4124                .collect::<Vec<_>>()
4125        );
4126    }
4127
4128    #[test]
4129    fn read_fstype_none() {
4130        let font = Font::load(&glyphs3_dir().join("infinity.glyphs")).unwrap();
4131        assert!(font.custom_parameters.fs_type.is_none());
4132    }
4133
4134    #[test]
4135    fn read_fstype_zero() {
4136        let font = Font::load(&glyphs3_dir().join("fstype_0x0000.glyphs")).unwrap();
4137        assert_eq!(Some(0), font.custom_parameters.fs_type);
4138    }
4139
4140    #[test]
4141    fn read_fstype_bits() {
4142        let font = Font::load(&glyphs3_dir().join("fstype_0x0104.glyphs")).unwrap();
4143        assert_eq!(Some(0x104), font.custom_parameters.fs_type);
4144    }
4145
4146    #[test]
4147    fn anchor_components() {
4148        let font = Font::load(&glyphs3_dir().join("ComponentAnchor.glyphs")).unwrap();
4149        let glyph = font.glyphs.get("A_Aacute").unwrap();
4150        let acute_comb = glyph.layers[0]
4151            .shapes
4152            .iter()
4153            .find_map(|shape| match shape {
4154                Shape::Component(c) if c.name == "acutecomb" => Some(c),
4155                _ => None,
4156            })
4157            .unwrap();
4158        assert_eq!(acute_comb.anchor.as_deref(), Some("top_2"));
4159    }
4160
4161    #[test]
4162    fn parse_alignment_zone_smoke_test() {
4163        assert_eq!(
4164            super::parse_alignment_zone("{1, -12}").map(|x| (x.0 .0, x.1 .0)),
4165            Some((1., -12.))
4166        );
4167        assert_eq!(
4168            super::parse_alignment_zone("{-5001, 12}").map(|x| (x.0 .0, x.1 .0)),
4169            Some((-5001., 12.))
4170        );
4171    }
4172
4173    // a little helper used in tests below
4174    impl FontMaster {
4175        fn get_metric(&self, name: &str) -> Option<(f64, f64)> {
4176            self.metric_values
4177                .get(name)
4178                .map(|raw| (raw.pos.0, raw.over.0))
4179        }
4180    }
4181
4182    #[test]
4183    fn v2_alignment_zones_to_metrics() {
4184        let font = Font::load(&glyphs2_dir().join("alignment_zones_v2.glyphs")).unwrap();
4185        let master = font.default_master();
4186
4187        assert_eq!(master.get_metric("ascender"), Some((800., 17.)));
4188        assert_eq!(master.get_metric("cap height"), Some((700., 16.)));
4189        assert_eq!(master.get_metric("baseline"), Some((0., -16.)));
4190        assert_eq!(master.get_metric("descender"), Some((-200., -17.)));
4191        assert_eq!(master.get_metric("x-height"), Some((500., 15.)));
4192        assert_eq!(master.get_metric("italic angle"), None);
4193    }
4194
4195    #[test]
4196    fn v3_duplicate_metrics_first_wins() {
4197        // In this test font, the default master contains two 'x-height' metric values,
4198        // the first (501) that applies globally, and a second one (450) that applies
4199        // only to small-caps, using GSMetric's `filter` attribute which we ignore as
4200        // it is not relevant to build OS/2 and MVAR tables.
4201        // We match glyphsLib and only consider the first metric with a given name.
4202        let font = Font::load(&glyphs3_dir().join("WghtVar_OS2.glyphs")).unwrap();
4203        let master = font.default_master();
4204
4205        assert_eq!(master.get_metric("x-height"), Some((501., 0.)));
4206    }
4207
4208    #[test]
4209    fn v2_preserve_custom_alignment_zones() {
4210        let font = Font::load(&glyphs2_dir().join("alignment_zones_v2.glyphs")).unwrap();
4211        let master = font.default_master();
4212        assert_eq!(master.get_metric("zone 1"), Some((1000., 20.)));
4213        assert_eq!(master.get_metric("zone 2"), Some((-100., -15.)));
4214    }
4215
4216    // If category is unknown, we should ignore and compute it
4217    #[test]
4218    fn unknown_glyph_category() {
4219        let raw = super::RawGlyph {
4220            glyphname: "A".into(),
4221            category: Some("Fake".into()),
4222            ..Default::default()
4223        };
4224
4225        let cooked = raw.build(FormatVersion::V2, &GlyphData::default()).unwrap();
4226        assert_eq!(
4227            (cooked.category, cooked.sub_category),
4228            (Some(Category::Letter), None)
4229        );
4230    }
4231
4232    #[test]
4233    fn custom_params_disable() {
4234        let font = Font::load(&glyphs3_dir().join("custom_param_disable.glyphs")).unwrap();
4235
4236        assert!(font.custom_parameters.fs_type.is_none())
4237    }
4238
4239    #[test]
4240    fn parse_numbers() {
4241        let font = Font::load(&glyphs3_dir().join("number_value.glyphs")).unwrap();
4242        assert_eq!(
4243            font.masters[0].number_values.get("foo"),
4244            Some(&OrderedFloat(12.4f64))
4245        );
4246        assert_eq!(
4247            font.masters[1].number_values.get("foo"),
4248            Some(&OrderedFloat(0f64))
4249        );
4250    }
4251
4252    #[test]
4253    fn read_font_metrics() {
4254        let font =
4255            Font::load(&glyphs3_dir().join("GlobalMetrics_font_customParameters.glyphs")).unwrap();
4256        assert_eq!(Some(950), font.custom_parameters.typo_ascender);
4257        assert_eq!(Some(-350), font.custom_parameters.typo_descender);
4258        assert_eq!(Some(0), font.custom_parameters.typo_line_gap);
4259        assert_eq!(Some(950), font.custom_parameters.hhea_ascender);
4260        assert_eq!(Some(-350), font.custom_parameters.hhea_descender);
4261        assert_eq!(Some(0), font.custom_parameters.hhea_line_gap);
4262        assert_eq!(Some(1185), font.custom_parameters.win_ascent);
4263        assert_eq!(Some(420), font.custom_parameters.win_descent);
4264        assert_eq!(
4265            Some(OrderedFloat(42_f64)),
4266            font.custom_parameters.underline_thickness
4267        );
4268        assert_eq!(
4269            Some(OrderedFloat(-300_f64)),
4270            font.custom_parameters.underline_position
4271        );
4272    }
4273
4274    #[test]
4275    fn parse_legacy_stylistic_set_name() {
4276        let font = Font::load(&glyphs2_dir().join("FeaLegacyName.glyphs")).unwrap();
4277        assert_eq!(font.features.len(), 2);
4278        let [ss01, ss02] = font.features.as_slice() else {
4279            panic!("wrong number of features");
4280        };
4281
4282        assert!(ss01
4283            .content
4284            .contains("name 3 1 0x409 \"Alternate placeholder\""));
4285        assert!(!ss02.content.contains("name 3 1"))
4286    }
4287
4288    // <https://github.com/googlefonts/fontc/issues/1175>
4289    #[test]
4290    fn one_italic_is_enough() {
4291        let font = Font::load(&glyphs2_dir().join("ItalicItalic.glyphs")).unwrap();
4292        for master in font.masters {
4293            let mut fragments = master.name.split_ascii_whitespace().collect::<Vec<_>>();
4294            fragments.sort();
4295            for words in fragments.windows(2) {
4296                assert!(
4297                    words[0] != words[1],
4298                    "Multiple instances of {} in {}",
4299                    words[0],
4300                    master.name
4301                );
4302            }
4303        }
4304    }
4305
4306    // We had a bug where if a master wasn't at a mapping point the Axis Mapping was modified
4307    #[test]
4308    fn ignore_masters_if_axis_mapping() {
4309        let font = Font::load(&glyphs2_dir().join("MasterNotMapped.glyphs")).unwrap();
4310        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
4311        assert_eq!(
4312            vec![
4313                (OrderedFloat(400_f64), OrderedFloat(40.0)),
4314                (OrderedFloat(700_f64), OrderedFloat(70.0))
4315            ],
4316            *mapping
4317        );
4318    }
4319
4320    #[rstest]
4321    #[case::rotate_0(0.0, Affine::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]))]
4322    #[case::rotate_360(360.0, Affine::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]))]
4323    #[case::rotate_90(90.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
4324    #[case::rotate_minus_90(-90.0, Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]))]
4325    #[case::rotate_180(180.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
4326    #[case::rotate_minus_180(-180.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
4327    #[case::rotate_270(270.0, Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]))]
4328    #[case::rotate_minus_270(-270.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
4329    #[case::rotate_450(450.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
4330    #[case::rotate_540(540.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
4331    fn cardinal_rotation_contain_exact_zeros_and_ones(
4332        #[case] angle: f64,
4333        #[case] expected: Affine,
4334    ) {
4335        // When Glyphs 3 components are rotated by a 90, 180 or 270 degree angle,
4336        // we want to match fontTools Transform.rotate() and have the sine and
4337        // cosine terms rounded exactly to 0.0 or ±1.0 to avoid unnecessary diffs
4338        // upon rounding transformed coordinates to integer:
4339        // https://github.com/googlefonts/fontc/issues/1218
4340        assert_eq!(expected, normalized_rotation(angle));
4341    }
4342
4343    #[rstest]
4344    #[case::rotate_30(30.0, 4, Affine::new([0.866, 0.5, -0.5, 0.866, 0.0, 0.0]))]
4345    #[case::rotate_minus_30(-30.0, 4, Affine::new([0.866, -0.5, 0.5, 0.866, 0.0, 0.0]))]
4346    #[case::rotate_almost_90(
4347        90.01, 8, Affine::new([-0.00017453, 0.99999998, -0.99999998, -0.00017453, 0.0, 0.0])
4348    )]
4349    fn non_cardinal_rotation_left_untouched(
4350        #[case] angle: f64,
4351        #[case] precision: u8,
4352        #[case] expected: Affine,
4353    ) {
4354        // any other angles' sin and cos != (0, ±1) are passed through unchanged
4355        assert_eq!(expected, round(normalized_rotation(angle), precision));
4356    }
4357
4358    #[test]
4359    fn parse_colrv1_identify_colr_glyphs() {
4360        let font = Font::load(&glyphs3_dir().join("COLRv1-simple.glyphs")).unwrap();
4361        let expected_colr = HashSet::from(["A", "B", "C", "D", "K", "L", "M", "N"]);
4362        assert_eq!(
4363            expected_colr,
4364            font.glyphs
4365                .values()
4366                .filter(|g| g.layers.iter().all(|l| l.attributes.color))
4367                .map(|g| g.name.as_str())
4368                .collect::<HashSet<_>>()
4369        );
4370    }
4371
4372    #[test]
4373    fn parse_colrv1_gradients() {
4374        let font = Font::load(&glyphs3_dir().join("COLRv1-simple.glyphs")).unwrap();
4375        let expected_colr = HashSet::from([
4376            (
4377                "A",
4378                Gradient {
4379                    start: vec![OrderedFloat(0.1), OrderedFloat(0.1)],
4380                    end: vec![OrderedFloat(0.9), OrderedFloat(0.9)],
4381                    colors: vec![
4382                        Color {
4383                            r: 255,
4384                            g: 0,
4385                            b: 0,
4386                            a: 255,
4387                            stop_offset: 0.into(),
4388                        },
4389                        Color {
4390                            r: 0,
4391                            g: 0,
4392                            b: 255,
4393                            a: 255,
4394                            stop_offset: 1.into(),
4395                        },
4396                    ],
4397                    ..Default::default()
4398                },
4399            ),
4400            (
4401                "N",
4402                Gradient {
4403                    start: vec![OrderedFloat(1.0), OrderedFloat(1.0)],
4404                    end: vec![OrderedFloat(0.0), OrderedFloat(0.0)],
4405                    colors: vec![
4406                        Color {
4407                            r: 255,
4408                            g: 0,
4409                            b: 0,
4410                            a: 255,
4411                            stop_offset: 0.into(),
4412                        },
4413                        Color {
4414                            r: 0,
4415                            g: 0,
4416                            b: 255,
4417                            a: 255,
4418                            stop_offset: 1.into(),
4419                        },
4420                    ],
4421                    style: "circle".to_string(),
4422                },
4423            ),
4424        ]);
4425        assert_eq!(
4426            expected_colr,
4427            font.glyphs
4428                .values()
4429                .filter(|g| expected_colr.iter().any(|(name, _)| *name == g.name))
4430                .flat_map(|g| g
4431                    .layers
4432                    .iter()
4433                    .flat_map(|l| l.shapes.iter())
4434                    .map(|s| (g.name.as_str(), s.attributes().gradient.clone())))
4435                .collect::<HashSet<_>>()
4436        );
4437    }
4438
4439    #[test]
4440    fn parse_grayscale_colors() {
4441        let font = Font::load(&glyphs3_dir().join("COLRv1-grayscale.glyphs")).unwrap();
4442        assert_eq!(
4443            vec![
4444                Color {
4445                    r: 64,
4446                    g: 64,
4447                    b: 64,
4448                    a: 255,
4449                    stop_offset: 0.into(),
4450                },
4451                Color {
4452                    r: 0,
4453                    g: 0,
4454                    b: 0,
4455                    a: 255,
4456                    stop_offset: 1.into(),
4457                },
4458            ],
4459            font.glyphs
4460                .values()
4461                .flat_map(|g| g
4462                    .layers
4463                    .iter()
4464                    .flat_map(|l| l.shapes.iter())
4465                    .flat_map(|s| (s.attributes().gradient.colors.iter().cloned())))
4466                .collect::<Vec<_>>()
4467        );
4468    }
4469
4470    // we want to match glyphsLib and use a value of 600 if the width is missing.
4471    #[test]
4472    fn missing_width_no_problem() {
4473        let font = Font::load(&glyphs3_dir().join("MissingWidth.glyphs")).unwrap();
4474        let glyph = font.glyphs.get("widthless").unwrap();
4475        assert_eq!(glyph.layers[0].width, 600.);
4476    }
4477
4478    #[test]
4479    fn read_preferred_names_from_properties() {
4480        let font = Font::load(&glyphs3_dir().join("PreferableNames.glyphs")).unwrap();
4481        assert_eq!(
4482            vec![
4483                Some("Pref Family Name"),
4484                Some("Pref Regular"),
4485                Some("Name 25?!")
4486            ],
4487            vec![
4488                font.names.get("preferredFamilyNames").map(|s| s.as_str()),
4489                font.names
4490                    .get("preferredSubfamilyNames")
4491                    .map(|s| s.as_str()),
4492                font.names
4493                    .get("variationsPostScriptNamePrefix")
4494                    .map(|s| s.as_str()),
4495            ]
4496        );
4497    }
4498
4499    #[test]
4500    fn zero_value_metrics() {
4501        let font = Font::load(&glyphs3_dir().join("ZeroMetrics.glyphs")).unwrap();
4502        let master = font.default_master();
4503        assert_eq!(master.ascender(), Some(789.));
4504        // this has no value set, but has overshoot set, so this should be a 0
4505        assert_eq!(master.cap_height(), Some(0.));
4506        // this has neither value nor overshoot, but since the metric is defined,
4507        // it's still here and still zero
4508        assert_eq!(master.x_height(), Some(0.));
4509        // but this metric is not defined in the font, so it it's `None`
4510        assert_eq!(master.italic_angle(), None);
4511    }
4512
4513    /// If there is a variable instance and it does not have a set familyName,
4514    /// we should still use names stored in that instance.
4515    #[test]
4516    fn names_from_instances() {
4517        let font = Font::load(&glyphs3_dir().join("InstanceNames.glyphs")).unwrap();
4518        assert_eq!(font.names.get("preferredSubfamilyNames").unwrap(), "Italic")
4519    }
4520
4521    #[rstest]
4522    #[case::v2(glyphs2_dir())]
4523    #[case::v3(glyphs3_dir())]
4524    fn glyph_production_names(#[case] glyphs_dir: PathBuf) {
4525        let font = Font::load(&glyphs_dir.join("ProductionNames.glyphs")).unwrap();
4526        let glyphs = font.glyphs;
4527
4528        // this glyph has no production name in GlyphData.xml nor in the .glyphs file
4529        assert_eq!(glyphs.get("A").unwrap().production_name, None);
4530
4531        // this one would have 'dotlessi' in GlyphData.xml, but the .glyphs file overrides it
4532        assert_eq!(
4533            glyphs.get("idotless").unwrap().production_name,
4534            Some("uni0131".into())
4535        );
4536
4537        // this has no custom production_name in the .glyphs file, and the one from
4538        // GlyphData.xml is used
4539        assert_eq!(
4540            glyphs.get("nbspace").unwrap().production_name,
4541            Some("uni00A0".into())
4542        );
4543    }
4544
4545    #[test]
4546    fn parse_axis_rules() {
4547        let raw = RawFont::load(&glyphs3_dir().join("AxisRules.glyphs")).unwrap();
4548        let space = &raw.glyphs[0];
4549        let axes = &space.layers[0].attributes.axis_rules;
4550        assert_eq!(
4551            axes,
4552            &[
4553                AxisRule {
4554                    min: None,
4555                    max: Some(400)
4556                },
4557                AxisRule {
4558                    min: Some(100),
4559                    max: None,
4560                },
4561                AxisRule {
4562                    min: None,
4563                    max: None,
4564                },
4565            ]
4566        )
4567    }
4568
4569    #[test]
4570    fn parse_layer_normal() {
4571        for name in &["[60]", "Light Extended [ 60 ]"] {
4572            let rule = AxisRule::from_layer_name(name);
4573            assert_eq!(
4574                rule,
4575                Some(AxisRule {
4576                    min: Some(60),
4577                    max: None
4578                }),
4579                "{name}"
4580            )
4581        }
4582    }
4583
4584    #[test]
4585    fn parse_layer_reversed() {
4586        for name in &["]60]", "Light Extended ] 60 ]"] {
4587            let rule = AxisRule::from_layer_name(name);
4588            assert_eq!(
4589                rule,
4590                Some(AxisRule {
4591                    min: None,
4592                    max: Some(60)
4593                })
4594            )
4595        }
4596    }
4597
4598    #[test]
4599    fn parse_layer_fails() {
4600        for name in &["[hi]", "[45opsz]", "Medium [499‹wg]"] {
4601            assert!(AxisRule::from_layer_name(name).is_none(), "{name}")
4602        }
4603    }
4604
4605    #[test]
4606    fn v2_bracket_layers() {
4607        let font = Font::load(&glyphs2_dir().join("WorkSans-minimal-bracketlayer.glyphs")).unwrap();
4608        let glyph = font.glyphs.get("colonsign").unwrap();
4609        assert_eq!(glyph.layers.len(), 3);
4610        assert_eq!(glyph.bracket_layers.len(), 3);
4611
4612        assert!(glyph
4613            .layers
4614            .iter()
4615            .all(|l| l.attributes.axis_rules.is_empty()));
4616        assert!(glyph
4617            .bracket_layers
4618            .iter()
4619            .all(|l| !l.attributes.axis_rules.is_empty()));
4620    }
4621
4622    #[test]
4623    fn v3_bracket_layers() {
4624        let font = Font::load(&glyphs3_dir().join("LibreFranklin-bracketlayer.glyphs")).unwrap();
4625        let glyph = font.glyphs.get("peso").unwrap();
4626        assert_eq!(glyph.layers.len(), 2);
4627        assert_eq!(glyph.bracket_layers.len(), 2);
4628
4629        assert!(glyph
4630            .layers
4631            .iter()
4632            .all(|l| l.attributes.axis_rules.is_empty()));
4633        assert!(glyph
4634            .bracket_layers
4635            .iter()
4636            .all(|l| !l.attributes.axis_rules.is_empty()));
4637    }
4638
4639    #[test]
4640    fn bracket_layers_where_only_brackets_have_a_component_and_it_has_anchors() {
4641        let font = Font::load(&glyphs2_dir().join("AlumniSans-wononly.glyphs")).unwrap();
4642        let glyph = font.glyphs.get("won").unwrap();
4643
4644        assert_eq!(glyph.layers.len(), 2);
4645        assert_eq!(glyph.bracket_layers.len(), 2);
4646
4647        for layer in glyph.layers.iter().chain(glyph.bracket_layers.iter()) {
4648            assert_eq!(layer.anchors.len(), 2, "{}", layer.layer_id);
4649        }
4650    }
4651
4652    #[test]
4653    fn glyphs2_weight_class_custom_instance_parameter() {
4654        // older glyphs sources can use a custom param in the instance
4655        // in order to pass a custom value for the weight class.
4656        let font = Font::load(&glyphs2_dir().join("WeightClassCustomParam.glyphs")).unwrap();
4657        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
4658        assert_eq!(
4659            mapping.as_slice(),
4660            &[
4661                (OrderedFloat(200f64), OrderedFloat(42f64)),
4662                (OrderedFloat(700.), OrderedFloat(125.)),
4663                (OrderedFloat(1000.), OrderedFloat(208.))
4664            ]
4665        )
4666    }
4667
4668    #[test]
4669    fn glyphs2_avoid_adding_mapping_from_master() {
4670        // this source has a master which does not correspond to an instance,
4671        // but the instances provide a valid non-identity mapping; we should
4672        // avoid adding the mapping from the master.
4673
4674        let font = Font::load(&glyphs2_dir().join("master_missing_from_instances.glyphs")).unwrap();
4675
4676        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
4677        assert_eq!(
4678            mapping.as_slice(),
4679            &[
4680                (OrderedFloat(100f64), OrderedFloat(20f64)),
4681                (OrderedFloat(300.), OrderedFloat(50.)),
4682                (OrderedFloat(400.), OrderedFloat(71.)),
4683                (OrderedFloat(700.), OrderedFloat(156.)),
4684                (OrderedFloat(900.), OrderedFloat(226.)),
4685            ]
4686        )
4687    }
4688
4689    #[test]
4690    fn does_not_truncate_axis_location() {
4691        let font = Font::load(&glyphs3_dir().join("WghtVar_AxisLocationFloat.glyphs")).unwrap();
4692        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
4693
4694        // The user location 400.75 must *NOT* be truncated
4695        assert_eq!(
4696            mapping.as_slice(),
4697            &[(OrderedFloat(400.75), OrderedFloat(0f64)),]
4698        )
4699    }
4700}