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