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::ops::RangeInclusive;
12use std::str::FromStr;
13use std::sync::{LazyLock, Mutex};
14use std::{fs, path};
15
16use crate::glyphdata::{Category, GlyphData, Subcategory};
17use ascii_plist_derive::FromPlist;
18use fontdrasil::types::WidthClass;
19use indexmap::{IndexMap, IndexSet};
20use kurbo::{Affine, CubicBez, Line, PathSeg, Point, QuadBez, Vec2};
21use log::{debug, warn};
22use ordered_float::OrderedFloat;
23use regex::Regex;
24use smol_str::SmolStr;
25
26use crate::error::Error;
27use crate::plist::{FromPlist, Plist, Token, Tokenizer, VecDelimiters};
28
29// Static set to track warnings we've already logged, to avoid repetition
30static LOGGED_WARNINGS: LazyLock<Mutex<HashSet<String>>> =
31    LazyLock::new(|| Mutex::new(HashSet::new()));
32
33/// Log a warning message only once per process lifetime
34macro_rules! log_once_warn {
35    ($($arg:tt)*) => {{
36        if log::log_enabled!(log::Level::Warn) {
37            let msg = format!($($arg)*);
38            let mut logged = LOGGED_WARNINGS.lock().unwrap();
39            if !logged.contains(&msg) {
40                log::warn!("{}", msg);
41                logged.insert(msg);
42            }
43        }
44    }};
45}
46
47const V3_METRIC_NAMES: [&str; 6] = [
48    "ascender",
49    "baseline",
50    "descender",
51    "cap height",
52    "x-height",
53    "italic angle",
54];
55
56#[derive(Clone, Debug, Default, PartialEq, Hash)]
57pub struct UserToDesignMapping(BTreeMap<String, AxisUserToDesignMap>);
58
59#[derive(Clone, Debug, Default, PartialEq, Hash)]
60pub struct AxisUserToDesignMap(Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>);
61
62/// A tidied up font from a plist.
63///
64/// Normalized representation of Glyphs 2/3 content
65#[derive(Clone, Debug, Default, PartialEq, Hash)]
66pub struct Font {
67    pub units_per_em: u16,
68    pub axes: Vec<Axis>,
69    pub masters: Vec<FontMaster>,
70    pub default_master_idx: usize,
71    pub glyphs: BTreeMap<SmolStr, Glyph>,
72    pub glyph_order: Vec<SmolStr>,
73    // tag => (user:design) tuples
74    pub axis_mappings: UserToDesignMapping,
75    pub virtual_masters: Vec<BTreeMap<String, OrderedFloat<f64>>>,
76    pub features: Vec<FeatureSnippet>,
77    // Incliudes both default (English) and localized property names.
78    // Maps property key (e.g., "familyNames") to list of (language code, value)
79    pub all_names: BTreeMap<String, Vec<(String, String)>>,
80    pub instances: Vec<Instance>,
81    pub version_major: i32,
82    pub version_minor: u32,
83    pub date: Option<String>,
84
85    // master id => { (name or class, name or class) => adjustment }
86    pub kerning_ltr: Kerning,
87    pub kerning_rtl: Kerning,
88
89    pub custom_parameters: CustomParameters,
90}
91
92/// Custom parameter options that can be set on a glyphs font
93#[derive(Clone, Debug, PartialEq, Hash, Default)]
94pub struct CustomParameters {
95    pub propagate_anchors: Option<bool>,
96    pub use_typo_metrics: Option<bool>,
97    pub is_fixed_pitch: Option<bool>,
98    pub fs_type: Option<u16>,
99    pub has_wws_names: Option<bool>,
100    pub typo_ascender: Option<i64>,
101    pub typo_descender: Option<i64>,
102    pub typo_line_gap: Option<i64>,
103    pub win_ascent: Option<i64>,
104    pub win_descent: Option<i64>,
105    pub hhea_ascender: Option<i64>,
106    pub hhea_descender: Option<i64>,
107    pub hhea_line_gap: Option<i64>,
108    pub vhea_ascender: Option<i64>,
109    pub vhea_descender: Option<i64>,
110    pub vhea_line_gap: Option<i64>,
111    pub underline_thickness: Option<OrderedFloat<f64>>,
112    pub underline_position: Option<OrderedFloat<f64>>,
113    pub strikeout_position: Option<i64>,
114    pub strikeout_size: Option<i64>,
115    pub subscript_x_offset: Option<i64>,
116    pub subscript_x_size: Option<i64>,
117    pub subscript_y_offset: Option<i64>,
118    pub subscript_y_size: Option<i64>,
119    pub superscript_x_offset: Option<i64>,
120    pub superscript_x_size: Option<i64>,
121    pub superscript_y_offset: Option<i64>,
122    pub superscript_y_size: Option<i64>,
123    pub unicode_range_bits: Option<BTreeSet<u32>>,
124    pub codepage_range_bits: Option<BTreeSet<u32>>,
125    pub panose: Option<Vec<i64>>,
126
127    pub lowest_rec_ppem: Option<i64>,
128    pub hhea_caret_slope_run: Option<i64>,
129    pub hhea_caret_slope_rise: Option<i64>,
130    pub hhea_caret_offset: Option<i64>,
131    pub vhea_caret_slope_run: Option<i64>,
132    pub vhea_caret_slope_rise: Option<i64>,
133    pub vhea_caret_offset: Option<i64>,
134    pub meta_table: Option<MetaTableValues>,
135    pub dont_use_production_names: Option<bool>,
136    // Master-level parameters for linking metrics to another master
137    pub link_metrics_with_first_master: Option<bool>,
138    pub link_metrics_with_master: Option<SmolStr>,
139    // these fields are parsed via the config, but are stored
140    // in the top-level `Font` struct
141    pub virtual_masters: Option<Vec<BTreeMap<String, OrderedFloat<f64>>>>,
142    pub glyph_order: Option<Vec<SmolStr>>,
143    pub gasp_table: Option<BTreeMap<i64, i64>>,
144    pub feature_for_feature_variations: Option<SmolStr>,
145    pub color_palettes: Option<Vec<Vec<Color>>>,
146}
147
148/// Values for the 'meta Table' custom parameter
149#[derive(Clone, Debug, PartialEq, Hash, Default)]
150pub struct MetaTableValues {
151    pub dlng: Vec<SmolStr>,
152    pub slng: Vec<SmolStr>,
153}
154
155impl MetaTableValues {
156    fn from_plist(plist: &Plist) -> Option<Self> {
157        let mut ret = MetaTableValues::default();
158        let as_array = plist.as_array()?;
159        for item in as_array {
160            let tag = item.get("tag").and_then(Plist::as_str)?;
161            let data = item.get("data").and_then(Plist::as_str)?;
162            let data = data.split(',').map(SmolStr::new).collect();
163
164            match tag {
165                "dlng" => ret.dlng = data,
166                "slng" => ret.slng = data,
167                _ => log_once_warn!("Unknown meta table tag '{tag}'"),
168            }
169        }
170
171        if ret.dlng.len() + ret.slng.len() > 0 {
172            Some(ret)
173        } else {
174            None
175        }
176    }
177}
178
179/// master id => { (name or class, name or class) => adjustment }
180#[derive(Clone, Debug, Default, PartialEq, Hash)]
181pub struct Kerning(BTreeMap<String, BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>>);
182
183impl Kerning {
184    pub fn get(&self, master_id: &str) -> Option<&BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>> {
185        self.0.get(master_id)
186    }
187
188    pub fn keys(&self) -> impl Iterator<Item = &String> {
189        self.0.keys()
190    }
191
192    pub fn iter(
193        &self,
194    ) -> impl Iterator<Item = (&String, &BTreeMap<(SmolStr, SmolStr), OrderedFloat<f64>>)> {
195        self.0.iter()
196    }
197
198    fn insert(
199        &mut self,
200        master_id: String,
201        lhs_class_or_group: SmolStr,
202        rhs_class_or_group: SmolStr,
203        kern: f64,
204    ) {
205        *self
206            .0
207            .entry(master_id)
208            .or_default()
209            .entry((lhs_class_or_group, rhs_class_or_group))
210            .or_default() = kern.into();
211    }
212}
213
214/// Hand-parse because it's a bit weird
215impl FromPlist for Kerning {
216    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
217        let mut kerning = Kerning::default();
218
219        tokenizer.eat(b'{')?;
220
221        loop {
222            if tokenizer.eat(b'}').is_ok() {
223                break;
224            }
225
226            // parse string-that-is-master-id = {
227            let master_id: String = tokenizer.parse()?;
228            tokenizer.eat(b'=')?;
229
230            // The map for the master
231            tokenizer.eat(b'{')?;
232            loop {
233                if tokenizer.eat(b'}').is_ok() {
234                    break;
235                }
236                let lhs_name_or_class: SmolStr = tokenizer.parse()?;
237                tokenizer.eat(b'=')?;
238                tokenizer.eat(b'{')?;
239
240                // rhs name = value pairs
241                loop {
242                    if tokenizer.eat(b'}').is_ok() {
243                        break;
244                    }
245
246                    let rhs_name_or_class: SmolStr = tokenizer.parse()?;
247                    tokenizer.eat(b'=')?;
248                    let value: f64 = tokenizer.parse()?;
249                    tokenizer.eat(b';')?;
250
251                    kerning.insert(
252                        master_id.clone(),
253                        lhs_name_or_class.clone(),
254                        rhs_name_or_class,
255                        value,
256                    );
257                }
258                tokenizer.eat(b';')?;
259            }
260
261            tokenizer.eat(b';')?;
262        }
263
264        Ok(kerning)
265    }
266}
267
268/// A chunk of FEA code
269#[derive(Clone, Debug, PartialEq, Eq, Hash)]
270pub struct FeatureSnippet {
271    pub content: String,
272    /// If `true` the content should be ignored.
273    pub disabled: bool,
274}
275
276impl FeatureSnippet {
277    pub fn new(content: String, disabled: bool) -> Self {
278        FeatureSnippet { content, disabled }
279    }
280
281    pub fn str_if_enabled(&self) -> Option<&str> {
282        (!self.disabled).then_some(&self.content)
283    }
284}
285
286#[derive(Clone, Default, Debug, PartialEq, Hash)]
287pub struct Glyph {
288    pub name: SmolStr,
289    pub export: bool,
290    pub layers: Vec<Layer>,
291    pub bracket_layers: Vec<Layer>,
292    pub unicode: BTreeSet<u32>,
293    /// The left kerning group
294    pub left_kern: Option<SmolStr>,
295    /// The right kerning group
296    pub right_kern: Option<SmolStr>,
297    pub category: Option<Category>,
298    pub sub_category: Option<Subcategory>,
299    pub production_name: Option<SmolStr>,
300    /// If this is a smart component, these are the axe names -> user coords
301    pub smart_component_axes: BTreeMap<SmolStr, RangeInclusive<i64>>,
302}
303
304impl Glyph {
305    pub fn is_nonspacing_mark(&self) -> bool {
306        matches!(
307            (self.category, self.sub_category),
308            (Some(Category::Mark), Some(Subcategory::Nonspacing))
309        )
310    }
311
312    pub(crate) fn has_components(&self) -> bool {
313        self.layers
314            .iter()
315            .chain(self.bracket_layers.iter())
316            .flat_map(Layer::components)
317            .next()
318            .is_some()
319    }
320}
321
322/// Whether a given layer of a smart component is at the min or max of a given axis.
323#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
324pub enum AxisPole {
325    /// this layer is at the minimum value for this property
326    Min,
327    /// this layer is at the maximum value for this property
328    Max,
329}
330
331/// A hint in a Glyphs layer (e.g., corner component, stem hint, etc.)
332#[derive(Clone, Debug, Default, PartialEq, FromPlist)]
333pub struct RawHint {
334    #[fromplist(alt_name = "type")]
335    hint_type: SmolStr,
336    name: SmolStr,
337    origin: RawHint2Tuple, // (shape_index, node_index)
338    scale: Option<RawHint2Tuple>,
339    options: i64, // Alignment option for corner components
340}
341
342// we use a custom type because the data has a different shape between
343// glyphs formats
344#[derive(Clone, Copy, Debug, Default, PartialEq)]
345pub struct RawHint2Tuple {
346    x: f64,
347    y: f64,
348}
349
350impl FromPlist for RawHint2Tuple {
351    fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
352        if matches!(tokenizer.peek(), Ok(Token::String(_))) {
353            let s = tokenizer.parse::<SmolStr>().unwrap();
354            // format2 is a string, like "{0, 3}"
355            let s = s.trim_matches(['{', '}']);
356            let Some((x, y)) = s.split_once(',') else {
357                return Err(crate::plist::Error::ExpectedComma);
358            };
359            let x = x
360                .trim()
361                .parse()
362                .map_err(|_| crate::plist::Error::ExpectedNumber)?;
363            let y = y
364                .trim()
365                .parse()
366                .map_err(|_| crate::plist::Error::ExpectedNumber)?;
367            return Ok(RawHint2Tuple { x, y });
368        }
369
370        let origin: Vec<f64> = tokenizer.parse()?;
371        Ok(RawHint2Tuple {
372            x: origin.first().copied().unwrap_or_default(),
373            y: origin.get(1).copied().unwrap_or_default(),
374        })
375    }
376}
377
378impl RawHint {
379    fn to_hint(&self) -> Result<Hint, Error> {
380        let type_ = match self.hint_type.as_str() {
381            "Corner" => HintType::Corner,
382            "Cap" => HintType::Cap,
383            "Stem" => HintType::Stem,
384            "TTStem" => HintType::TTStem,
385            _other => {
386                log::info!("unhandled hint type '{_other}'");
387                HintType::Unknown
388            }
389        };
390
391        let shape_index = self.origin.x as usize;
392        let node_index = self.origin.y as usize;
393
394        let scale = self
395            .scale
396            .map(|RawHint2Tuple { x, y }| Scale::new(x, y))
397            .unwrap_or_default();
398
399        let alignment = match self.options {
400            0 => Alignment::OutStroke,
401            1 => Alignment::InStroke,
402            2 => Alignment::Middle,
403            3 => Alignment::Unused,
404            4 => Alignment::Unaligned,
405            _other => {
406                // doesn't seem worth it to fail for this, easy to imagine
407                // new variants added in the future
408                log::info!("unexpected hint option '{_other}'");
409                Default::default()
410            }
411        };
412
413        Ok(Hint {
414            type_,
415            name: self.name.clone(),
416            shape_index,
417            node_index,
418            scale,
419            alignment,
420        })
421    }
422}
423
424#[derive(Debug, Clone, PartialEq, Hash)]
425pub struct Hint {
426    pub type_: HintType,
427    pub name: SmolStr,
428    pub shape_index: usize,
429    pub node_index: usize,
430    pub scale: Scale,
431    pub alignment: Alignment,
432}
433
434#[derive(Debug, Clone, PartialEq, Hash)]
435pub struct Scale {
436    pub x: OrderedFloat<f64>,
437    pub y: OrderedFloat<f64>,
438}
439
440impl Default for Scale {
441    fn default() -> Self {
442        Self {
443            x: 1.0.into(),
444            y: 1.0.into(),
445        }
446    }
447}
448
449impl Scale {
450    fn new(x: f64, y: f64) -> Self {
451        Self {
452            x: x.into(),
453            y: y.into(),
454        }
455    }
456}
457
458#[derive(Debug, Clone, Copy, PartialEq, Hash)]
459#[non_exhaustive]
460pub enum HintType {
461    Corner,
462    // other hint types are currently unused
463    Stem,
464    Cap,
465    TTStem,
466    // catch all, we don't want to fail parsing unknwon variants,
467    Unknown,
468}
469
470/// How to align a corner component to the attaching node
471#[derive(Debug, Clone, Copy, Default, PartialEq, Hash)]
472#[non_exhaustive]
473pub enum Alignment {
474    // glyphs calls this 'left'
475    #[default]
476    OutStroke,
477    // glyphs calls this 'right'
478    InStroke,
479    Middle,
480    Unused,
481    Unaligned,
482}
483
484#[derive(Debug, Default, Clone, PartialEq, Hash)]
485pub struct Layer {
486    pub layer_id: String,
487    pub associated_master_id: Option<String>,
488    pub width: OrderedFloat<f64>,
489    pub vert_width: Option<OrderedFloat<f64>>,
490    pub vert_origin: Option<OrderedFloat<f64>>,
491    pub shapes: Vec<Shape>,
492    pub anchors: Vec<Anchor>,
493    pub attributes: LayerAttributes,
494    /// If this layer is part of a smart component, this defines its location.
495    ///
496    /// Smart component layers can only be defined at the min or max of a given
497    /// axis.
498    pub smart_component_positions: BTreeMap<SmolStr, AxisPole>,
499    /// Hints for this layer (e.g., corner components, stem hints, etc.)
500    pub hints: Vec<Hint>,
501}
502
503impl Layer {
504    pub fn is_master(&self) -> bool {
505        self.associated_master_id.is_none()
506    }
507
508    /// associated master id if set, else layer id
509    pub fn master_id(&self) -> &str {
510        self.associated_master_id
511            .as_deref()
512            .unwrap_or(&self.layer_id)
513    }
514
515    /// A key used to identify a bracket layer during anchor propagation
516    pub(crate) fn axis_rules_key(&self) -> Option<String> {
517        (!self.attributes.axis_rules.is_empty())
518            .then(|| format!("{:?} {}", self.attributes.axis_rules, self.master_id()))
519    }
520
521    pub fn is_intermediate(&self) -> bool {
522        self.associated_master_id.is_some() && !self.attributes.coordinates.is_empty()
523    }
524
525    pub fn is_color(&self) -> bool {
526        self.attributes.color || self.attributes.color_palette.is_some()
527    }
528
529    pub(crate) fn components(&self) -> impl Iterator<Item = &Component> + '_ {
530        self.shapes.iter().filter_map(|shape| match shape {
531            Shape::Path(_) => None,
532            Shape::Component(comp) => Some(comp),
533        })
534    }
535
536    /// NOTE: panics if called on a non-bracket layer
537    fn bracket_info(&self, axes: &[Axis]) -> BTreeMap<String, (Option<i64>, Option<i64>)> {
538        assert!(
539            !self.attributes.axis_rules.is_empty(),
540            "all bracket layers have axis rules"
541        );
542        axes.iter()
543            .zip(&self.attributes.axis_rules)
544            .map(|(axis, rule)| (axis.tag.clone(), (rule.min, rule.max)))
545            .collect()
546    }
547
548    pub(crate) fn get_anchor_pt(&self, anchor_name: &str) -> Option<Point> {
549        self.anchors
550            .iter()
551            .find(|a| a.name == anchor_name)
552            .map(|a| a.pos)
553    }
554    // TODO add is_alternate, is_color, etc.
555}
556
557#[derive(Clone, Default, Debug, PartialEq, Hash)]
558pub struct LayerAttributes {
559    pub coordinates: Vec<OrderedFloat<f64>>,
560    pub color: bool,
561    // in the same order that axes are declared for the font
562    pub axis_rules: Vec<AxisRule>,
563    pub color_palette: Option<i64>,
564}
565
566#[derive(Clone, Default, FromPlist, Debug, PartialEq, Hash)]
567pub struct AxisRule {
568    // if missing, assume default min/max for font
569    pub min: Option<i64>,
570    pub max: Option<i64>,
571}
572
573impl AxisRule {
574    /// Parse an AxisRule from a glyphs v2 layer name.
575    ///
576    /// See <https://glyphsapp.com/learn/alternating-glyph-shapes> for an
577    /// overview of the naming conventions.
578    fn from_layer_name(name: &str) -> Option<Self> {
579        // the pattern can either be "$name [100]" or "$name ]100]"
580        // (python uses a regex for this:)
581        // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d/Lib/glyphsLib/classes.py#L3938
582        let idx = name.find([']', '['])?;
583        let reversed = name.as_bytes()[idx] == b']';
584        let tail = name.get(idx + 1..)?;
585        let (value, _) = tail.split_once(']')?;
586        let value = str::parse::<u32>(value.trim()).ok()?;
587        let (min, max) = if reversed {
588            (None, Some(value as _))
589        } else {
590            (Some(value as _), None)
591        };
592        Some(AxisRule { min, max })
593    }
594}
595
596// hand-parse because they can take multiple shapes
597impl FromPlist for LayerAttributes {
598    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
599        let mut coordinates = Vec::new();
600        let mut color = false;
601        let mut axis_rules = Vec::new();
602        let mut color_palette = None;
603
604        tokenizer.eat(b'{')?;
605
606        loop {
607            if tokenizer.eat(b'}').is_ok() {
608                break;
609            }
610
611            let key: String = tokenizer.parse()?;
612            tokenizer.eat(b'=')?;
613            match key.as_str() {
614                "coordinates" => coordinates = tokenizer.parse()?,
615                "color" => color = tokenizer.parse()?,
616                "axisRules" => axis_rules = tokenizer.parse()?,
617                "colorPalette" => color_palette = Some(tokenizer.parse()?),
618                // skip unsupported attributes for now
619                // TODO: match the others
620                _ => tokenizer.skip_rec()?,
621            }
622            tokenizer.eat(b';')?;
623        }
624
625        Ok(LayerAttributes {
626            coordinates,
627            color,
628            axis_rules,
629            color_palette,
630        })
631    }
632}
633
634#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
635pub struct ShapeAttributes {
636    pub gradient: Option<Gradient>,
637    pub fill_color: Option<Color>,
638}
639
640impl ShapeAttributes {
641    pub fn colors(&self) -> impl Iterator<Item = &Color> {
642        self.gradient
643            .iter()
644            .flat_map(|g| g.colors())
645            .chain(self.fill_color.iter())
646    }
647}
648
649#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
650pub struct Gradient {
651    pub start: Vec<OrderedFloat<f64>>,
652    pub end: Vec<OrderedFloat<f64>>,
653    pub colors: Vec<ColorStop>,
654    #[fromplist(key = "type")]
655    pub style: String,
656}
657
658impl Gradient {
659    fn colors(&self) -> impl Iterator<Item = &Color> {
660        self.colors.iter().map(|c| &c.color)
661    }
662}
663
664#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
665pub struct Color {
666    pub r: i64,
667    pub g: i64,
668    pub b: i64,
669    pub a: i64,
670}
671
672#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
673pub struct ColorStop {
674    pub color: Color,
675    // The position on the color line, see <https://learn.microsoft.com/en-us/typography/opentype/spec/colr#color-lines>
676    pub stop_offset: OrderedFloat<f64>,
677}
678
679impl Color {
680    pub fn rgba(r: i64, g: i64, b: i64, a: i64) -> Self {
681        Self { r, g, b, a }
682    }
683
684    pub(crate) fn from_glyphs_color(colors: &[i64]) -> Result<Self, crate::plist::Error> {
685        // See <https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/common.py#L41-L50>
686        match *colors {
687            // Grayscale
688            [black, alpha] => Ok(Color::rgba(black, black, black, alpha)),
689            // RGBA
690            [r, g, b, a] => Ok(Color::rgba(r, g, b, a)),
691            // 5 is CMYK, match python by not supporting that
692            _ => Err(crate::plist::Error::UnexpectedNumberOfValues {
693                value_type: "grayscale (2) or rgba (4)",
694                actual: colors.len(),
695            }),
696        }
697    }
698}
699
700// hand-parse because it's a list of inconsistent types
701impl FromPlist for Color {
702    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
703        let colors = tokenizer.parse::<Vec<i64>>()?;
704        Color::from_glyphs_color(&colors)
705    }
706}
707
708// hand-parse because it's a list of inconsistent types
709impl FromPlist for ColorStop {
710    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
711        tokenizer.eat(b'(')?;
712
713        let color = Color::parse(tokenizer)?;
714        tokenizer.eat(b',')?;
715        let stop_offset = tokenizer.parse::<f64>()?;
716        tokenizer.eat(b')')?;
717
718        Ok(ColorStop {
719            color,
720            stop_offset: stop_offset.into(),
721        })
722    }
723}
724
725#[derive(Debug, Clone, PartialEq, Hash)]
726pub enum Shape {
727    Path(Path),
728    Component(Component),
729}
730
731impl Shape {
732    pub fn attributes(&self) -> &ShapeAttributes {
733        match self {
734            Shape::Path(p) => &p.attributes,
735            Shape::Component(c) => &c.attributes,
736        }
737    }
738
739    pub(crate) fn as_path(&self) -> Option<&Path> {
740        match self {
741            Shape::Path(path) => Some(path),
742            _ => None,
743        }
744    }
745
746    pub(crate) fn as_smart_component(&self) -> Option<&Component> {
747        match self {
748            Shape::Component(component) if !component.smart_component_values.is_empty() => {
749                Some(component)
750            }
751            _ => None,
752        }
753    }
754
755    /// If the shape is a path, reverse it
756    pub(crate) fn reverse(&mut self) {
757        if let Shape::Path(path) = self {
758            path.reverse();
759        }
760    }
761}
762
763#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
764enum FormatVersion {
765    #[default]
766    V2,
767    V3,
768}
769
770// The font you get directly from a plist, minimally modified
771// Types chosen specifically to accomodate plist translation.
772#[derive(Default, Debug, PartialEq, FromPlist)]
773#[allow(non_snake_case)]
774struct RawFont {
775    #[fromplist(key = ".formatVersion")]
776    format_version: FormatVersion,
777    units_per_em: Option<i64>,
778    metrics: Vec<RawMetric>,
779    family_name: String,
780    date: Option<String>,
781    copyright: Option<String>,
782    designer: Option<String>,
783    designerURL: Option<String>,
784    manufacturer: Option<String>,
785    manufacturerURL: Option<String>,
786    versionMajor: Option<i64>,
787    versionMinor: Option<i64>,
788    axes: Vec<Axis>,
789    glyphs: Vec<RawGlyph>,
790    font_master: Vec<RawFontMaster>,
791    instances: Vec<RawInstance>,
792    feature_prefixes: Vec<RawFeature>,
793    features: Vec<RawFeature>,
794    classes: Vec<RawFeature>,
795    properties: Vec<RawName>,
796    #[fromplist(alt_name = "kerning")]
797    kerning_LTR: Kerning,
798    kerning_RTL: Kerning,
799    custom_parameters: RawCustomParameters,
800    numbers: Vec<NumberName>,
801}
802
803#[derive(Default, Debug, PartialEq, FromPlist)]
804struct NumberName {
805    name: SmolStr,
806}
807
808// we use a vec of tuples instead of a map because there can be multiple
809// values for the same name (e.g. 'Virtual Master')
810#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)]
811pub(crate) struct RawCustomParameters(Vec<RawCustomParameterValue>);
812
813#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, FromPlist)]
814struct RawCustomParameterValue {
815    name: SmolStr,
816    value: Plist,
817    disabled: Option<bool>,
818}
819
820impl RawCustomParameterValue {
821    /// Returns true if this parameter has been taken
822    ///
823    /// We use a special sentinel value to indicate that a parameter has been handled
824    /// specially and shouldn't be parsed as/stored in CustomParameters.
825    fn taken(&self) -> bool {
826        self == &Self::default()
827    }
828}
829
830impl FromPlist for FormatVersion {
831    fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
832        let raw: i64 = FromPlist::parse(tokenizer)?;
833        if raw == 3 {
834            Ok(FormatVersion::V3)
835        } else {
836            // format version 2 is the default value, if no explicit version is set
837            Err(crate::plist::Error::Parse(
838                "'3' is the only known format version".into(),
839            ))
840        }
841    }
842}
843
844impl FormatVersion {
845    fn is_v2(self) -> bool {
846        self == FormatVersion::V2
847    }
848
849    fn codepoint_radix(self) -> u32 {
850        match self {
851            FormatVersion::V2 => 16,
852            FormatVersion::V3 => 10,
853        }
854    }
855}
856
857impl FromPlist for RawCustomParameters {
858    fn parse(tokenizer: &mut Tokenizer) -> Result<Self, crate::plist::Error> {
859        Vec::parse(tokenizer).map(RawCustomParameters)
860    }
861}
862
863/// Convenience methods for converting `Plist` structs to concrete custom param types
864trait PlistParamsExt {
865    fn as_codepage_bits(&self) -> Option<BTreeSet<u32>>;
866    fn as_unicode_code_ranges(&self) -> Option<BTreeSet<u32>>;
867    fn as_fs_type(&self) -> Option<u16>;
868    fn as_mapping_values(&self) -> Option<Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>>;
869    fn as_axis_location(&self) -> Option<AxisLocation>;
870    fn as_axis(&self) -> Option<Axis>;
871    fn as_bool(&self) -> Option<bool>;
872    fn as_ordered_f64(&self) -> Option<OrderedFloat<f64>>;
873    fn as_vec_of_ints(&self) -> Option<Vec<i64>>;
874    fn as_axis_locations(&self) -> Option<Vec<AxisLocation>>;
875    fn as_vec_of_string(&self) -> Option<Vec<SmolStr>>;
876    fn as_axes(&self) -> Option<Vec<Axis>>;
877    fn as_axis_mappings(&self) -> Option<Vec<AxisMapping>>;
878    fn as_virtual_master(&self) -> Option<BTreeMap<String, OrderedFloat<f64>>>;
879    fn as_gasp_table(&self) -> Option<BTreeMap<i64, i64>>;
880    fn as_color_palettes(&self) -> Option<Vec<Vec<Color>>>;
881}
882
883impl PlistParamsExt for Plist {
884    fn as_bool(&self) -> Option<bool> {
885        self.as_i64().map(|val| val == 1)
886    }
887
888    fn as_ordered_f64(&self) -> Option<OrderedFloat<f64>> {
889        self.as_f64().map(OrderedFloat)
890    }
891
892    fn as_vec_of_ints(&self) -> Option<Vec<i64>> {
893        // exciting! apparently glyphs (at least sometimes?) serializes
894        // this as a vec of strings? (this is true in WghtVar_OS2.glyphs)
895        if let Some(thing_that_makes_sense) = self.as_array()?.iter().map(Plist::as_i64).collect() {
896            return Some(thing_that_makes_sense);
897        }
898
899        self.as_array()?
900            .iter()
901            .map(|val| val.as_str().and_then(|s| s.parse::<i64>().ok()))
902            .collect()
903    }
904
905    fn as_axis_location(&self) -> Option<AxisLocation> {
906        let plist = self.as_dict()?;
907        let name = plist.get("Axis").and_then(Plist::as_str)?;
908        let location = plist.get("Location").and_then(Plist::as_f64)?;
909        Some(AxisLocation {
910            axis_name: name.into(),
911            location: location.into(),
912        })
913    }
914
915    fn as_axis_locations(&self) -> Option<Vec<AxisLocation>> {
916        let array = self.as_array()?;
917        array.iter().map(Plist::as_axis_location).collect()
918    }
919
920    fn as_vec_of_string(&self) -> Option<Vec<SmolStr>> {
921        self.as_array()?
922            .iter()
923            .map(|plist| plist.as_str().map(SmolStr::from))
924            .collect()
925    }
926
927    fn as_axis(&self) -> Option<Axis> {
928        let plist = self.as_dict()?;
929        let name = plist.get("Name").and_then(Plist::as_str)?;
930        let tag = plist.get("Tag").and_then(Plist::as_str)?;
931        let hidden = plist.get("hidden").and_then(Plist::as_bool);
932        Some(Axis {
933            name: name.into(),
934            tag: tag.into(),
935            hidden,
936        })
937    }
938
939    fn as_axes(&self) -> Option<Vec<Axis>> {
940        self.as_array()?.iter().map(Plist::as_axis).collect()
941    }
942
943    fn as_mapping_values(&self) -> Option<Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>> {
944        let plist = self.as_dict()?;
945        plist
946            .iter()
947            .map(|(key, value)| {
948                // our keys are strings, but we want to interpret them as floats:
949                let key = key
950                    .parse::<f64>()
951                    .or_else(|_| key.parse::<i64>().map(|i| i as f64))
952                    .ok()?;
953                let val = value.as_f64()?;
954                Some((key.into(), val.into()))
955            })
956            .collect()
957    }
958
959    fn as_axis_mappings(&self) -> Option<Vec<AxisMapping>> {
960        self.as_dict()?
961            .iter()
962            .map(|(tag, mappings)| {
963                let user_to_design = mappings.as_mapping_values()?;
964                Some(AxisMapping {
965                    tag: tag.to_string(),
966                    user_to_design,
967                })
968            })
969            .collect()
970    }
971
972    fn as_virtual_master(&self) -> Option<BTreeMap<String, OrderedFloat<f64>>> {
973        self.as_array()?
974            .iter()
975            .map(Plist::as_axis_location)
976            .map(|loc| loc.map(|loc| (loc.axis_name, loc.location)))
977            .collect()
978    }
979
980    fn as_fs_type(&self) -> Option<u16> {
981        Some(self.as_vec_of_ints()?.iter().map(|bit| 1 << bit).sum())
982    }
983
984    fn as_unicode_code_ranges(&self) -> Option<BTreeSet<u32>> {
985        let bits = self.as_vec_of_ints()?;
986        Some(bits.iter().map(|bit| *bit as u32).collect())
987    }
988
989    fn as_codepage_bits(&self) -> Option<BTreeSet<u32>> {
990        let bits = self.as_vec_of_ints()?;
991        bits.iter()
992            .map(|b| codepage_range_bit(*b as _))
993            .collect::<Result<_, _>>()
994            .ok()
995    }
996
997    fn as_gasp_table(&self) -> Option<BTreeMap<i64, i64>> {
998        Some(
999            self.as_dict()?
1000                .iter()
1001                .filter_map(|(k, v)| k.parse::<i64>().ok().zip(v.as_i64()))
1002                .collect(),
1003        )
1004    }
1005
1006    fn as_color_palettes(&self) -> Option<Vec<Vec<Color>>> {
1007        let raw_palettes = self.as_array()?;
1008        let mut palettes = Vec::with_capacity(raw_palettes.len());
1009        for raw_palette in raw_palettes {
1010            let Some(raw_palette) = raw_palette.as_array() else {
1011                warn!("Non-array color palette, ignored");
1012                continue;
1013            };
1014
1015            let mut palette = Vec::with_capacity(raw_palette.len());
1016            for raw_color in raw_palette {
1017                let Some(raw_color) = raw_color.as_vec_of_ints() else {
1018                    warn!("Non-int-array color palette entry, ignored");
1019                    continue;
1020                };
1021                let Ok(color) = Color::from_glyphs_color(&raw_color) else {
1022                    warn!("Invalid color palette entry, ignored");
1023                    continue;
1024                };
1025                palette.push(color);
1026            }
1027
1028            palettes.push(palette);
1029        }
1030        Some(palettes)
1031    }
1032}
1033
1034impl RawCustomParameters {
1035    ////convert into the parsed params for a top-level font
1036    fn to_custom_params(&self) -> Result<CustomParameters, Error> {
1037        let mut params = CustomParameters::default();
1038        let mut virtual_masters = Vec::<BTreeMap<String, OrderedFloat<f64>>>::new();
1039        // PANOSE custom parameter is accessible under a short name and a long name:
1040        //     https://github.com/googlefonts/glyphsLib/blob/050ef62c/Lib/glyphsLib/builder/custom_params.py#L322-L323
1041        // ...with the value under the short name taking precendence:
1042        //     https://github.com/googlefonts/glyphsLib/blob/050ef62c/Lib/glyphsLib/builder/custom_params.py#L258-L269
1043        let mut panose = None;
1044        let mut panose_old = None;
1045
1046        for param in &self.0 {
1047            // Silently skip special parameters that were handled elsewhere
1048            if param.taken() {
1049                continue;
1050            }
1051
1052            let name = &param.name;
1053            let value = &param.value;
1054            let disabled = &param.disabled;
1055
1056            // we need to use a macro here because you can't pass the name of a field to a
1057            // function.
1058            macro_rules! add_and_report_issues {
1059                ($field:ident, $converter:path) => {{ add_and_report_issues!($field, $converter(value)) }};
1060
1061                ($field:ident, $converter:path, into) => {{ add_and_report_issues!($field, $converter(value).map(Into::into)) }};
1062
1063                ($field:ident, $value_expr:expr_2021) => {{
1064                    let value = $value_expr;
1065
1066                    if value.is_none() {
1067                        log::warn!("failed to parse param for '{}'", stringify!($field));
1068                    }
1069                    // Only set if not already set - glyphsLib uses the first occurrence
1070                    // for duplicate params, not the last:
1071                    // <https://github.com/googlefonts/glyphsLib/blob/a5b99adc/Lib/glyphsLib/classes.py#L517-L520>
1072                    if params.$field.is_none() {
1073                        params.$field = value;
1074                    } else {
1075                        log::warn!(
1076                            "ignoring duplicate param value for field '{}'",
1077                            stringify!($field)
1078                        );
1079                    }
1080                }};
1081            }
1082
1083            if *disabled == Some(true) {
1084                log::debug!("skipping disabled custom param '{name}'");
1085                continue;
1086            }
1087            match name.as_str() {
1088                "Propagate Anchors" => add_and_report_issues!(propagate_anchors, Plist::as_bool),
1089                "Use Typo Metrics" => add_and_report_issues!(use_typo_metrics, Plist::as_bool),
1090                // <https://github.com/googlefonts/glyphsLib/blob/52c982399ba20dc96a2c2195df6fc6cea1f9a906/Lib/glyphsLib/builder/custom_params.py#L356>
1091                "postscriptIsFixedPitch" | "isFixedPitch" => {
1092                    add_and_report_issues!(is_fixed_pitch, Plist::as_bool)
1093                }
1094                "Has WWS Names" => add_and_report_issues!(has_wws_names, Plist::as_bool),
1095                "typoAscender" => add_and_report_issues!(typo_ascender, Plist::as_i64),
1096                "typoDescender" => add_and_report_issues!(typo_descender, Plist::as_i64),
1097                "typoLineGap" => add_and_report_issues!(typo_line_gap, Plist::as_i64),
1098                "winAscent" => add_and_report_issues!(win_ascent, Plist::as_i64),
1099                "winDescent" => add_and_report_issues!(win_descent, Plist::as_i64),
1100                "hheaAscender" => add_and_report_issues!(hhea_ascender, Plist::as_i64),
1101                "hheaDescender" => add_and_report_issues!(hhea_descender, Plist::as_i64),
1102                "hheaLineGap" => add_and_report_issues!(hhea_line_gap, Plist::as_i64),
1103                "vheaVertAscender" => add_and_report_issues!(vhea_ascender, Plist::as_i64),
1104                "vheaVertDescender" => add_and_report_issues!(vhea_descender, Plist::as_i64),
1105                "vheaVertLineGap" => add_and_report_issues!(vhea_line_gap, Plist::as_i64),
1106                "underlineThickness" => {
1107                    add_and_report_issues!(underline_thickness, Plist::as_ordered_f64)
1108                }
1109                "underlinePosition" => {
1110                    add_and_report_issues!(underline_position, Plist::as_ordered_f64)
1111                }
1112                "strikeoutPosition" => add_and_report_issues!(strikeout_position, Plist::as_i64),
1113                "strikeoutSize" => add_and_report_issues!(strikeout_size, Plist::as_i64),
1114                "subscriptXOffset" => add_and_report_issues!(subscript_x_offset, Plist::as_i64),
1115                "subscriptXSize" => add_and_report_issues!(subscript_x_size, Plist::as_i64),
1116                "subscriptYOffset" => add_and_report_issues!(subscript_y_offset, Plist::as_i64),
1117                "subscriptYSize" => add_and_report_issues!(subscript_y_size, Plist::as_i64),
1118                "superscriptXOffset" => add_and_report_issues!(superscript_x_offset, Plist::as_i64),
1119                "superscriptXSize" => add_and_report_issues!(superscript_x_size, Plist::as_i64),
1120                "superscriptYOffset" => add_and_report_issues!(superscript_y_offset, Plist::as_i64),
1121                "superscriptYSize" => add_and_report_issues!(superscript_y_size, Plist::as_i64),
1122                "fsType" => add_and_report_issues!(fs_type, Plist::as_fs_type),
1123                "unicodeRanges" | "openTypeOS2UnicodeRanges" => {
1124                    add_and_report_issues!(unicode_range_bits, Plist::as_unicode_code_ranges)
1125                }
1126                "codePageRanges" => {
1127                    add_and_report_issues!(codepage_range_bits, Plist::as_codepage_bits)
1128                }
1129                // these values are not listed in the glyphs.app UI, but exist
1130                // in some fonts:
1131                // https://github.com/googlefonts/fontc/pull/1144#issuecomment-2503134008
1132                "openTypeHeadLowestRecPPEM" => {
1133                    add_and_report_issues!(lowest_rec_ppem, Plist::as_i64)
1134                }
1135                "openTypeHheaCaretSlopeRun" => {
1136                    add_and_report_issues!(hhea_caret_slope_run, Plist::as_i64)
1137                }
1138                "openTypeHheaCaretSlopeRise" => {
1139                    add_and_report_issues!(hhea_caret_slope_rise, Plist::as_i64)
1140                }
1141                "openTypeHheaCaretOffset" => {
1142                    add_and_report_issues!(hhea_caret_offset, Plist::as_i64)
1143                }
1144                "openTypeVheaCaretSlopeRun" => {
1145                    add_and_report_issues!(vhea_caret_slope_run, Plist::as_i64)
1146                }
1147                "openTypeVheaCaretSlopeRise" => {
1148                    add_and_report_issues!(vhea_caret_slope_rise, Plist::as_i64)
1149                }
1150                "openTypeVheaCaretOffset" => {
1151                    add_and_report_issues!(vhea_caret_offset, Plist::as_i64)
1152                }
1153                "meta Table" => {
1154                    add_and_report_issues!(meta_table, MetaTableValues::from_plist)
1155                }
1156                "Don't use Production Names" => {
1157                    add_and_report_issues!(dont_use_production_names, Plist::as_bool)
1158                }
1159                "Link Metrics With First Master" => {
1160                    add_and_report_issues!(link_metrics_with_first_master, Plist::as_bool)
1161                }
1162                "Link Metrics With Master" => {
1163                    add_and_report_issues!(link_metrics_with_master, Plist::as_str, into)
1164                }
1165                // these might need to be handled? they're in the same list as
1166                // the items above:
1167                // https://github.com/googlefonts/glyphsLib/blob/74c63244fdb/Lib/glyphsLib/builder/custom_params.py#L429
1168                "openTypeOS2FamilyClass" | "openTypeHeadFlags" => {
1169                    log_once_warn!("unhandled custom param '{name}'")
1170                }
1171
1172                "Virtual Master" => match value.as_virtual_master() {
1173                    Some(val) => virtual_masters.push(val),
1174                    None => log::warn!("failed to parse virtual master '{value:?}'"),
1175                },
1176                "panose" => panose = value.as_vec_of_ints(),
1177                "openTypeOS2Panose" => panose_old = value.as_vec_of_ints(),
1178                "glyphOrder" => add_and_report_issues!(glyph_order, Plist::as_vec_of_string),
1179                "gasp Table" => add_and_report_issues!(gasp_table, Plist::as_gasp_table),
1180                "Feature for Feature Variations" => {
1181                    add_and_report_issues!(feature_for_feature_variations, Plist::as_str, into)
1182                }
1183                "Color Palettes" => {
1184                    add_and_report_issues!(color_palettes, Plist::as_color_palettes)
1185                }
1186                _ => log_once_warn!("unknown custom parameter '{name}'"),
1187            }
1188        }
1189        params.panose = panose.or(panose_old);
1190        params.virtual_masters = Some(virtual_masters).filter(|x| !x.is_empty());
1191        Ok(params)
1192    }
1193
1194    fn contains(&self, name: &str) -> bool {
1195        self.0.iter().any(|val| val.name == name)
1196    }
1197
1198    /// Consume and return the first (non-disabled) parameter with the given name, if any.
1199    ///
1200    /// This is for parameters that are handled elsewhere before `to_custom_params` and
1201    /// shouldn't be stored in CustomParameters.
1202    fn take(&mut self, name: &str) -> Option<Plist> {
1203        let pos = self
1204            .0
1205            .iter()
1206            .position(|val| val.name == name && val.disabled != Some(true))?;
1207
1208        // Replace with a sentinel value rather than removing it from the Vec to avoid
1209        // the overhead of shifting elements. This is skipped during `to_custom_params`
1210        let taken = std::mem::take(&mut self.0[pos]);
1211
1212        Some(taken.value)
1213    }
1214
1215    fn take_string(&mut self, name: &str) -> Option<String> {
1216        self.take(name)
1217            .and_then(|p| p.as_str().map(|s| s.to_owned()))
1218    }
1219
1220    fn take_axes(&mut self) -> Option<Vec<Axis>> {
1221        self.take("Axes").and_then(|p| p.as_axes())
1222    }
1223
1224    fn take_axis_mappings(&mut self) -> Option<Vec<AxisMapping>> {
1225        self.take("Axis Mappings")
1226            .and_then(|p| p.as_axis_mappings())
1227    }
1228
1229    fn take_axis_locations(&mut self) -> Option<Vec<AxisLocation>> {
1230        self.take("Axis Location")
1231            .and_then(|p| p.as_axis_locations())
1232    }
1233}
1234
1235#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
1236pub struct AxisLocation {
1237    axis_name: String,
1238    location: OrderedFloat<f64>,
1239}
1240
1241#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
1242pub struct AxisMapping {
1243    //TODO this should really be a `Tag`?
1244    tag: String,
1245    user_to_design: Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>,
1246}
1247
1248#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
1249struct RawMetric {
1250    // So named to let FromPlist populate it from a field called "type"
1251    type_: String,
1252}
1253
1254#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
1255struct RawName {
1256    key: String,
1257    value: Option<String>,
1258    values: Vec<RawNameValue>,
1259}
1260
1261/// Compute a priority score for a language code when selecting default names.
1262///
1263/// Lower scores have higher priority.
1264/// Priority order: dflt > default > ENG > others (by insertion order)
1265fn default_language_score(lang: &str, index: usize, scale: i32) -> i32 {
1266    match lang {
1267        "dflt" => -scale * 3 - index as i32,
1268        "default" => -scale * 2 - index as i32,
1269        "ENG" => -scale - index as i32,
1270        _ => index as i32,
1271    }
1272}
1273
1274impl RawName {
1275    fn is_empty(&self) -> bool {
1276        self.value.is_none() && self.values.is_empty()
1277    }
1278
1279    fn get_value(&self) -> Option<&str> {
1280        if let Some(value) = &self.value {
1281            return Some(value.as_str());
1282        }
1283
1284        // This is a localized (multivalued) name. Pick a winner.
1285        // See <https://github.com/googlefonts/fontc/issues/1011>
1286        // In order of preference: dflt, default, ENG, whatever is first
1287        // <https://github.com/googlefonts/glyphsLib/blob/1cb4fc5ae2cf385df95d2b7768e7ab4eb60a5ac3/Lib/glyphsLib/classes.py#L3155-L3161>
1288        // If the same name is defined repeatedly, e.g. dflt has 2 values, pick the last occurrence.
1289
1290        let scale = self.values.len() as i32;
1291        self.values
1292            .iter()
1293            .enumerate()
1294            .map(|(i, raw)| {
1295                (
1296                    default_language_score(raw.language.as_str(), i, scale),
1297                    raw.value.as_str(),
1298                )
1299            })
1300            .min()
1301            .map(|(_, raw)| raw)
1302    }
1303
1304    fn localized_values(&self) -> impl Iterator<Item = (&str, &str)> {
1305        self.values
1306            .iter()
1307            .map(|raw| (raw.language.as_str(), raw.value.as_str()))
1308    }
1309}
1310
1311#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
1312struct RawNameValue {
1313    language: String,
1314    value: String,
1315}
1316
1317#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
1318struct RawFeature {
1319    automatic: Option<i64>,
1320    disabled: Option<i64>,
1321    name: Option<String>,
1322    tag: Option<String>,
1323    notes: Option<String>,
1324    code: String,
1325    labels: Vec<RawNameValue>,
1326
1327    #[fromplist(ignore)]
1328    other_stuff: BTreeMap<String, Plist>,
1329}
1330
1331#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
1332pub struct Axis {
1333    #[fromplist(alt_name = "Name")]
1334    pub name: String,
1335    #[fromplist(alt_name = "Tag")]
1336    pub tag: String,
1337    pub hidden: Option<bool>,
1338}
1339
1340#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1341struct RawGlyph {
1342    layers: Vec<RawLayer>,
1343    glyphname: SmolStr,
1344    export: Option<bool>,
1345    #[fromplist(alt_name = "leftKerningGroup")]
1346    kern_left: Option<SmolStr>,
1347    #[fromplist(alt_name = "rightKerningGroup")]
1348    kern_right: Option<SmolStr>,
1349    unicode: Option<String>,
1350    category: Option<SmolStr>,
1351    sub_category: Option<SmolStr>,
1352    #[fromplist(alt_name = "production")]
1353    production_name: Option<SmolStr>,
1354    parts_settings: Vec<RawPartSetting>,
1355    #[fromplist(ignore)]
1356    other_stuff: BTreeMap<String, Plist>,
1357}
1358
1359#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1360struct RawPartSetting {
1361    name: SmolStr,
1362    bottom_name: Option<SmolStr>,
1363    bottom_value: i64,
1364    top_name: Option<SmolStr>,
1365    top_value: i64,
1366}
1367
1368#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1369struct RawLayer {
1370    name: String,
1371    layer_id: String,
1372    associated_master_id: Option<String>,
1373    width: Option<OrderedFloat<f64>>,
1374    vert_width: Option<OrderedFloat<f64>>,
1375    vert_origin: Option<OrderedFloat<f64>>,
1376    shapes: Vec<RawShape>,
1377    paths: Vec<Path>,
1378    components: Vec<Component>,
1379    anchors: Vec<RawAnchor>,
1380    hints: Vec<RawHint>,
1381    #[fromplist(alt_name = "attr")]
1382    attributes: LayerAttributes,
1383    // if this layer is part of a smart component; values should be 1 or 2
1384    // (this is validated when we convert to Layer)
1385    part_selection: BTreeMap<SmolStr, i64>,
1386    // glyphs2 only?
1387    user_data: LayerUserData,
1388    #[fromplist(ignore)]
1389    other_stuff: BTreeMap<String, Plist>,
1390}
1391
1392#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1393struct LayerUserData {
1394    #[fromplist(alt_name = "PartSelection")]
1395    part_selection: BTreeMap<SmolStr, i64>,
1396}
1397
1398impl RawLayer {
1399    /// Return true if the layer is a draft that is not meant to be compiled.
1400    ///
1401    /// The presence of an associated master indicates this is not a simple 'master' instance.
1402    /// Without 'attributes' that specify whether it's a special intermediate, alternate or
1403    /// color layer, we can assume the non-master layer is a draft.
1404    fn is_draft(&self) -> bool {
1405        self.associated_master_id.is_some()
1406            && self.attributes == Default::default()
1407            && self.part_selection.is_empty() // v3
1408            && self.user_data.part_selection.is_empty() // v2
1409    }
1410
1411    /// Glyphs uses the concept of 'bracket layers' to represent GSUB feature variations.
1412    ///
1413    /// See <https://glyphsapp.com/learn/switching-shapes#g-1-alternate-layers-bracket-layers>
1414    /// for more information.
1415    fn is_bracket_layer(&self, format_version: FormatVersion) -> bool {
1416        // can't be a bracket without an associated_master_id
1417        //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d57/Lib/glyphsLib/builder/builders.py#L270
1418        self.associated_master_id.is_some()
1419            && self.associated_master_id.as_ref() != Some(&self.layer_id)
1420            //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d5/Lib/glyphsLib/classes.py#L3942
1421            && match format_version {
1422                FormatVersion::V2 => AxisRule::from_layer_name(&self.name).is_some(),
1423                FormatVersion::V3 => !self.attributes.axis_rules.is_empty(),
1424            }
1425    }
1426
1427    fn v2_to_v3_attributes(&mut self) {
1428        // In Glyphs v2, 'brace' or intermediate layer coordinates are stored in the
1429        // layer name as comma-separated values inside braces
1430        let mut brace_coordinates = Vec::new();
1431        if let (Some(start), Some(end)) = (self.name.find('{'), self.name.find('}')) {
1432            let mut tokenizer = Tokenizer::new(&self.name[start..=end]);
1433            // we don't want this to fail, technically '{foobar}' is valid inside a
1434            // layer name which is not meant to specify intermediate coordinates.
1435            // Typos are also possible. Perhaps we should warn?
1436            brace_coordinates = tokenizer
1437                .parse_delimited_vec(VecDelimiters::CSV_IN_BRACES)
1438                .unwrap_or_default();
1439        }
1440        if !brace_coordinates.is_empty() {
1441            self.attributes.coordinates = brace_coordinates;
1442        }
1443        // TODO: handle 'bracket' layers and other attributes
1444    }
1445}
1446
1447/// Represents a path OR a component
1448///
1449/// <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#differences-between-version-2>
1450#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1451struct RawShape {
1452    // TODO: add numerous unsupported attributes
1453
1454    // When I'm a path
1455    closed: Option<bool>,
1456    nodes: Vec<Node>,
1457
1458    // When I'm a component I specifically want all my attributes to end up in other_stuff
1459    // My Component'ness can be detected by presence of a ref (Glyphs3) or name(Glyphs2) attribute
1460    // ref is reserved so take advantage of alt names
1461    #[fromplist(alt_name = "ref", alt_name = "name")]
1462    glyph_name: Option<SmolStr>,
1463    // if this is a reference to a smart component, this is the location in the
1464    // mini designspace of the component instance (user coords).
1465    piece: BTreeMap<SmolStr, f64>,
1466
1467    // for components, an optional name to rename an anchor
1468    // on the target glyph during anchor propagation
1469    anchor: Option<SmolStr>,
1470    transform: Option<String>, // v2
1471    pos: Vec<f64>,             // v3
1472    angle: Option<f64>,        // v3
1473    scale: Vec<f64>,           // v3
1474
1475    #[fromplist(alt_name = "attr")]
1476    attributes: ShapeAttributes,
1477}
1478
1479/// <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#spec-glyphs-3-path>
1480#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromPlist)]
1481pub struct Path {
1482    pub closed: bool,
1483    pub nodes: Vec<Node>,
1484    pub attributes: ShapeAttributes,
1485}
1486
1487/// <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#spec-glyphs-3-component>
1488#[derive(Default, Clone, Debug, FromPlist)]
1489pub struct Component {
1490    /// The glyph this component references
1491    pub name: SmolStr,
1492    /// meh
1493    pub transform: Affine,
1494    /// An alternative anchor name used during anchor propagation
1495    ///
1496    /// For instance, if an acute accent is a component of a ligature glyph,
1497    /// we might rename its 'top' anchor to 'top_2'
1498    pub anchor: Option<SmolStr>,
1499    /// For smart components, the location in the mini designspace of the instance.
1500    ///
1501    /// Keys should reference the axes in the component, with the value being
1502    /// a position in user coords.
1503    #[fromplist(alt_name = "piece")]
1504    pub smart_component_values: BTreeMap<SmolStr, f64>,
1505    pub attributes: ShapeAttributes,
1506}
1507
1508impl PartialEq for Component {
1509    fn eq(&self, other: &Self) -> bool {
1510        self.name == other.name
1511            && Into::<AffineForEqAndHash>::into(self.transform) == other.transform.into()
1512    }
1513}
1514
1515impl Eq for Component {}
1516
1517impl Hash for Component {
1518    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1519        self.name.hash(state);
1520        Into::<AffineForEqAndHash>::into(self.transform).hash(state);
1521    }
1522}
1523
1524#[derive(Clone, Copy, Debug)]
1525pub struct Node {
1526    pub pt: Point,
1527    pub node_type: NodeType,
1528}
1529
1530impl Node {
1531    pub fn is_on_curve(&self) -> bool {
1532        !matches!(self.node_type, NodeType::OffCurve)
1533    }
1534}
1535
1536impl PartialEq for Node {
1537    fn eq(&self, other: &Self) -> bool {
1538        Into::<PointForEqAndHash>::into(self.pt) == other.pt.into()
1539            && self.node_type == other.node_type
1540    }
1541}
1542
1543impl Eq for Node {}
1544
1545impl Hash for Node {
1546    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1547        PointForEqAndHash::new(self.pt).hash(state);
1548        self.node_type.hash(state);
1549    }
1550}
1551
1552#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1553pub enum NodeType {
1554    Line,
1555    LineSmooth,
1556    OffCurve,
1557    Curve,
1558    CurveSmooth,
1559    QCurve,
1560    QCurveSmooth,
1561}
1562
1563#[derive(Default, Clone, Debug, PartialEq, FromPlist)]
1564struct RawAnchor {
1565    name: SmolStr,
1566    pos: Option<Point>,       // v3
1567    position: Option<String>, // v2
1568}
1569
1570#[derive(Clone, Debug, PartialEq)]
1571pub struct Anchor {
1572    pub name: SmolStr,
1573    pub pos: Point,
1574}
1575
1576impl Anchor {
1577    pub(crate) fn is_origin(&self) -> bool {
1578        self.name == "*origin"
1579    }
1580
1581    pub(crate) fn origin_delta(&self) -> Option<Vec2> {
1582        self.is_origin().then_some(self.pos.to_vec2())
1583    }
1584}
1585
1586impl Hash for Anchor {
1587    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1588        self.name.hash(state);
1589        PointForEqAndHash::new(self.pos).hash(state);
1590    }
1591}
1592
1593#[derive(Clone, Debug, PartialEq, Hash)]
1594pub struct FontMaster {
1595    pub id: String,
1596    pub name: String,
1597    pub axes_values: Vec<OrderedFloat<f64>>,
1598    metric_values: BTreeMap<String, MetricValue>,
1599    pub number_values: BTreeMap<SmolStr, OrderedFloat<f64>>,
1600    pub custom_parameters: CustomParameters,
1601    pub user_data: BTreeMap<SmolStr, Plist>,
1602    pub metrics_source_id: Option<String>,
1603}
1604
1605impl FontMaster {
1606    fn read_metric(&self, metric_name: &str) -> Option<f64> {
1607        self.metric_values
1608            .get(metric_name)
1609            .map(|metric| metric.pos.into_inner())
1610    }
1611
1612    pub fn ascender(&self) -> Option<f64> {
1613        self.read_metric("ascender")
1614    }
1615
1616    pub fn descender(&self) -> Option<f64> {
1617        self.read_metric("descender")
1618    }
1619
1620    pub fn x_height(&self) -> Option<f64> {
1621        self.read_metric("x-height")
1622    }
1623
1624    pub fn cap_height(&self) -> Option<f64> {
1625        self.read_metric("cap height")
1626    }
1627
1628    pub fn italic_angle(&self) -> Option<f64> {
1629        self.read_metric("italic angle")
1630    }
1631}
1632
1633/// Resolves the linked metrics master ID from "Link Metrics..." custom parameters.
1634///
1635/// See <https://github.com/googlefonts/glyphsLib/blob/a045b48/Lib/glyphsLib/classes.py#L1641-L1665>
1636fn resolve_metrics_source_id(
1637    custom_params: &CustomParameters,
1638    master_ids_to_names: &IndexMap<String, Option<String>>,
1639) -> Option<String> {
1640    // "Link Metrics With First Master" takes precedence over "Link Metrics With Master"
1641    if custom_params.link_metrics_with_first_master == Some(true) {
1642        return master_ids_to_names.first().map(|(id, _)| id.clone());
1643    }
1644
1645    if let Some(ref source_ref) = custom_params.link_metrics_with_master {
1646        // Try by master ID first
1647        if master_ids_to_names.contains_key(source_ref.as_str()) {
1648            return Some(source_ref.to_string());
1649        }
1650        // Try by master name
1651        if let Some((id, _)) = master_ids_to_names
1652            .iter()
1653            .find(|(_, name)| name.as_deref() == Some(source_ref.as_str()))
1654        {
1655            return Some(id.clone());
1656        }
1657        log::warn!("Source master for metrics not found: '{source_ref}'");
1658    }
1659
1660    None
1661}
1662
1663#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
1664struct RawFontMaster {
1665    id: String,
1666    name: Option<String>,
1667
1668    weight: Option<String>,
1669    width: Option<String>,
1670    custom: Option<String>,
1671
1672    weight_value: Option<OrderedFloat<f64>>,
1673    interpolation_weight: Option<OrderedFloat<f64>>,
1674
1675    width_value: Option<OrderedFloat<f64>>,
1676    interpolation_width: Option<OrderedFloat<f64>>,
1677
1678    custom_value: Option<OrderedFloat<f64>>,
1679
1680    typo_ascender: Option<i64>,
1681    typo_descender: Option<OrderedFloat<f64>>,
1682    typo_line_gap: Option<OrderedFloat<f64>>,
1683    win_ascender: Option<OrderedFloat<f64>>,
1684    win_descender: Option<OrderedFloat<f64>>,
1685
1686    axes_values: Vec<OrderedFloat<f64>>,
1687    metric_values: Vec<RawMetricValue>, // v3
1688
1689    ascender: Option<OrderedFloat<f64>>,   // v2
1690    baseline: Option<OrderedFloat<f64>>,   // v2
1691    descender: Option<OrderedFloat<f64>>,  // v2
1692    cap_height: Option<OrderedFloat<f64>>, // v2
1693    x_height: Option<OrderedFloat<f64>>,   // v2
1694    #[fromplist(alt_name = "italic angle")]
1695    italic_angle: Option<OrderedFloat<f64>>, // v2
1696
1697    alignment_zones: Vec<String>, // v2
1698
1699    custom_parameters: RawCustomParameters,
1700    number_values: Vec<OrderedFloat<f64>>,
1701    user_data: BTreeMap<SmolStr, Plist>,
1702
1703    #[fromplist(ignore)]
1704    other_stuff: BTreeMap<String, Plist>,
1705}
1706
1707#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
1708struct RawMetricValue {
1709    pos: Option<OrderedFloat<f64>>,
1710    over: Option<OrderedFloat<f64>>,
1711}
1712
1713#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
1714pub struct MetricValue {
1715    pos: OrderedFloat<f64>,
1716    over: OrderedFloat<f64>,
1717}
1718
1719impl From<RawMetricValue> for MetricValue {
1720    fn from(src: RawMetricValue) -> MetricValue {
1721        MetricValue {
1722            pos: src.pos.unwrap_or_default(),
1723            over: src.over.unwrap_or_default(),
1724        }
1725    }
1726}
1727
1728#[derive(Clone, Debug, PartialEq, Hash)]
1729pub struct Instance {
1730    pub name: String,
1731    pub active: bool,
1732    // So named to let FromPlist populate it from a field called "type"
1733    pub type_: InstanceType,
1734    pub axis_mappings: BTreeMap<String, AxisUserToDesignMap>,
1735    pub axes_values: Vec<OrderedFloat<f64>>,
1736    pub custom_parameters: CustomParameters,
1737    properties: Vec<RawName>, // used for name resolution
1738}
1739
1740/// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/classes.py#L150>
1741#[derive(Clone, Debug, PartialEq, Hash)]
1742pub enum InstanceType {
1743    Single,
1744    Variable,
1745}
1746
1747impl From<&str> for InstanceType {
1748    fn from(value: &str) -> Self {
1749        if value.eq_ignore_ascii_case("variable") {
1750            InstanceType::Variable
1751        } else {
1752            InstanceType::Single
1753        }
1754    }
1755}
1756
1757#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromPlist)]
1758struct RawInstance {
1759    name: String,
1760    exports: Option<i64>,
1761    active: Option<i64>,
1762    type_: Option<String>,
1763    axes_values: Vec<OrderedFloat<f64>>,
1764
1765    weight_value: Option<OrderedFloat<f64>>,
1766    interpolation_weight: Option<OrderedFloat<f64>>,
1767
1768    width_value: Option<OrderedFloat<f64>>,
1769    interpolation_width: Option<OrderedFloat<f64>>,
1770
1771    custom_value: Option<OrderedFloat<f64>>,
1772
1773    weight_class: Option<String>,
1774    width_class: Option<String>,
1775    properties: Vec<RawName>,
1776    custom_parameters: RawCustomParameters,
1777}
1778
1779impl RawInstance {
1780    /// Per glyphsLib both "exports=0" and "active=0" mean inactive
1781    /// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L637>
1782    fn is_active(&self) -> bool {
1783        self.exports.unwrap_or(1) != 0 && self.active.unwrap_or(1) != 0
1784    }
1785}
1786
1787trait GlyphsV2OrderedAxes {
1788    fn weight_value(&self) -> Option<OrderedFloat<f64>>;
1789    fn interpolation_weight(&self) -> Option<OrderedFloat<f64>>;
1790    fn width_value(&self) -> Option<OrderedFloat<f64>>;
1791    fn interpolation_width(&self) -> Option<OrderedFloat<f64>>;
1792    fn custom_value(&self) -> Option<OrderedFloat<f64>>;
1793
1794    fn value_for_nth_axis(&self, nth_axis: usize) -> Result<OrderedFloat<f64>, Error> {
1795        // Per https://github.com/googlefonts/fontmake-rs/pull/42#pullrequestreview-1211619812
1796        // the field to use is based on the order in axes NOT the tag.
1797        // That is, whatever the first axis is, it's value is in the weightValue field. Long sigh.
1798        // Defaults per https://github.com/googlefonts/fontmake-rs/pull/42#discussion_r1044415236.
1799        // v2 instances use novel field names so send back several for linear probing.
1800        Ok(match nth_axis {
1801            0 => self
1802                .weight_value()
1803                .or(self.interpolation_weight())
1804                .unwrap_or(100.0.into()),
1805            1 => self
1806                .width_value()
1807                .or(self.interpolation_width())
1808                .unwrap_or(100.0.into()),
1809            2 => self.custom_value().unwrap_or(0.0.into()),
1810            _ => {
1811                return Err(Error::StructuralError(format!(
1812                    "We don't know what field to use for axis {nth_axis}"
1813                )));
1814            }
1815        })
1816    }
1817
1818    fn axis_values(&self, axes: &[Axis]) -> Result<Vec<OrderedFloat<f64>>, Error> {
1819        (0..axes.len())
1820            .map(|nth_axis| self.value_for_nth_axis(nth_axis))
1821            .collect::<Result<Vec<OrderedFloat<f64>>, Error>>()
1822    }
1823}
1824
1825impl GlyphsV2OrderedAxes for RawFontMaster {
1826    fn weight_value(&self) -> Option<OrderedFloat<f64>> {
1827        self.weight_value
1828    }
1829
1830    fn interpolation_weight(&self) -> Option<OrderedFloat<f64>> {
1831        self.interpolation_weight
1832    }
1833
1834    fn width_value(&self) -> Option<OrderedFloat<f64>> {
1835        self.width_value
1836    }
1837
1838    fn interpolation_width(&self) -> Option<OrderedFloat<f64>> {
1839        self.interpolation_width
1840    }
1841
1842    fn custom_value(&self) -> Option<OrderedFloat<f64>> {
1843        self.custom_value
1844    }
1845}
1846
1847impl GlyphsV2OrderedAxes for RawInstance {
1848    fn weight_value(&self) -> Option<OrderedFloat<f64>> {
1849        self.weight_value
1850    }
1851
1852    fn interpolation_weight(&self) -> Option<OrderedFloat<f64>> {
1853        self.interpolation_weight
1854    }
1855
1856    fn width_value(&self) -> Option<OrderedFloat<f64>> {
1857        self.width_value
1858    }
1859
1860    fn interpolation_width(&self) -> Option<OrderedFloat<f64>> {
1861        self.interpolation_width
1862    }
1863
1864    fn custom_value(&self) -> Option<OrderedFloat<f64>> {
1865        self.custom_value
1866    }
1867}
1868
1869fn parse_node_from_string(value: &str) -> Node {
1870    let mut spl = value.splitn(3, ' ');
1871    let x = spl.next().unwrap().parse().unwrap();
1872    let y = spl.next().unwrap().parse().unwrap();
1873    let pt = Point::new(x, y);
1874    let mut raw_node_type = spl.next().unwrap();
1875    // drop the userData dict, we don't use it for compilation
1876    if raw_node_type.contains('{') {
1877        raw_node_type = raw_node_type.split('{').next().unwrap().trim_end();
1878    }
1879    let node_type = raw_node_type.parse().unwrap();
1880    Node { pt, node_type }
1881}
1882
1883fn parse_node_from_tokenizer(tokenizer: &mut Tokenizer<'_>) -> Result<Node, crate::plist::Error> {
1884    // (x,y,type)
1885    let x: f64 = tokenizer.parse()?;
1886    tokenizer.eat(b',')?;
1887    let y: f64 = tokenizer.parse()?;
1888    tokenizer.eat(b',')?;
1889    let node_type: String = tokenizer.parse()?;
1890    let node_type = NodeType::from_str(&node_type)
1891        .map_err(|_| crate::plist::Error::Parse(format!("unknown node type '{node_type}'")))?;
1892
1893    // Sometimes there is userData; ignore it
1894    if tokenizer.eat(b',').is_ok() {
1895        tokenizer.skip_rec()?;
1896    }
1897
1898    Ok(Node {
1899        pt: Point { x, y },
1900        node_type,
1901    })
1902}
1903
1904impl std::str::FromStr for NodeType {
1905    type Err = String;
1906    fn from_str(s: &str) -> Result<Self, Self::Err> {
1907        match s {
1908            // Glyphs 2 style
1909            "LINE" => Ok(NodeType::Line),
1910            "LINE SMOOTH" => Ok(NodeType::LineSmooth),
1911            "OFFCURVE" => Ok(NodeType::OffCurve),
1912            "CURVE" => Ok(NodeType::Curve),
1913            "CURVE SMOOTH" => Ok(NodeType::CurveSmooth),
1914            "QCURVE" => Ok(NodeType::QCurve),
1915            "QCURVE SMOOTH" => Ok(NodeType::QCurveSmooth),
1916            // Glyphs 3 style
1917            "l" => Ok(NodeType::Line),
1918            "ls" => Ok(NodeType::LineSmooth),
1919            "o" => Ok(NodeType::OffCurve),
1920            "c" => Ok(NodeType::Curve),
1921            "cs" => Ok(NodeType::CurveSmooth),
1922            "q" => Ok(NodeType::QCurve),
1923            "qs" => Ok(NodeType::QCurveSmooth),
1924            _ => Err(format!("unknown node type {s}")),
1925        }
1926    }
1927}
1928
1929// Hand-parse Node because it doesn't follow the normal structure
1930impl FromPlist for Node {
1931    fn parse(tokenizer: &mut Tokenizer<'_>) -> Result<Self, crate::plist::Error> {
1932        use crate::plist::Error;
1933        let tok = tokenizer.lex()?;
1934        let node = match &tok {
1935            Token::Atom(value) => parse_node_from_string(value),
1936            Token::String(value) => parse_node_from_string(value),
1937            Token::OpenParen => {
1938                let node = parse_node_from_tokenizer(tokenizer)?;
1939                tokenizer.eat(b')')?;
1940                node
1941            }
1942            _ => return Err(Error::ExpectedString),
1943        };
1944        Ok(node)
1945    }
1946}
1947
1948impl Path {
1949    pub fn new(closed: bool) -> Path {
1950        Path {
1951            nodes: Vec::new(),
1952            closed,
1953            ..Default::default()
1954        }
1955    }
1956
1957    pub fn add(&mut self, pt: impl Into<Point>, node_type: NodeType) {
1958        let pt = pt.into();
1959        self.nodes.push(Node { pt, node_type });
1960    }
1961
1962    /// Rotate left by one, placing the first point at the end. This is because
1963    /// it's what glyphs seems to expect.
1964    pub fn rotate_left(&mut self, delta: usize) {
1965        self.nodes.rotate_left(delta);
1966    }
1967
1968    pub fn reverse(&mut self) {
1969        self.nodes.reverse();
1970    }
1971
1972    /// get the segment ending at `idx`
1973    pub(crate) fn get_previous_segment(&self, idx: usize) -> Option<PathSeg> {
1974        if self.nodes.len() < 2 {
1975            return None;
1976        }
1977        let end = self.nodes.get(idx)?.pt;
1978        let mut idx = self.prev_idx(idx);
1979        let prev = self.nodes.get(idx)?;
1980        if prev.node_type == NodeType::OffCurve {
1981            idx = self.prev_idx(idx);
1982            let control1 = self.nodes.get(idx)?;
1983            if control1.node_type != NodeType::OffCurve {
1984                // seems extremely unlikely but not exactly impossible, let's at least log?
1985                log::info!("path for corner component contains unexpected quadratic");
1986                return Some(QuadBez::new(control1.pt, prev.pt, end).into());
1987            }
1988            let start = self.nodes.get(self.prev_idx(idx))?;
1989            Some(CubicBez::new(start.pt, control1.pt, prev.pt, end).into())
1990        } else {
1991            Some(Line::new(prev.pt, end).into())
1992        }
1993    }
1994
1995    /// get the segment beginning at `idx`
1996    pub(crate) fn get_next_segment(&self, idx: usize) -> Option<PathSeg> {
1997        if self.nodes.len() < 2 {
1998            return None;
1999        }
2000        let start = self.nodes.get(idx)?.pt;
2001        let mut idx = self.next_idx(idx);
2002        let next = self.nodes.get(idx)?;
2003        if next.node_type == NodeType::OffCurve {
2004            idx = self.next_idx(idx);
2005            let control2 = self.nodes.get(idx)?;
2006            if control2.node_type != NodeType::OffCurve {
2007                // seems extremely unlikely but not exactly impossible, let's at least log?
2008                log::info!("path for corner component contains unexpected quadratic");
2009                return Some(QuadBez::new(start, next.pt, control2.pt).into());
2010            }
2011            let end = self.nodes.get(self.next_idx(idx))?;
2012            Some(CubicBez::new(start, next.pt, control2.pt, end.pt).into())
2013        } else {
2014            Some(Line::new(start, next.pt).into())
2015        }
2016    }
2017
2018    pub(crate) fn prev_idx(&self, idx: usize) -> usize {
2019        if idx == 0 {
2020            self.nodes.len().saturating_sub(1)
2021        } else {
2022            idx - 1
2023        }
2024    }
2025
2026    pub(crate) fn next_idx(&self, idx: usize) -> usize {
2027        (idx + 1) % self.nodes.len()
2028    }
2029
2030    #[cfg(test)]
2031    pub(crate) fn to_points(&self) -> Vec<Point> {
2032        self.nodes.iter().map(|n| n.pt).collect()
2033    }
2034}
2035
2036fn v2_to_v3_name(v2_prop: Option<&str>, v3_name: &str) -> Option<RawName> {
2037    // https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#properties
2038    // Keys ending with "s" are localizable that means the second key is values
2039    v2_prop.map(|value| {
2040        if v3_name.ends_with('s') {
2041            RawName {
2042                key: v3_name.into(),
2043                value: None,
2044                values: vec![RawNameValue {
2045                    language: "dflt".into(),
2046                    value: value.to_string(),
2047                }],
2048            }
2049        } else {
2050            RawName {
2051                key: v3_name.into(),
2052                value: Some(value.to_string()),
2053                values: vec![],
2054            }
2055        }
2056    })
2057}
2058
2059impl RawFont {
2060    pub fn load_from_string(raw_content: &str) -> Result<Self, crate::plist::Error> {
2061        let raw_content = preprocess_unparsed_plist(raw_content);
2062        Self::parse_plist(&raw_content)
2063    }
2064
2065    pub fn load(glyphs_file: &path::Path) -> Result<Self, Error> {
2066        if glyphs_file.extension() == Some(OsStr::new("glyphspackage")) {
2067            return Self::load_package(glyphs_file);
2068        }
2069
2070        debug!("Read glyphs {glyphs_file:?}");
2071        let raw_content = fs::read_to_string(glyphs_file).map_err(Error::IoError)?;
2072        Self::load_from_string(&raw_content)
2073            .map_err(|e| Error::ParseError(glyphs_file.to_path_buf(), e.to_string()))
2074    }
2075
2076    /// load from a .glyphspackage
2077    fn load_package(glyphs_package: &path::Path) -> Result<RawFont, Error> {
2078        if !glyphs_package.is_dir() {
2079            return Err(Error::NotAGlyphsPackage(glyphs_package.to_path_buf()));
2080        }
2081        debug!("Read glyphs package {glyphs_package:?}");
2082
2083        let fontinfo_file = glyphs_package.join("fontinfo.plist");
2084        let fontinfo_data = fs::read_to_string(&fontinfo_file).map_err(Error::IoError)?;
2085        let mut raw_font = RawFont::parse_plist(&fontinfo_data)
2086            .map_err(|e| Error::ParseError(fontinfo_file.to_path_buf(), format!("{e}")))?;
2087
2088        let mut glyphs: HashMap<SmolStr, RawGlyph> = HashMap::new();
2089        let glyphs_dir = glyphs_package.join("glyphs");
2090        if glyphs_dir.is_dir() {
2091            for entry in fs::read_dir(glyphs_dir).map_err(Error::IoError)? {
2092                let entry = entry.map_err(Error::IoError)?;
2093                let path = entry.path();
2094                if path.extension() == Some(OsStr::new("glyph")) {
2095                    let glyph_data = fs::read_to_string(&path).map_err(Error::IoError)?;
2096                    let glyph_data = preprocess_unparsed_plist(&glyph_data);
2097                    let glyph = RawGlyph::parse_plist(&glyph_data)
2098                        .map_err(|e| Error::ParseError(path.clone(), e.to_string()))?;
2099                    if glyph.glyphname.is_empty() {
2100                        return Err(Error::ParseError(
2101                            path.clone(),
2102                            "Glyph dict must have a 'glyphname' key".to_string(),
2103                        ));
2104                    }
2105                    glyphs.insert(glyph.glyphname.clone(), glyph);
2106                }
2107            }
2108        }
2109
2110        // if order.plist file exists, read it and sort glyphs in it accordingly
2111        let order_file = glyphs_package.join("order.plist");
2112        let mut ordered_glyphs = Vec::new();
2113        if order_file.exists() {
2114            let order_data = fs::read_to_string(&order_file).map_err(Error::IoError)?;
2115            let order_plist = Plist::parse(&order_data)
2116                .map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
2117            let order = order_plist
2118                .expect_array()
2119                .map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
2120            for glyph_name in order {
2121                let glyph_name = glyph_name
2122                    .expect_string()
2123                    .map_err(|e| Error::ParseError(order_file.to_path_buf(), e.to_string()))?;
2124                if let Some(glyph) = glyphs.remove(glyph_name.as_str()) {
2125                    ordered_glyphs.push(glyph);
2126                }
2127            }
2128        }
2129        // sort the glyphs not in order.plist by their name
2130        let mut glyph_names: Vec<_> = glyphs.keys().cloned().collect();
2131        glyph_names.sort();
2132        ordered_glyphs.extend(
2133            glyph_names
2134                .into_iter()
2135                .map(|glyph_name| glyphs.remove(&glyph_name).unwrap()),
2136        );
2137        assert!(glyphs.is_empty());
2138        raw_font.glyphs = ordered_glyphs;
2139
2140        // ignore UIState.plist which stuff like displayStrings that are not used by us
2141
2142        Ok(raw_font)
2143    }
2144
2145    fn v2_to_v3_axes(&mut self) -> Result<Vec<String>, Error> {
2146        let mut tags = Vec::new();
2147        if let Some(v2_axes) = self.custom_parameters.take_axes() {
2148            for v2_axis in v2_axes {
2149                tags.push(v2_axis.tag.clone());
2150                self.axes.push(v2_axis.clone());
2151            }
2152        }
2153
2154        // Match the defaults from https://github.com/googlefonts/glyphsLib/blob/f6e9c4a29ce764d34c309caef5118c48c156be36/Lib/glyphsLib/builder/axes.py#L526
2155        // if we have nothing
2156        if self.axes.is_empty() {
2157            self.axes.push(Axis {
2158                name: "Weight".into(),
2159                tag: "wght".into(),
2160                hidden: None,
2161            });
2162            self.axes.push(Axis {
2163                name: "Width".into(),
2164                tag: "wdth".into(),
2165                hidden: None,
2166            });
2167            self.axes.push(Axis {
2168                name: "Custom".into(),
2169                tag: "XXXX".into(),
2170                hidden: None,
2171            });
2172        }
2173
2174        if self.axes.len() > 3 {
2175            return Err(Error::StructuralError(
2176                "We only understand 0..3 axes for Glyphs v2".into(),
2177            ));
2178        }
2179
2180        // v2 stores values for axes in specific fields, find them and put them into place
2181        // "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."
2182        for master in self.font_master.iter_mut() {
2183            master.axes_values = master.axis_values(&self.axes)?;
2184        }
2185        for instance in self.instances.iter_mut() {
2186            instance.axes_values = instance.axis_values(&self.axes)?;
2187        }
2188
2189        Ok(tags)
2190    }
2191
2192    fn v2_to_v3_metrics(&mut self) -> Result<(), Error> {
2193        // setup storage for the basic metrics
2194        self.metrics = V3_METRIC_NAMES
2195            .iter()
2196            .map(|n| RawMetric {
2197                type_: n.to_string(),
2198            })
2199            .collect();
2200
2201        let mut used_metrics = [false; 6];
2202        // first which metric occurs in any master:
2203        for m in &self.font_master {
2204            for (i, val) in [
2205                m.ascender,
2206                m.baseline,
2207                m.descender,
2208                m.cap_height,
2209                m.x_height,
2210                m.italic_angle,
2211            ]
2212            .iter()
2213            .enumerate()
2214            {
2215                used_metrics[i] |= val.is_some();
2216            }
2217        }
2218
2219        // add only used metrics to the metric list
2220        self.metrics = V3_METRIC_NAMES
2221            .into_iter()
2222            .zip(used_metrics)
2223            .filter(|(_, used)| *used)
2224            .map(|(name, _)| RawMetric {
2225                type_: name.to_string(),
2226            })
2227            .collect();
2228
2229        let mut non_metric_alignment_zones = vec![vec![]; self.font_master.len()];
2230
2231        // in each font master setup the parallel array
2232        for (i, master) in self.font_master.iter_mut().enumerate() {
2233            // each master has to have an entry for each defined metric,
2234            // even if it is 'None', and they need to be in the same order
2235            // as in the self.metrics list.
2236            let mut metric_values = vec![RawMetricValue::default(); self.metrics.len()];
2237            for (metric_idx, name) in self.metrics.iter().enumerate() {
2238                let value = match name.type_.as_ref() {
2239                    "ascender" => master.ascender,
2240                    "baseline" => master.baseline,
2241                    "descender" => master.descender,
2242                    "cap height" => master.cap_height,
2243                    "x-height" => master.x_height,
2244                    "italic angle" => master.italic_angle,
2245                    _ => unreachable!("only these values exist in V3_METRIC_NAMES"),
2246                };
2247                metric_values[metric_idx].pos = value;
2248            }
2249
2250            // "alignmentZones is now a set of over (overshoot) properties attached to metrics"
2251            for alignment_zone in &master.alignment_zones {
2252                let Some((pos, over)) = parse_alignment_zone(alignment_zone) else {
2253                    warn!("Confusing alignment zone '{alignment_zone}', skipping");
2254                    continue;
2255                };
2256
2257                // skip zero-height zones
2258                if over == 0. {
2259                    continue;
2260                }
2261
2262                // special handling for this; we assume it's the baseline, and
2263                // we know where that is in our vec (and it has pos: None, set
2264                // above)
2265                if pos == 0. {
2266                    if let Some((idx, _)) = self
2267                        .metrics
2268                        .iter()
2269                        .enumerate()
2270                        .find(|(_, name)| name.type_ == "baseline")
2271                    {
2272                        metric_values[idx].over = Some(over);
2273                    }
2274                    continue;
2275                }
2276
2277                // now look for a metric that has the same position as this zone
2278                // this is quadratic but N is small
2279                if let Some(metric) = metric_values.iter_mut().find(|x| x.pos == Some(pos)) {
2280                    metric.over = Some(over);
2281                } else {
2282                    non_metric_alignment_zones[i].push((pos, over))
2283                }
2284            }
2285            master.metric_values = metric_values;
2286        }
2287
2288        // now handle any non-metric alignment zones, converting them to metrics.
2289        // first we assign a name to each unique position:
2290        let mut new_metrics = HashMap::new();
2291        for pos in non_metric_alignment_zones
2292            .iter()
2293            .flat_map(|master| master.iter().map(|(pos, _)| *pos))
2294        {
2295            if !new_metrics.contains_key(&pos) {
2296                let next_zone = new_metrics.len() + 1;
2297                let idx = self.metrics.len();
2298                self.metrics.push(RawMetric {
2299                    type_: format!("zone {next_zone}"),
2300                });
2301                new_metrics.insert(pos, idx);
2302            }
2303        }
2304
2305        // flip our map, so it's ordered on index:
2306        let new_metrics: BTreeMap<_, _> = new_metrics.into_iter().map(|(k, v)| (v, k)).collect();
2307
2308        // then for each master, add a metric value for each newly named metric
2309        for (idx, metrics) in non_metric_alignment_zones.into_iter().enumerate() {
2310            for pos_to_add in new_metrics.values().copied() {
2311                let to_add = metrics.iter().copied().find_map(|(pos, over)| {
2312                    (pos == pos_to_add).then_some(RawMetricValue {
2313                        pos: Some(pos),
2314                        over: Some(over),
2315                    })
2316                });
2317
2318                self.font_master[idx]
2319                    .metric_values
2320                    .push(to_add.unwrap_or_default());
2321            }
2322        }
2323        Ok(())
2324    }
2325
2326    fn v2_to_v3_master_names(&mut self) -> Result<(), Error> {
2327        // in Glyphs 2, masters don't have a single 'name' attribute, but rather
2328        // a concatenation of three other optional attributes weirdly called
2329        // 'width', 'weight' and 'custom' (in exactly this order).
2330        // The first two can only contain few predefined values, the last one is
2331        // residual and free-form. They default to 'Regular' when omitted in
2332        // the source. See:
2333        // https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv2.md
2334        // https://github.com/googlefonts/glyphsLib/blob/9d5828d/Lib/glyphsLib/classes.py#L1700-L1711
2335        for master in self.font_master.iter_mut() {
2336            // Even though glyphs2 masters don't officially have a 'name' attribute,
2337            // some glyphs2 sources produced by more recent versions of Glyphs
2338            // sometimes have it (unclear exactly when or from which version on).
2339            // We keep the 'name' attribute as is, instead of generating a one.
2340            if master.name.is_some() {
2341                continue;
2342            }
2343
2344            // Remove Nones, empty strings and redundant occurrences of 'Regular'
2345            let mut names = [
2346                master.width.as_deref(),
2347                master.weight.as_deref(),
2348                master.custom.as_deref(),
2349            ]
2350            .iter()
2351            .flatten()
2352            .flat_map(|n| n.split_ascii_whitespace())
2353            .filter(|x| *x != "Regular")
2354            .collect::<Vec<_>>();
2355
2356            // append "Italic" if italic angle != 0
2357            if let Some(italic_angle) = master.italic_angle
2358                && italic_angle != 0.0
2359                && (names.is_empty()
2360                    || !names
2361                        .iter()
2362                        .any(|name| *name == "Italic" || *name == "Oblique"))
2363            {
2364                names.push("Italic");
2365            }
2366            // if all are empty, default to "Regular"
2367            master.name = if names.is_empty() {
2368                Some("Regular".into())
2369            } else {
2370                Some(names.join(" "))
2371            };
2372        }
2373        Ok(())
2374    }
2375
2376    fn v2_to_v3_names(&mut self) -> Result<(), Error> {
2377        // The copyright, designer, designerURL, manufacturer, manufacturerURL top-level entries
2378        // have been moved into new top-level properties dictionary and made localizable.
2379        // Take properties to avoid incompatible borrowing against self
2380        let mut properties = std::mem::take(&mut self.properties);
2381
2382        properties.extend(v2_to_v3_name(self.copyright.as_deref(), "copyrights"));
2383        properties.extend(v2_to_v3_name(self.designer.as_deref(), "designers"));
2384        properties.extend(v2_to_v3_name(self.designerURL.as_deref(), "designerURL"));
2385        properties.extend(v2_to_v3_name(self.manufacturer.as_deref(), "manufacturers"));
2386        properties.extend(v2_to_v3_name(
2387            self.manufacturerURL.as_deref(),
2388            "manufacturerURL",
2389        ));
2390
2391        // glyphsLib tries both long and short names, with short names taking precedence
2392        //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L258
2393        let mut v2_to_v3_param = |v2_names: &[&str], v3_name: &str| {
2394            if properties.iter().any(|n| n.key == v3_name && !n.is_empty()) {
2395                return;
2396            }
2397            for v2_name in v2_names {
2398                if let Some(value) = v2_to_v3_name(
2399                    self.custom_parameters.take_string(v2_name).as_deref(),
2400                    v3_name,
2401                ) {
2402                    properties.push(value);
2403                    return;
2404                }
2405            }
2406        };
2407
2408        v2_to_v3_param(&["description", "openTypeNameDescription"], "descriptions");
2409        v2_to_v3_param(&["licenseURL", "openTypeNameLicenseURL"], "licenseURL");
2410        v2_to_v3_param(&["versionString", "openTypeNameVersion"], "versionString");
2411        v2_to_v3_param(&["compatibleFullName"], "compatibleFullNames");
2412        v2_to_v3_param(&["license", "openTypeNameLicense"], "licenses");
2413        v2_to_v3_param(&["uniqueID", "openTypeNameUniqueID"], "uniqueID");
2414        v2_to_v3_param(&["trademark"], "trademarks");
2415        v2_to_v3_param(&["sampleText", "openTypeNameSampleText"], "sampleTexts");
2416        v2_to_v3_param(&["postscriptFullName"], "postscriptFullName");
2417        v2_to_v3_param(&["postscriptFontName"], "postscriptFontName");
2418        v2_to_v3_param(
2419            &["WWSFamilyName", "openTypeNameWWSFamilyName"],
2420            "WWSFamilyName",
2421        );
2422        v2_to_v3_param(&["vendorID", "openTypeOS2VendorID"], "vendorID");
2423
2424        self.properties = properties;
2425
2426        Ok(())
2427    }
2428
2429    fn v2_to_v3_instances(&mut self) -> Result<(), Error> {
2430        for instance in self.instances.iter_mut() {
2431            if let Some(custom_weight_class) = instance.custom_parameters.take("weightClass") {
2432                instance.weight_class = custom_weight_class.to_string().into();
2433            }
2434            // named clases become #s in v3
2435            for (tag, opt) in [
2436                ("wght", &mut instance.weight_class),
2437                ("wdth", &mut instance.width_class),
2438            ] {
2439                let Some(value) = opt.as_ref() else {
2440                    continue;
2441                };
2442                if f64::from_str(value).is_ok() {
2443                    continue;
2444                };
2445                let Some(value) = lookup_class_value(tag, value) else {
2446                    return Err(Error::UnknownValueName(value.clone()));
2447                };
2448                let _ = opt.insert(value.to_string());
2449            }
2450
2451            instance.properties.extend(v2_to_v3_name(
2452                instance
2453                    .custom_parameters
2454                    .take_string("postscriptFontName")
2455                    .as_deref(),
2456                "postscriptFontName",
2457            ));
2458        }
2459
2460        Ok(())
2461    }
2462
2463    fn v2_to_v3_layer_attributes(&mut self) {
2464        for raw_glyph in self.glyphs.iter_mut() {
2465            for layer in raw_glyph.layers.iter_mut() {
2466                layer.v2_to_v3_attributes();
2467            }
2468        }
2469    }
2470
2471    /// `<See https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#differences-between-version-2>`
2472    fn v2_to_v3(&mut self) -> Result<(), Error> {
2473        self.v2_to_v3_master_names()?;
2474        self.v2_to_v3_axes()?;
2475        self.v2_to_v3_metrics()?;
2476        self.v2_to_v3_instances()?;
2477        self.v2_to_v3_names()?; // uses instances
2478        self.v2_to_v3_layer_attributes();
2479        Ok(())
2480    }
2481}
2482
2483// in the form '{INT, INT}'
2484fn parse_alignment_zone(zone: &str) -> Option<(OrderedFloat<f64>, OrderedFloat<f64>)> {
2485    let (one, two) = zone.split_once(',')?;
2486    let one = one.trim_start_matches(['{', ' ']).parse::<i32>().ok()?;
2487    let two = two.trim_start().trim_end_matches('}').parse::<i32>().ok()?;
2488    Some((OrderedFloat(one as f64), OrderedFloat(two as f64)))
2489}
2490
2491fn make_glyph_order(glyphs: &[RawGlyph], custom_order: Option<Vec<SmolStr>>) -> Vec<SmolStr> {
2492    let mut valid_names: HashSet<_> = glyphs.iter().map(|g| &g.glyphname).collect();
2493    let mut glyph_order = Vec::new();
2494
2495    // Add all valid glyphOrder entries in order
2496    // See https://github.com/googlefonts/fontmake-rs/pull/43/files#r1044627972
2497    for name in custom_order.into_iter().flatten() {
2498        if valid_names.remove(&name) {
2499            glyph_order.push(name.clone());
2500        }
2501    }
2502
2503    // Add anything left over in file order
2504    glyph_order.extend(
2505        glyphs
2506            .iter()
2507            .filter(|g| valid_names.contains(&g.glyphname))
2508            .map(|g| g.glyphname.clone()),
2509    );
2510
2511    glyph_order
2512}
2513
2514// glyphs2 uses hex, glyphs3 uses base10
2515fn parse_codepoint_str(s: &str, radix: u32) -> BTreeSet<u32> {
2516    s.split(',')
2517        .map(|cp| u32::from_str_radix(cp, radix).unwrap())
2518        .collect()
2519}
2520
2521/// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L578>
2522fn default_master_idx(raw_font: &mut RawFont) -> usize {
2523    // Prefer an explicit origin
2524    // https://github.com/googlefonts/fontmake-rs/issues/44
2525    if let Some(master_idx) = raw_font
2526        .custom_parameters
2527        .take_string("Variable Font Origin")
2528        .and_then(|origin| {
2529            raw_font
2530                .font_master
2531                .iter()
2532                .position(|master| master.id == origin)
2533        })
2534    {
2535        return master_idx;
2536    }
2537
2538    // No explicit origin, try to pick a winner
2539
2540    // Contenders: (ordinal, words in name) for all masters that have names
2541    let contenders = raw_font
2542        .font_master
2543        .iter()
2544        .enumerate()
2545        .filter_map(|(i, m)| {
2546            m.name
2547                .as_deref()
2548                .map(|name| (i, whitespace_separated_tokens(name)))
2549        })
2550        .collect::<Vec<_>>();
2551
2552    // EARLY EXIT: no contenders, just pick 0
2553    if contenders.is_empty() {
2554        return 0;
2555    }
2556
2557    // In Python find_base_style <https://github.com/googlefonts/glyphsLib/blob/9d5828d874110c42dfc5f542db8eb84f88641eb5/Lib/glyphsLib/builder/axes.py#L652-L663>
2558    let mut common_words = contenders[0].1.clone();
2559    for (_, words) in contenders.iter().skip(1) {
2560        common_words.retain(|w| words.contains(w));
2561    }
2562
2563    // Find the best match:
2564    //   Find the common words in the master names
2565    //   If any master is named exactly that, it wins
2566    //      "Foo Bar" is the best match for {Foo Bar Donkey, Foo Bar Cat, Foo Bar}
2567    //   Otherwise, a master whose name matches the common words if we delete "Regular" wins
2568    //      "Foo Bar Regular" is the best match for {Foo Bar Italic, Foo Bar Majestic, Foo Bar Regular}
2569    let mut best_idx = 0;
2570    for (idx, mut words) in contenders {
2571        // if name exactly matches common words you just win
2572        if *common_words == words {
2573            best_idx = idx;
2574            break;
2575        }
2576
2577        // if our words excluding "Regular" match we're the best
2578        // a subsequent contender could match exactly so we don't win yet
2579        words.retain(|w| *w != "Regular");
2580        if *common_words == words {
2581            best_idx = idx;
2582        }
2583    }
2584    best_idx
2585}
2586
2587fn whitespace_separated_tokens(s: &str) -> Vec<&str> {
2588    s.split_whitespace().collect()
2589}
2590
2591fn axis_index(axes: &[Axis], pred: impl Fn(&Axis) -> bool) -> Option<usize> {
2592    axes.iter()
2593        .enumerate()
2594        .find_map(|(i, a)| if pred(a) { Some(i) } else { None })
2595}
2596
2597fn user_to_design_from_axis_mapping(
2598    from: &mut RawFont,
2599) -> Option<BTreeMap<String, AxisUserToDesignMap>> {
2600    let mappings = from.custom_parameters.take_axis_mappings()?;
2601    let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
2602    for mapping in mappings {
2603        let Some(axis_index) = axis_index(&from.axes, |a| a.tag == mapping.tag) else {
2604            log::warn!(
2605                "axis mapping includes tag {:?} not included in font",
2606                mapping.tag
2607            );
2608            continue;
2609        };
2610        let axis_name = &from.axes.get(axis_index).unwrap().name;
2611        for (user, design) in mapping.user_to_design.iter() {
2612            axis_mappings
2613                .entry(axis_name.clone())
2614                .or_default()
2615                .add_if_new(*user, *design);
2616        }
2617    }
2618    Some(axis_mappings)
2619}
2620
2621fn user_to_design_from_axis_location(
2622    from: &mut RawFont,
2623) -> Option<BTreeMap<String, AxisUserToDesignMap>> {
2624    // glyphsLib only trusts Axis Location when all masters have it, match that
2625    // https://github.com/googlefonts/fontmake-rs/pull/83#discussion_r1065814670
2626    let master_locations: Vec<_> = from
2627        .font_master
2628        .iter_mut()
2629        .filter_map(|m| m.custom_parameters.take_axis_locations())
2630        .collect();
2631    if master_locations.len() != from.font_master.len() {
2632        if !master_locations.is_empty() {
2633            warn!(
2634                "{}/{} masters have Axis Location; ignoring",
2635                master_locations.len(),
2636                from.font_master.len()
2637            );
2638        }
2639        return None;
2640    }
2641
2642    let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
2643    for (master, axis_locations) in from.font_master.iter().zip(master_locations) {
2644        // masters' Axis Locations custom parameters can reference axes that aren't defined in the font,
2645        // so take font axes as the source of truth.
2646        for (idx, axis) in from.axes.iter().enumerate() {
2647            let Some(axis_location) = axis_locations.iter().find(|loc| loc.axis_name == axis.name)
2648            else {
2649                continue;
2650            };
2651            let user = axis_location.location;
2652            let design = master.axes_values[idx];
2653
2654            axis_mappings
2655                .entry(axis_location.axis_name.clone())
2656                .or_default()
2657                .add_if_new(user, design);
2658        }
2659    }
2660    Some(axis_mappings)
2661}
2662
2663impl AxisUserToDesignMap {
2664    fn add_any_new(&mut self, incoming: &AxisUserToDesignMap) {
2665        for (user, design) in incoming.0.iter() {
2666            self.add_if_new(*user, *design);
2667        }
2668    }
2669
2670    fn add_if_new(&mut self, user: OrderedFloat<f64>, design: OrderedFloat<f64>) {
2671        // only keys (input/user-space) should be unique, values (output/design-space) can be duplicated
2672        if self.0.iter().any(|(u, _)| *u == user) {
2673            return;
2674        }
2675        self.0.push((user, design));
2676    }
2677
2678    fn add_identity_map(&mut self, value: OrderedFloat<f64>) {
2679        // no duplicate keys nor values allowed
2680        if self.0.iter().any(|(u, d)| *u == value || *d == value) {
2681            return;
2682        }
2683        self.0.push((value, value));
2684    }
2685
2686    pub fn iter(&self) -> impl Iterator<Item = &(OrderedFloat<f64>, OrderedFloat<f64>)> {
2687        self.0.iter()
2688    }
2689
2690    pub fn is_identity(&self) -> bool {
2691        self.0.iter().all(|(u, d)| u == d)
2692    }
2693}
2694
2695impl UserToDesignMapping {
2696    /// From most to least preferred: Axis Mappings, Axis Location, mappings from instances, assume user == design
2697    ///
2698    /// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L155>
2699    fn new(from: &mut RawFont, instances: &[Instance]) -> Self {
2700        let from_axis_mapping = user_to_design_from_axis_mapping(from);
2701        let from_axis_location = user_to_design_from_axis_location(from);
2702        let (result, incomplete_mapping) = match (from_axis_mapping, from_axis_location) {
2703            (Some(from_mapping), Some(..)) => {
2704                warn!("Axis Mapping *and* Axis Location are defined; using Axis Mapping");
2705                (from_mapping, false)
2706            }
2707            (Some(from_mapping), None) => (from_mapping, false),
2708            (None, Some(from_location)) => (from_location, true),
2709            (None, None) => (BTreeMap::new(), true),
2710        };
2711        let mut result = Self(result);
2712        if incomplete_mapping {
2713            //https://github.com/googlefonts/glyphsLib/blob/682ff4b17/Lib/glyphsLib/builder/axes.py#L251
2714            result.add_instance_mappings_if_new(instances);
2715            if result.0.is_empty() || result.0.values().all(|v| v.is_identity()) {
2716                result.add_master_mappings_if_new(from);
2717            }
2718        }
2719        result
2720    }
2721
2722    pub fn contains(&self, axis_name: &str) -> bool {
2723        self.0.contains_key(axis_name)
2724    }
2725
2726    pub fn get(&self, axis_name: &str) -> Option<&AxisUserToDesignMap> {
2727        self.0.get(axis_name)
2728    }
2729
2730    /// * <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L128>
2731    /// * <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/builder/axes.py#L353>
2732    fn add_instance_mappings_if_new(&mut self, instances: &[Instance]) {
2733        for instance in instances
2734            .iter()
2735            .filter(|i| i.active && i.type_ == InstanceType::Single)
2736        {
2737            for (axis_name, inst_mapping) in instance.axis_mappings.iter() {
2738                self.0
2739                    .entry(axis_name.clone())
2740                    .or_default()
2741                    .add_any_new(inst_mapping);
2742            }
2743        }
2744    }
2745
2746    fn add_master_mappings_if_new(&mut self, from: &RawFont) {
2747        for master in from.font_master.iter() {
2748            for (axis, value) in from.axes.iter().zip(&master.axes_values) {
2749                self.0
2750                    .entry(axis.name.clone())
2751                    .or_default()
2752                    .add_identity_map(*value);
2753            }
2754        }
2755    }
2756}
2757
2758impl TryFrom<RawShape> for Shape {
2759    type Error = Error;
2760
2761    fn try_from(from: RawShape) -> Result<Self, Self::Error> {
2762        // TODO: handle numerous unsupported attributes
2763        // See <https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv3.md#differences-between-version-2>
2764
2765        let shape = if let Some(glyph_name) = from.glyph_name {
2766            assert!(!glyph_name.is_empty(), "A pointless component");
2767
2768            // V3 vs v2: The transform entry has been replaced by angle, pos and scale entries.
2769            let mut transform = if let Some(transform) = from.transform {
2770                Affine::parse_plist(&transform)?
2771            } else {
2772                Affine::IDENTITY
2773            };
2774
2775            // Glyphs 3 gives us {angle, pos, scale}. Glyphs 2 gives us the standard 2x3 matrix.
2776            // The matrix is more general and less ambiguous (what order do you apply the angle, pos, scale?)
2777            // so convert Glyphs 3 to that. Order based on saving the same transformed comonent as
2778            // Glyphs 2 and Glyphs 3 then trying to convert one to the other.
2779            if !from.pos.is_empty() {
2780                if from.pos.len() != 2 {
2781                    return Err(Error::StructuralError(format!("Bad pos: {:?}", from.pos)));
2782                }
2783                transform *= Affine::translate((from.pos[0], from.pos[1]));
2784            }
2785            if let Some(angle) = from.angle {
2786                transform *= normalized_rotation(angle);
2787            }
2788            if !from.scale.is_empty() {
2789                if from.scale.len() != 2 {
2790                    return Err(Error::StructuralError(format!(
2791                        "Bad scale: {:?}",
2792                        from.scale
2793                    )));
2794                }
2795                transform *= Affine::scale_non_uniform(from.scale[0], from.scale[1]);
2796            }
2797
2798            Shape::Component(Component {
2799                name: glyph_name,
2800                transform,
2801                anchor: from.anchor,
2802                attributes: from.attributes,
2803                smart_component_values: from.piece,
2804            })
2805        } else {
2806            // no ref; presume it's a path
2807            Shape::Path(Path {
2808                closed: from.closed.unwrap_or_default(),
2809                nodes: from.nodes.clone(),
2810                attributes: from.attributes,
2811            })
2812        };
2813        Ok(shape)
2814    }
2815}
2816
2817/// Return [kurbo::Affine] rotation around angle (in degrees) with normalized sin/cos.
2818///
2819/// This ensures that for the four "cardinal" rotations (0, 90, 180, 270), the values
2820/// of sin(angle) and cos(angle) are rounded to *exactly* 0.0, +1.0 or -1.0, thus avoiding
2821/// very small values (e.g. 1.2246467991473532e-16) whose effect may be amplified as some
2822/// transformed point coordinates end up close to the 0.5 threshold before being rounded
2823/// to integer later in the build.
2824/// It matches the output of the fontTools' Transform.rotate() used by glyphsLib.
2825/// <https://github.com/fonttools/fonttools/blob/b7509b2/Lib/fontTools/misc/transform.py#L246-L258>
2826fn normalized_rotation(angle_deg: f64) -> Affine {
2827    const ROT_90: Affine = Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]);
2828    const ROT_180: Affine = Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
2829    const ROT_270: Affine = Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]);
2830    // Normalize angle to [0, 360)
2831    let normalized_angle = angle_deg.rem_euclid(360.0);
2832
2833    match normalized_angle {
2834        0.0 => Affine::IDENTITY,
2835        90.0 => ROT_90,
2836        180.0 => ROT_180,
2837        270.0 => ROT_270,
2838        _ => Affine::rotate(angle_deg.to_radians()),
2839    }
2840}
2841
2842fn map_and_push_if_present<T, U>(dest: &mut Vec<T>, src: Vec<U>, map: fn(U) -> T) {
2843    src.into_iter().map(map).for_each(|v| dest.push(v));
2844}
2845
2846impl RawLayer {
2847    fn build(self, format_version: FormatVersion) -> Result<Layer, Error> {
2848        // we do what glyphsLib does:
2849        // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f4/Lib/glyphsLib/classes.py#L3662
2850        // which is apparently standard, in that if a field is missing it has
2851        // some implied default value? Although I don't know where these are
2852        // all documented, outside of glyphsLib.
2853        const DEFAULT_LAYER_WIDTH: f64 = 600.;
2854        let mut shapes = Vec::new();
2855
2856        // Glyphs v2 uses paths and components
2857        map_and_push_if_present(&mut shapes, self.paths, Shape::Path);
2858        map_and_push_if_present(&mut shapes, self.components, Shape::Component);
2859
2860        // Glyphs v3 uses shapes for both
2861        for raw_shape in self.shapes {
2862            shapes.push(raw_shape.try_into()?);
2863        }
2864
2865        let anchors = self
2866            .anchors
2867            .into_iter()
2868            .map(|ra| {
2869                let pos = if let Some(pos) = ra.pos {
2870                    pos
2871                } else if let Some(raw) = ra.position {
2872                    Point::parse_plist(&raw).unwrap()
2873                } else {
2874                    Point::ZERO
2875                };
2876                Anchor { name: ra.name, pos }
2877            })
2878            .collect();
2879
2880        let mut attributes = self.attributes;
2881        // convert v2 bracket layers (based on name) into AxisRule attrs
2882        if let Some(axis_rule) =
2883            AxisRule::from_layer_name(&self.name).filter(|_| format_version.is_v2())
2884        {
2885            assert!(
2886                attributes.axis_rules.is_empty(),
2887                "glyphs v2 does not use axisRules attr"
2888            );
2889            attributes.axis_rules.push(axis_rule);
2890        }
2891        let smart_component_positions = if format_version.is_v2() {
2892            self.user_data.part_selection
2893        } else {
2894            self.part_selection
2895        }
2896        .into_iter()
2897        .map(|(k, v)| match v {
2898            1 => Ok((k, AxisPole::Min)),
2899            2 => Ok((k, AxisPole::Max)),
2900            other => Err(Error::BadValue(format!(
2901                "expected only 1 or 2 in partSelection, found '{other}'"
2902            ))),
2903        })
2904        .collect::<Result<_, _>>()?;
2905        let hints = self
2906            .hints
2907            .iter()
2908            .map(RawHint::to_hint)
2909            .collect::<Result<_, _>>()?;
2910        Ok(Layer {
2911            layer_id: self.layer_id,
2912            associated_master_id: self.associated_master_id,
2913            width: self.width.unwrap_or(DEFAULT_LAYER_WIDTH.into()),
2914            vert_width: self.vert_width,
2915            vert_origin: self.vert_origin,
2916            shapes,
2917            anchors,
2918            attributes,
2919            smart_component_positions,
2920            hints,
2921        })
2922    }
2923}
2924
2925impl RawGlyph {
2926    // we pass in the radix because it depends on the version, stored in the font struct
2927    fn build(self, format_version: FormatVersion, glyph_data: &GlyphData) -> Result<Glyph, Error> {
2928        let mut instances = Vec::new();
2929        let mut bracket_layers = Vec::new();
2930        for mut layer in self.layers {
2931            if layer.is_bracket_layer(format_version) {
2932                bracket_layers.push(layer.build(format_version)?);
2933            } else if !layer.is_draft() {
2934                // it's possible for these to exist even if there's no
2935                // associated master id, in which case we don't care
2936                layer.attributes.axis_rules.clear();
2937                instances.push(layer.build(format_version)?);
2938            }
2939        }
2940        // if category/subcategory were set in the source, we keep them;
2941        // otherwise we look them up based on the bundled GlyphData.
2942        // (we use this info later to determine GDEF categories, zero the width
2943        // on non-spacing marks, etc)
2944        fn parse_category<T>(s: Option<&str>, glyph: &SmolStr) -> Option<T>
2945        where
2946            T: FromStr<Err = SmolStr>,
2947        {
2948            match s.filter(|s| !s.is_empty()).map(T::from_str).transpose() {
2949                Ok(x) => x,
2950                // if we don't know a category ignore it and we'll compute it later
2951                Err(err) => {
2952                    log::warn!("Unknown category '{err}' for glyph '{glyph}'");
2953                    None
2954                }
2955            }
2956        }
2957
2958        let mut category = parse_category(self.category.as_deref(), &self.glyphname);
2959        let mut sub_category = parse_category(self.sub_category.as_deref(), &self.glyphname);
2960        let mut production_name = self.production_name;
2961
2962        let codepoints = self
2963            .unicode
2964            .map(|s| parse_codepoint_str(&s, format_version.codepoint_radix()))
2965            .unwrap_or_default();
2966
2967        if (category.is_none() || sub_category.is_none() || production_name.is_none())
2968            && let Some(result) = glyph_data.query(&self.glyphname, Some(&codepoints))
2969        {
2970            // if they were manually set don't change them, otherwise do
2971            category = category.or(Some(result.category));
2972            sub_category = sub_category.or(result.subcategory);
2973            production_name = production_name.or(result.production_name.map(Into::into));
2974        }
2975
2976        let part_settings = self
2977            .parts_settings
2978            .iter()
2979            .map(|raw| (raw.name.clone(), raw.bottom_value..=raw.top_value))
2980            .collect();
2981
2982        Ok(Glyph {
2983            name: self.glyphname,
2984            export: self.export.unwrap_or(true),
2985            layers: instances,
2986            bracket_layers,
2987            left_kern: self.kern_left,
2988            right_kern: self.kern_right,
2989            unicode: codepoints,
2990            category,
2991            sub_category,
2992            production_name,
2993            smart_component_axes: part_settings,
2994        })
2995    }
2996}
2997
2998// https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/constants.py#L186
2999#[rustfmt::skip]
3000static GLYPHS_TO_OPENTYPE_LANGUAGE_ID: &[(&str, i32)] = &[
3001    ("AFK", 0x0436), ("ARA", 0x0C01), ("ASM", 0x044D), ("AZE", 0x042C), ("BEL", 0x0423),
3002    ("BEN", 0x0845), ("BGR", 0x0402), ("BRE", 0x047E), ("CAT", 0x0403), ("CSY", 0x0405),
3003    ("DAN", 0x0406), ("DEU", 0x0407), ("ELL", 0x0408), ("ENG", 0x0409), ("ESP", 0x0C0A),
3004    ("ETI", 0x0425), ("EUQ", 0x042D), ("FIN", 0x040B), ("FLE", 0x0813), ("FOS", 0x0438),
3005    ("FRA", 0x040C), ("FRI", 0x0462), ("GRN", 0x046F), ("GUJ", 0x0447), ("HAU", 0x0468),
3006    ("HIN", 0x0439), ("HRV", 0x041A), ("HUN", 0x040E), ("HVE", 0x042B), ("IRI", 0x083C),
3007    ("ISL", 0x040F), ("ITA", 0x0410), ("IWR", 0x040D), ("JPN", 0x0411), ("KAN", 0x044B),
3008    ("KAT", 0x0437), ("KAZ", 0x043F), ("KHM", 0x0453), ("KOK", 0x0457), ("LAO", 0x0454),
3009    ("LSB", 0x082E), ("LTH", 0x0427), ("LVI", 0x0426), ("MAR", 0x044E), ("MKD", 0x042F),
3010    ("MLR", 0x044C), ("MLY", 0x043E), ("MNG", 0x0352), ("MTS", 0x043A), ("NEP", 0x0461),
3011    ("NLD", 0x0413), ("NOB", 0x0414), ("ORI", 0x0448), ("PAN", 0x0446), ("PAS", 0x0463),
3012    ("PLK", 0x0415), ("PTG", 0x0816), ("PTG-BR", 0x0416), ("RMS", 0x0417), ("ROM", 0x0418),
3013    ("RUS", 0x0419), ("SAN", 0x044F), ("SKY", 0x041B), ("SLV", 0x0424), ("SQI", 0x041C),
3014    ("SRB", 0x081A), ("SVE", 0x041D), ("TAM", 0x0449), ("TAT", 0x0444), ("TEL", 0x044A),
3015    ("THA", 0x041E), ("TIB", 0x0451), ("TRK", 0x041F), ("UKR", 0x0422), ("URD", 0x0420),
3016    ("USB", 0x042E), ("UYG", 0x0480), ("UZB", 0x0443), ("VIT", 0x042A), ("WEL", 0x0452),
3017    ("ZHH", 0x0C04), ("ZHS", 0x0804), ("ZHT", 0x0404),
3018    ("dflt", 0x0409),
3019];
3020
3021/// Maps Glyphs language codes to OpenType language IDs
3022pub fn glyphs_to_opentype_lang_id(lang: &str) -> Option<u16> {
3023    GLYPHS_TO_OPENTYPE_LANGUAGE_ID
3024        .binary_search_by_key(&lang, |entry| entry.0)
3025        .ok()
3026        .map(|idx| GLYPHS_TO_OPENTYPE_LANGUAGE_ID[idx].1 as u16)
3027}
3028
3029impl RawFeature {
3030    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L43
3031    fn autostr(&self) -> &str {
3032        match self.automatic {
3033            Some(1) => "# automatic\n",
3034            _ => "",
3035        }
3036    }
3037
3038    fn name(&self) -> Result<&str, Error> {
3039        self.name
3040            .as_deref()
3041            .or(self.tag.as_deref())
3042            .ok_or_else(|| Error::StructuralError(format!("{self:?} missing name and tag")))
3043    }
3044
3045    fn disabled(&self) -> bool {
3046        self.disabled == Some(1)
3047    }
3048
3049    /// Some glyphs sources store stylistic set names in the 'note' field
3050    ///
3051    /// See the little sidebar item here:
3052    /// <https://glyphsapp.com/learn/stylistic-sets#g-names-for-stylistic-sets>
3053    fn legacy_name_record_maybe(&self) -> Option<String> {
3054        let name = self.notes.as_deref()?.strip_prefix("Name:")?.trim();
3055        if name.is_empty() {
3056            None
3057        } else {
3058            Some(format!("name 3 1 0x409 \"{name}\";"))
3059        }
3060    }
3061
3062    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L134
3063    fn feature_names(&self) -> String {
3064        let labels = self
3065            .labels
3066            .iter()
3067            .filter_map(|label| label.to_fea())
3068            .chain(self.legacy_name_record_maybe())
3069            .collect::<Vec<_>>()
3070            .join("\n");
3071        if labels.is_empty() {
3072            Default::default()
3073        } else {
3074            format!("featureNames {{\n{labels}\n}};\n")
3075        }
3076    }
3077
3078    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L90
3079    fn prefix_to_feature(&self) -> Result<FeatureSnippet, Error> {
3080        let name = self.name.as_deref().unwrap_or_default();
3081        let code = format!("# Prefix: {}\n{}{}", name, self.autostr(), self.code);
3082        Ok(FeatureSnippet::new(code, self.disabled()))
3083    }
3084
3085    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L101
3086    fn class_to_feature(&self) -> Result<FeatureSnippet, Error> {
3087        let name = self.name()?;
3088        let code = format!(
3089            "{}{}{name} = [ {}\n];",
3090            self.autostr(),
3091            if name.starts_with('@') { "" } else { "@" },
3092            self.code
3093        );
3094        Ok(FeatureSnippet::new(code, self.disabled()))
3095    }
3096
3097    // https://github.com/googlefonts/glyphsLib/blob/24b4d340e4c82948ba121dcfe563c1450a8e69c9/Lib/glyphsLib/builder/features.py#L113
3098    fn raw_feature_to_feature(&self) -> Result<FeatureSnippet, Error> {
3099        let name = self.name()?;
3100        let insert_mark = self.insert_mark_if_manual_kern_feature();
3101        let code = format!(
3102            "feature {name} {{\n{}{}{}{insert_mark}\n}} {name};",
3103            self.autostr(),
3104            self.feature_names(),
3105            self.code
3106        );
3107        Ok(FeatureSnippet::new(code, self.disabled()))
3108    }
3109
3110    //https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f4/Lib/glyphsLib/builder/features.py#L180
3111    fn insert_mark_if_manual_kern_feature(&self) -> &str {
3112        if self.name().unwrap_or_default() == "kern"
3113            && self.automatic != Some(1)
3114            && !self.code.contains("# Automatic Code")
3115        {
3116            "# Automatic Code\n"
3117        } else {
3118            ""
3119        }
3120    }
3121}
3122
3123impl RawNameValue {
3124    fn to_fea(&self) -> Option<String> {
3125        if self.value.is_empty() {
3126            // skip empty names:
3127            // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f/Lib/glyphsLib/builder/features.py#L155
3128            return None;
3129        }
3130
3131        let Some(language_id) = glyphs_to_opentype_lang_id(&self.language) else {
3132            warn!("Unknown feature label language: {}", self.language);
3133            return None;
3134        };
3135        let name = self.value.replace("\\", "\\005c").replace("\"", "\\0022");
3136        Some(format!("  name 3 1 0x{language_id:04X} \"{name}\";"))
3137    }
3138}
3139
3140/// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/classes.py#L220-L249>
3141fn lookup_class_value(axis_tag: &str, user_class: &str) -> Option<u16> {
3142    let user_class = match user_class {
3143        value if !value.is_empty() => {
3144            let mut value = value.to_ascii_lowercase();
3145            value.retain(|c| c != ' ');
3146            value
3147        }
3148        _ => String::from(""),
3149    };
3150    match (axis_tag, user_class.as_str()) {
3151        ("wght", "thin") => Some(100),
3152        ("wght", "extralight" | "ultralight") => Some(200),
3153        ("wght", "light") => Some(300),
3154        ("wght", "" | "normal" | "regular") => Some(400),
3155        ("wght", "medium") => Some(500),
3156        ("wght", "demibold" | "semibold") => Some(600),
3157        ("wght", "bold") => Some(700),
3158        ("wght", "ultrabold" | "extrabold") => Some(800),
3159        ("wght", "black" | "heavy") => Some(900),
3160        ("wdth", "ultracondensed") => Some(1),
3161        ("wdth", "extracondensed") => Some(2),
3162        ("wdth", "condensed") => Some(3),
3163        ("wdth", "semicondensed") => Some(4),
3164        ("wdth", "" | "Medium (normal)") => Some(5),
3165        ("wdth", "semiexpanded") => Some(6),
3166        ("wdth", "expanded") => Some(7),
3167        ("wdth", "extraexpanded") => Some(8),
3168        ("wdth", "ultraexpanded") => Some(9),
3169        _ => {
3170            warn!("Unrecognized ('{axis_tag}', '{user_class}')");
3171            None
3172        }
3173    }
3174}
3175
3176fn add_mapping_if_new(
3177    axis_mappings: &mut BTreeMap<String, AxisUserToDesignMap>,
3178    axes: &[Axis],
3179    axis_tag: &str,
3180    axes_values: &[OrderedFloat<f64>],
3181    value: f64,
3182) {
3183    let Some(idx) = axes.iter().position(|a| a.tag == axis_tag) else {
3184        return;
3185    };
3186    let axis = &axes[idx];
3187    let Some(design) = axes_values.get(idx) else {
3188        return;
3189    };
3190
3191    axis_mappings
3192        .entry(axis.name.clone())
3193        .or_default()
3194        .add_if_new(value.into(), *design);
3195}
3196
3197impl Instance {
3198    /// Glyphs 2 instances have fun fields.
3199    ///
3200    /// Mappings based on
3201    /// <https://github.com/googlefonts/glyphsLib/blob/6f243c1f732ea1092717918d0328f3b5303ffe56/Lib/glyphsLib/classes.py#L3451>
3202    fn new(
3203        axes: &[Axis],
3204        value: &mut RawInstance,
3205        masters_have_axis_locations: bool,
3206    ) -> Result<Self, Error> {
3207        let active = value.is_active();
3208        let mut axis_mappings: BTreeMap<String, AxisUserToDesignMap> = BTreeMap::new();
3209
3210        // Instances can also have "Axis Location" custom parameters, complementing the ones
3211        // from the masters: https://github.com/googlefonts/fontc/issues/918
3212        let mut tags_done = BTreeSet::new();
3213        for axis_location in value
3214            .custom_parameters
3215            .take_axis_locations()
3216            .unwrap_or_default()
3217        {
3218            let Some(axis_index) = axis_index(axes, |a| a.name == axis_location.axis_name) else {
3219                log_once_warn!(
3220                    "{} instance's 'Axis Location' includes axis {:?} not included in font",
3221                    value.name,
3222                    axis_location.axis_name
3223                );
3224                continue;
3225            };
3226            let user = axis_location.location;
3227            let design = value.axes_values[axis_index];
3228
3229            axis_mappings
3230                .entry(axis_location.axis_name.clone())
3231                .or_default()
3232                .add_if_new(user, design);
3233
3234            tags_done.insert(axes[axis_index].tag.as_str());
3235        }
3236
3237        // only infer legacy mappings when Axis Locations aren't defined
3238        if !masters_have_axis_locations && !tags_done.contains("wght") {
3239            // OS/2 weight_class corresponds to 'wght' axis user-space value
3240            add_mapping_if_new(
3241                &mut axis_mappings,
3242                axes,
3243                "wght",
3244                &value.axes_values,
3245                value
3246                    .weight_class
3247                    .as_ref()
3248                    .map(|v| f64::from_str(v).unwrap())
3249                    .unwrap_or(400.0),
3250            );
3251        }
3252
3253        if !masters_have_axis_locations && !tags_done.contains("wdth") {
3254            // OS/2 width_class gets mapped to 'wdth' percent scale, see:
3255            // https://github.com/googlefonts/glyphsLib/blob/7041311e/Lib/glyphsLib/builder/constants.py#L222
3256            add_mapping_if_new(
3257                &mut axis_mappings,
3258                axes,
3259                "wdth",
3260                value.axes_values.as_ref(),
3261                value
3262                    .width_class
3263                    .as_ref()
3264                    .map(|v| match WidthClass::try_from(u16::from_str(v).unwrap()) {
3265                        Ok(width_class) => width_class.to_percent(),
3266                        Err(err) => {
3267                            warn!("{err}");
3268                            100.0
3269                        }
3270                    })
3271                    .unwrap_or(100.0),
3272            );
3273        }
3274
3275        Ok(Instance {
3276            name: value.name.clone(),
3277            active,
3278            type_: value
3279                .type_
3280                .as_ref()
3281                .map(|v| v.as_str().into())
3282                .unwrap_or(InstanceType::Single),
3283            axis_mappings,
3284            axes_values: value.axes_values.clone(),
3285            properties: value.properties.clone(),
3286            custom_parameters: value.custom_parameters.to_custom_params()?,
3287        })
3288    }
3289
3290    fn family_name(&self) -> Option<&str> {
3291        self.properties
3292            .iter()
3293            .find(|raw| raw.key == "familyNames")
3294            .and_then(RawName::get_value)
3295        // glyphsLib here also checks for 'familyName' in customParams, but we
3296        // don't have that key? Possible that we're ignoring it in the raw params
3297        // conversion..
3298        // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577/Lib/glyphsLib/classes.py#L3271
3299    }
3300
3301    /// Get the optional postscript name to use for the `fvar` named instance.
3302    pub fn postscript_name(&self) -> Option<&str> {
3303        // https://handbook.glyphsapp.com/custom-parameter-descriptions/
3304        // https://github.com/googlefonts/glyphsLib/blob/7a8f643a711ffb9d351d037425a7eb60759e6219/Lib/glyphsLib/builder/instances.py#L118-L121
3305        ["variablePostscriptFontName", "postscriptFontName"]
3306            .iter()
3307            .find_map(|key| {
3308                self.properties
3309                    .iter()
3310                    .find(|raw| raw.key == *key)
3311                    .and_then(RawName::get_value)
3312            })
3313    }
3314}
3315
3316/// Glyphs appears to use code page identifiers rather than bits
3317///
3318/// <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#ulcodepagerange>
3319fn codepage_range_bit(codepage: u32) -> Result<u32, Error> {
3320    Ok(match codepage {
3321        1252 => 0,  // Latin 1
3322        1250 => 1,  // Latin 2: Eastern Europe
3323        1251 => 2,  // Cyrillic
3324        1253 => 3,  // Greek
3325        1254 => 4,  // Turkish
3326        1255 => 5,  // Hebrew
3327        1256 => 6,  // Arabic
3328        1257 => 7,  // Windows Baltic
3329        1258 => 8,  // Vietnamese
3330        874 => 16,  // Thai
3331        932 => 17,  // JIS/Japan
3332        936 => 18,  // Chinese: Simplified PRC and Singapore
3333        949 => 19,  // Korean Wansung
3334        950 => 20,  // Chinese: Traditional Taiwan and Hong Kong SAR
3335        1361 => 21, // Korean Johab
3336        869 => 48,  // IBM Greek
3337        866 => 49,  // MS-DOS Russian
3338        865 => 50,  // MS-DOS Nordic
3339        864 => 51,  // Arabic
3340        863 => 52,  //	MS-DOS Canadian French
3341        862 => 53,  //		Hebrew
3342        861 => 54,  //		MS-DOS Icelandic
3343        860 => 55,  //		MS-DOS Portuguese
3344        857 => 56,  //		IBM Turkish
3345        855 => 57,  //	IBM Cyrillic; primarily Russian
3346        852 => 58,  //		Latin 2
3347        775 => 59,  //		MS-DOS Baltic
3348        737 => 60,  //	Greek; former 437 G
3349        708 => 61,  //	Arabic; ASMO 708
3350        850 => 62,  //	WE/Latin 1
3351        437 => 63,  //	US
3352
3353        v if v < 64 => v, // an actual bit
3354        _ => return Err(Error::InvalidCodePage(codepage)),
3355    })
3356}
3357
3358fn update_names(all_names: &mut BTreeMap<String, Vec<(String, String)>>, raw_names: &[RawName]) {
3359    for name in raw_names {
3360        // Collect ALL language values into IndexMap to deduplicate (last occurrence wins)
3361        // while preserving insertion order. This is critical for the fallback behavior:
3362        // when there's no dflt/default/ENG, the FIRST entry becomes the English fallback,
3363        // see `return list(self._localized_values.values())[0]` in glyphsLib:
3364        // https://github.com/googlefonts/glyphsLib/blob/f90e406/Lib/glyphsLib/classes.py#L3162
3365        let mut lang_map: IndexMap<String, String> = IndexMap::new();
3366
3367        // Handle old-style single value (v2 format); we treat as dflt
3368        if let Some(value) = &name.value {
3369            lang_map.insert("dflt".to_string(), value.clone());
3370        }
3371
3372        // Handle new-style localized values (v3 format)
3373        for (lang, val) in name.localized_values() {
3374            lang_map.insert(lang.to_string(), val.to_string());
3375        }
3376
3377        if !lang_map.is_empty() {
3378            let values: Vec<_> = lang_map.into_iter().collect();
3379            all_names.insert(name.key.clone(), values);
3380        }
3381    }
3382}
3383
3384impl TryFrom<RawFont> for Font {
3385    type Error = Error;
3386
3387    fn try_from(mut from: RawFont) -> Result<Self, Self::Error> {
3388        if from.format_version.is_v2() {
3389            from.v2_to_v3()?;
3390        } else {
3391            // <https://github.com/googlefonts/fontc/issues/1029>
3392            from.v2_to_v3_names()?;
3393        }
3394
3395        // TODO: this should be provided in a manner that allows for overrides
3396        let glyph_data = GlyphData::default();
3397
3398        // originally glyphsLib would infer wght/wdth mappings from the instance weight/width classes;
3399        // then, a new "Axis Location" custom parameter was defined for both masters and instances;
3400        // when (all) masters use the latter, only explicit "Axis Location" parameters get used from
3401        // instances, and the legacy heuristic for wght/wdth is disabled, so we need to keep track
3402        // of this when building instance axis mappings:
3403        // https://github.com/googlefonts/glyphsLib/blob/7a8f643a/Lib/glyphsLib/builder/axes.py#L225-L227
3404        let masters_have_axis_locations = from
3405            .font_master
3406            .iter()
3407            .all(|master| master.custom_parameters.contains("Axis Location"));
3408
3409        let instances: Vec<_> = from
3410            .instances
3411            .iter_mut()
3412            .map(|ri| Instance::new(&from.axes, ri, masters_have_axis_locations))
3413            .collect::<Result<Vec<_>, Error>>()?;
3414
3415        // parameters like "Axis Location", "Axis Mappings" and "Variable Font Origin" are
3416        // handled separately from to_custom_params() and need to be taken before the latter
3417        // is called, to avoid spurious "unknown custom parameter" warnings
3418        // https://github.com/googlefonts/fontc/issues/1682
3419        let axis_mappings = UserToDesignMapping::new(&mut from, &instances);
3420        let default_master_idx = default_master_idx(&mut from);
3421
3422        let mut custom_parameters = from.custom_parameters.to_custom_params()?;
3423
3424        let glyph_order = make_glyph_order(&from.glyphs, custom_parameters.glyph_order.take());
3425
3426        let mut glyphs = BTreeMap::new();
3427        for raw_glyph in from.glyphs.into_iter() {
3428            let mut glyph = raw_glyph.build(from.format_version, &glyph_data)?;
3429            // v2 bracket layers can only reference a single axis position (on the
3430            // first axis in the font) so we add empty axis rules for any other
3431            // axes.
3432            for layer in glyph.bracket_layers.iter_mut() {
3433                layer
3434                    .attributes
3435                    .axis_rules
3436                    .resize(from.axes.len(), Default::default());
3437            }
3438            glyphs.insert(glyph.name.clone(), glyph);
3439        }
3440
3441        let mut features = Vec::new();
3442        for class in from.classes {
3443            features.push(class.class_to_feature()?);
3444        }
3445        for prefix in from.feature_prefixes {
3446            features.push(prefix.prefix_to_feature()?);
3447        }
3448        for feature in from.features {
3449            features.push(feature.raw_feature_to_feature()?);
3450        }
3451
3452        let units_per_em = from.units_per_em.ok_or(Error::NoUnitsPerEm)?;
3453        let units_per_em = units_per_em.try_into().map_err(Error::InvalidUpem)?;
3454
3455        let mut all_names = BTreeMap::new();
3456        update_names(&mut all_names, &from.properties);
3457        for instance in &instances {
3458            if instance.active
3459                && instance.type_ == InstanceType::Variable
3460                    // glyphsLib has instance.familyName return the root familyName
3461                    // if it has no other value; we just unwrap_or_true here
3462                    // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577/Lib/glyphsLib/classes.py#L3272
3463                && instance.family_name().map(|name| name == from.family_name.as_str()).unwrap_or(true)
3464            {
3465                update_names(&mut all_names, &instance.properties);
3466            }
3467        }
3468        // Insert RawFont.family_name as the default familyNames where downstream code
3469        // expects to find it
3470        let family_names = all_names
3471            .entry("familyNames".into())
3472            .or_insert_with(Vec::new);
3473        family_names.retain(|(lang, _)| lang != "dflt");
3474        family_names.insert(0, ("dflt".to_string(), from.family_name.clone()));
3475        // Rename versionString to version
3476        if let Some(version_values) = all_names.remove("versionString") {
3477            all_names.insert("version".into(), version_values);
3478        }
3479
3480        let metric_names: BTreeMap<usize, String> = from
3481            .metrics
3482            .into_iter()
3483            .enumerate()
3484            .map(|(idx, metric)| (idx, metric.type_))
3485            .collect();
3486
3487        let master_ids_to_names: IndexMap<_, _> = from
3488            .font_master
3489            .iter()
3490            .map(|m| (m.id.clone(), m.name.clone()))
3491            .collect();
3492
3493        let masters = from
3494            .font_master
3495            .into_iter()
3496            .map(|m| {
3497                let custom_parameters = m.custom_parameters.to_custom_params()?;
3498                let metrics_source_id =
3499                    resolve_metrics_source_id(&custom_parameters, &master_ids_to_names);
3500                Ok(FontMaster {
3501                    id: m.id,
3502                    name: m.name.unwrap_or_default(),
3503                    axes_values: m.axes_values,
3504                    metric_values: m
3505                        .metric_values
3506                        .into_iter()
3507                        .enumerate()
3508                        .filter_map(|(idx, value)| {
3509                            metric_names.get(&idx).map(|name| (name.clone(), value))
3510                        })
3511                        .fold(BTreeMap::new(), |mut acc, (name, value)| {
3512                            // only insert a metric if one with the same name hasn't been added
3513                            // yet; matches glyphsLib's behavior where the first duplicate wins
3514                            // https://github.com/googlefonts/fontc/issues/1269
3515                            acc.entry(name).or_insert(value.into());
3516                            acc
3517                        }),
3518                    number_values: from
3519                        .numbers
3520                        .iter()
3521                        .zip(m.number_values.iter())
3522                        .map(|(k, v)| (k.name.clone(), *v))
3523                        .collect(),
3524                    custom_parameters,
3525                    user_data: m.user_data,
3526                    metrics_source_id,
3527                })
3528            })
3529            .collect::<Result<Vec<_>, Error>>()?;
3530
3531        let virtual_masters = custom_parameters.virtual_masters.take().unwrap_or_default();
3532        Ok(Font {
3533            units_per_em,
3534            axes: from.axes,
3535            masters,
3536            default_master_idx,
3537            glyphs,
3538            glyph_order,
3539            axis_mappings,
3540            virtual_masters,
3541            features,
3542            all_names,
3543            instances,
3544            version_major: from.versionMajor.unwrap_or_default() as i32,
3545            version_minor: from.versionMinor.unwrap_or_default() as u32,
3546            date: from.date,
3547            kerning_ltr: from.kerning_LTR,
3548            kerning_rtl: from.kerning_RTL,
3549            custom_parameters,
3550        })
3551    }
3552}
3553
3554fn preprocess_unparsed_plist(s: &str) -> Cow<'_, str> {
3555    // Glyphs has a wide variety of unicode definitions, not all of them parser friendly
3556    // Make unicode always a string, without any wrapping () so we can parse as csv, radix based on format version
3557    let unicode_re =
3558        Regex::new(r"(?m)^(?P<prefix>\s*unicode\s*=\s*)[(]?(?P<value>[0-9a-zA-Z,]+)[)]?;\s*$")
3559            .unwrap();
3560    unicode_re.replace_all(s, r#"$prefix"$value";"#)
3561}
3562
3563fn variable_instance_for<'a>(instances: &'a [Instance], name: &str) -> Option<&'a Instance> {
3564    instances
3565        .iter()
3566        .find(|i| i.active && i.type_ == InstanceType::Variable && i.name == name)
3567}
3568
3569impl Font {
3570    pub fn load_from_string(data: &str) -> Result<Font, Error> {
3571        let raw_font = RawFont::load_from_string(data)?;
3572        let mut font = Font::try_from(raw_font)?;
3573        font.preprocess()?;
3574        Ok(font)
3575    }
3576
3577    pub fn load(glyphs_file: &path::Path) -> Result<Font, Error> {
3578        let mut font = Self::load_raw(glyphs_file)?;
3579        font.preprocess()?;
3580        Ok(font)
3581    }
3582
3583    pub fn color_palettes(&self) -> Option<&Vec<Vec<Color>>> {
3584        self.default_master()
3585            .custom_parameters
3586            .color_palettes
3587            .as_ref()
3588            .or(self.custom_parameters.color_palettes.as_ref())
3589    }
3590
3591    pub fn default_palette(&self) -> Option<&[Color]> {
3592        self.color_palettes().map(|p| p[0].as_slice())
3593    }
3594
3595    fn preprocess(&mut self) -> Result<(), Error> {
3596        // ensure that glyphs with components that have bracket layers
3597        // also have bracket layers.
3598        self.align_bracket_layers();
3599
3600        // propagate anchors by default unless explicitly set to false
3601        if self.custom_parameters.propagate_anchors.unwrap_or(true) {
3602            self.propagate_all_anchors();
3603        }
3604        // if any glyphs reference smart components, convert them to outlines.
3605        // (see https://glyphsapp.com/learn/smart-components)
3606        self.instantiate_all_smart_components()?;
3607
3608        // if any glyphs have corner component hints, insert them into the paths
3609        self.insert_all_corner_components()
3610    }
3611
3612    /// if a glyph has components that have alternate layers, copy the layer
3613    /// locations into the glyph.
3614    ///
3615    /// This is required to correctly handle bracket glyphs.
3616    // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d/Lib/glyphsLib/builder/bracket_layers.py#L176
3617    fn align_bracket_layers(&mut self) {
3618        // python does this recursively but we'll presort instead
3619        let todo = self.depth_sorted_composite_glyphs();
3620
3621        for name in &todo {
3622            let mut needed = IndexMap::new();
3623            let glyph = self.glyphs.get(name).unwrap();
3624            let my_bracket_layers = glyph
3625                .bracket_layers
3626                .iter()
3627                .map(|l| l.bracket_info(&self.axes))
3628                .collect::<IndexSet<_>>();
3629            for layer in &glyph.layers {
3630                for comp in layer.components() {
3631                    let comp_glyph = self.glyphs.get(&comp.name).unwrap();
3632
3633                    let comp_bracket_layers = comp_glyph
3634                        .bracket_layers
3635                        .iter()
3636                        .map(|l| l.bracket_info(&self.axes))
3637                        .collect::<IndexSet<_>>();
3638                    if comp_bracket_layers != my_bracket_layers {
3639                        let missing = comp_bracket_layers.difference(&my_bracket_layers);
3640                        needed
3641                            .entry(layer.layer_id.clone())
3642                            .or_insert(IndexSet::new())
3643                            .extend(missing.cloned());
3644                    }
3645                }
3646            }
3647
3648            if needed.is_empty() {
3649                // all good, next glyph
3650                continue;
3651            }
3652
3653            let mut new_layers = Vec::new();
3654            for (master, needed_brackets) in needed {
3655                let master_name = self
3656                    .masters
3657                    .iter()
3658                    .find_map(|m| (m.id == master).then_some(m.name.as_str()))
3659                    .unwrap_or(master.as_str());
3660                if !my_bracket_layers.is_empty() {
3661                    log::warn!(
3662                        "Glyph {name} in master {master_name} has different bracket layers \
3663                        than its components. This is not supported, so some bracket layers \
3664                        will not be applied. Consider fixing the source instead."
3665                    );
3666                }
3667
3668                let base_layer = glyph.layers.iter().find(|l| l.layer_id == master).unwrap();
3669                for box_ in needed_brackets {
3670                    log::debug!("synthesized layer {box_:?} in master {master_name} for '{name}'",);
3671                    let new_layer = synthesize_bracket_layer(base_layer, box_, &self.axes);
3672                    new_layers.push(new_layer);
3673                }
3674            }
3675
3676            self.glyphs
3677                .get_mut(name)
3678                .unwrap()
3679                .bracket_layers
3680                .extend_from_slice(&new_layers);
3681        }
3682    }
3683
3684    // smart components get converted into normal paths before IR.
3685    // see https://glyphsapp.com/learn/smart-components
3686    fn instantiate_all_smart_components(&mut self) -> Result<(), Error> {
3687        // find all the glyphs that include smart components
3688        let mut glyphs_with_smart_components = self
3689            .glyphs
3690            .values()
3691            .filter(|glyph| {
3692                glyph
3693                    .layers
3694                    .iter()
3695                    .flat_map(|layer| layer.components())
3696                    .any(|comp| {
3697                        //https://github.com/googlefonts/glyphsLib/blob/52c982399b/Lib/glyphsLib/builder/components.py#L77
3698                        !comp.smart_component_values.is_empty()
3699                            && self
3700                                .glyphs
3701                                .get(&comp.name)
3702                                .map(|ref_glyph| !ref_glyph.smart_component_axes.is_empty())
3703                                .unwrap_or(false)
3704                    })
3705            })
3706            .cloned()
3707            .collect::<Vec<_>>();
3708
3709        if glyphs_with_smart_components.is_empty() {
3710            return Ok(());
3711        }
3712        // then make sure we do everything depth-first, so that if any smart components
3713        // contain _other_ smart components, those are instantiated first.
3714        let depth_ranked = self
3715            .depth_sorted_composite_glyphs()
3716            .into_iter()
3717            .enumerate()
3718            .map(|(rank, name)| (name, rank))
3719            .collect::<HashMap<_, _>>();
3720
3721        glyphs_with_smart_components.sort_by_key(|g| depth_ranked.get(&g.name).unwrap());
3722
3723        // convert the smart components to normal outlines, per-glyph
3724        for mut glyph in glyphs_with_smart_components {
3725            for layer in glyph.layers.iter_mut() {
3726                let old_shapes = std::mem::take(&mut layer.shapes);
3727                let mut new_shapes = Vec::new();
3728                for shape in old_shapes {
3729                    if let Some(comp) = shape.as_smart_component()
3730                        && let Some(ref_glyph) = self
3731                            .glyphs
3732                            .get(&comp.name)
3733                            .filter(|g| !g.smart_component_axes.is_empty())
3734                    {
3735                        log::debug!(
3736                            "instantiating smart component '{}' for '{}'",
3737                            comp.name,
3738                            glyph.name
3739                        );
3740                        new_shapes.extend(
3741                            crate::smart_components::instantiate_for_layer(
3742                                layer.master_id(),
3743                                comp,
3744                                ref_glyph,
3745                            )
3746                            .map_err(|issue| {
3747                                Error::BadSmartComponent {
3748                                    glyph: glyph.name.clone(),
3749                                    component: comp.name.clone(),
3750                                    issue,
3751                                }
3752                            })?,
3753                        );
3754                    } else {
3755                        layer.shapes.push(shape);
3756                    }
3757                }
3758                layer.shapes.extend(new_shapes);
3759            }
3760            // replace the old glyph with the new, component-free glyph
3761            self.glyphs.insert(glyph.name.clone(), glyph);
3762        }
3763        Ok(())
3764    }
3765
3766    // insert corner components into glyphs that have them
3767    fn insert_all_corner_components(&mut self) -> Result<(), Error> {
3768        // Find all glyphs that have corner component hints
3769        let glyphs_with_corners: Vec<_> = self
3770            .glyphs
3771            .values()
3772            .filter(|glyph| {
3773                glyph
3774                    .layers
3775                    .iter()
3776                    .chain(glyph.bracket_layers.iter())
3777                    .any(|layer| {
3778                        layer
3779                            .hints
3780                            .iter()
3781                            .any(|hint| hint.type_ == HintType::Corner)
3782                    })
3783            })
3784            .cloned()
3785            .collect();
3786
3787        if glyphs_with_corners.is_empty() {
3788            return Ok(());
3789        }
3790
3791        for mut glyph in glyphs_with_corners {
3792            for layer in glyph
3793                .layers
3794                .iter_mut()
3795                .chain(glyph.bracket_layers.iter_mut())
3796            {
3797                crate::corner_components::insert_corner_components_for_layer(layer, &self.glyphs)
3798                    .map_err(|issue| Error::BadCornerComponent {
3799                    glyph: glyph.name.clone(),
3800                    issue,
3801                })?;
3802            }
3803
3804            self.glyphs.insert(glyph.name.clone(), glyph);
3805        }
3806
3807        Ok(())
3808    }
3809
3810    // load without propagating anchors or components
3811    pub(crate) fn load_raw(glyphs_file: impl AsRef<path::Path>) -> Result<Font, Error> {
3812        RawFont::load(glyphs_file.as_ref()).and_then(Font::try_from)
3813    }
3814
3815    pub fn default_master(&self) -> &FontMaster {
3816        &self.masters[self.default_master_idx]
3817    }
3818
3819    /// <https://handbook.glyphsapp.com/exports/>
3820    pub fn variable_export_settings(&self, master: &FontMaster) -> Option<&Instance> {
3821        variable_instance_for(&self.instances, &master.name)
3822    }
3823
3824    /// Get the default (English) value for a name property.
3825    ///
3826    /// Searches for a value with language "dflt", "default", or "ENG" in the all_names field,
3827    /// in order of preference: dflt > default > ENG > first value.
3828    /// Returns None if the property key doesn't exist or has no values.
3829    pub fn get_default_name(&self, key: &str) -> Option<&str> {
3830        let values = self.all_names.get(key)?;
3831        let scale = values.len() as i32;
3832        values
3833            .iter()
3834            .enumerate()
3835            .map(|(i, (lang, val))| {
3836                (
3837                    default_language_score(lang.as_str(), i, scale),
3838                    val.as_str(),
3839                )
3840            })
3841            .min()
3842            .map(|(_, val)| val)
3843    }
3844
3845    /// Get all localized values for a name property.
3846    ///
3847    /// Returns an iterator of (language code, value) tuples. Default languages
3848    /// ("dflt", "default", "ENG") are excluded.
3849    pub fn get_localized_names(&self, key: &str) -> impl Iterator<Item = (&str, &str)> + '_ {
3850        self.all_names
3851            .get(key)
3852            .into_iter()
3853            .flat_map(|v| v.iter())
3854            .filter(|(lang, _)| !matches!(lang.as_str(), "dflt" | "default" | "ENG"))
3855            .map(|(lang, val)| (lang.as_str(), val.as_str()))
3856    }
3857
3858    /// Iterate over all default name entries.
3859    ///
3860    /// Returns an iterator of (property key, default value) tuples.
3861    /// Uses priority order: dflt > default > ENG > first value.
3862    pub fn default_names(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
3863        self.all_names
3864            .keys()
3865            .filter_map(|key| self.get_default_name(key).map(|val| (key.as_str(), val)))
3866    }
3867
3868    /// Iterate over all localized name entries.
3869    ///
3870    /// Returns an iterator of (property key, language code, value) tuples.
3871    /// Default languages ("dflt", "default", "ENG") are excluded.
3872    pub fn localized_names(&self) -> impl Iterator<Item = (&str, &str, &str)> + '_ {
3873        self.all_names.iter().flat_map(|(key, values)| {
3874            values
3875                .iter()
3876                .filter(|(lang, _)| !matches!(lang.as_str(), "dflt" | "default" | "ENG"))
3877                .map(move |(lang, val)| (key.as_str(), lang.as_str(), val.as_str()))
3878        })
3879    }
3880
3881    pub fn vendor_id(&self) -> Option<&str> {
3882        self.get_default_name("vendorID")
3883    }
3884}
3885
3886//https://github.com/googlefonts/glyphsLib/blob/c4db6b981d/Lib/glyphsLib/builder/bracket_layers.py#L258
3887fn synthesize_bracket_layer(
3888    old_layer: &Layer,
3889    box_: BTreeMap<String, (Option<i64>, Option<i64>)>,
3890    axes: &[Axis],
3891) -> Layer {
3892    let mut new_layer = old_layer.clone();
3893    new_layer.associated_master_id = Some(std::mem::take(&mut new_layer.layer_id));
3894    new_layer.attributes.axis_rules = axes
3895        .iter()
3896        .map(|axis| {
3897            if let Some((min, max)) = box_.get(&axis.tag) {
3898                AxisRule {
3899                    min: min.map(|x| x as _),
3900                    max: max.map(|x| x as _),
3901                }
3902            } else {
3903                Default::default()
3904            }
3905        })
3906        .collect();
3907
3908    new_layer
3909}
3910
3911/// Convert [kurbo::Point] to this for eq and hash/
3912#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3913struct PointForEqAndHash {
3914    x: OrderedFloat<f64>,
3915    y: OrderedFloat<f64>,
3916}
3917
3918impl PointForEqAndHash {
3919    fn new(point: Point) -> PointForEqAndHash {
3920        point.into()
3921    }
3922}
3923
3924impl From<Point> for PointForEqAndHash {
3925    fn from(value: Point) -> Self {
3926        PointForEqAndHash {
3927            x: value.x.into(),
3928            y: value.y.into(),
3929        }
3930    }
3931}
3932
3933/// Convert [kurbo::Affine] to this for eq and hash/
3934#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3935struct AffineForEqAndHash([OrderedFloat<f64>; 6]);
3936
3937impl From<Affine> for AffineForEqAndHash {
3938    fn from(value: Affine) -> Self {
3939        Self(value.as_coeffs().map(|coeff| coeff.into()))
3940    }
3941}
3942
3943#[cfg(test)]
3944mod tests {
3945    use super::*;
3946    use crate::{Font, FontMaster, Node, Shape, plist::FromPlist, smart_components};
3947    use std::{
3948        collections::{BTreeMap, BTreeSet, HashSet},
3949        path::{Path, PathBuf},
3950    };
3951
3952    use ordered_float::OrderedFloat;
3953
3954    use pretty_assertions::assert_eq;
3955
3956    use kurbo::{Affine, Point, Rect};
3957
3958    use rstest::rstest;
3959    use smol_str::ToSmolStr;
3960
3961    fn testdata_dir() -> PathBuf {
3962        // working dir varies CLI vs VSCode
3963        let mut dir = Path::new("../resources/testdata");
3964        if !dir.is_dir() {
3965            dir = Path::new("./resources/testdata");
3966        }
3967        assert!(dir.is_dir());
3968        dir.to_path_buf()
3969    }
3970
3971    fn glyphs2_dir() -> PathBuf {
3972        testdata_dir().join("glyphs2")
3973    }
3974
3975    fn glyphs3_dir() -> PathBuf {
3976        testdata_dir().join("glyphs3")
3977    }
3978
3979    fn round(transform: Affine, digits: u8) -> Affine {
3980        let m = 10f64.powi(digits as i32);
3981        let mut coeffs = transform.as_coeffs();
3982        for c in coeffs.iter_mut() {
3983            *c = (*c * m).round() / m;
3984        }
3985        Affine::new(coeffs)
3986    }
3987
3988    #[test]
3989    fn v2_format_version() {
3990        let v2_font = glyphs2_dir().join("Mono.glyphs");
3991        let as_str = std::fs::read_to_string(&v2_font).unwrap();
3992        assert!(!as_str.contains(".formatVersion"), "only exists in v3");
3993        let font = RawFont::load(&v2_font).unwrap();
3994        // falls back to default
3995        assert_eq!(font.format_version, FormatVersion::V2);
3996    }
3997
3998    #[test]
3999    fn v3_format_version() {
4000        let v3_font = glyphs3_dir().join("MasterNames.glyphs");
4001        let as_str = std::fs::read_to_string(&v3_font).unwrap();
4002        assert!(as_str.contains(".formatVersion"), "exists in v3");
4003        let font = RawFont::load(&v3_font).unwrap();
4004        // falls back to default
4005        assert_eq!(font.format_version, FormatVersion::V3);
4006    }
4007
4008    #[test]
4009    fn test_glyphs3_node() {
4010        let node: Node = Node::parse_plist("(354, 183, l)").unwrap();
4011        assert_eq!(
4012            Node {
4013                node_type: crate::NodeType::Line,
4014                pt: super::Point { x: 354.0, y: 183.0 }
4015            },
4016            node
4017        );
4018    }
4019
4020    #[test]
4021    fn test_glyphs2_node() {
4022        let node: Node = Node::parse_plist("\"354 183 LINE\"").unwrap();
4023        assert_eq!(
4024            Node {
4025                node_type: crate::NodeType::Line,
4026                pt: super::Point { x: 354.0, y: 183.0 }
4027            },
4028            node
4029        );
4030    }
4031
4032    #[test]
4033    fn test_glyphs3_node_userdata() {
4034        let node = Node::parse_plist("(354, 183, l,{name = hr00;})").unwrap();
4035        assert_eq!(
4036            Node {
4037                node_type: crate::NodeType::Line,
4038                pt: super::Point { x: 354.0, y: 183.0 }
4039            },
4040            node
4041        );
4042    }
4043
4044    #[test]
4045    fn test_glyphs2_node_userdata() {
4046        let node = Node::parse_plist("\"354 183 LINE {name=duck}\"").unwrap();
4047        assert_eq!(
4048            Node {
4049                node_type: crate::NodeType::Line,
4050                pt: super::Point { x: 354.0, y: 183.0 }
4051            },
4052            node
4053        );
4054    }
4055
4056    // unquoted infinity likes to parse as a float which is suboptimal for glyph names. Survive.
4057    // Observed on Work Sans and Lexend.
4058    #[test]
4059    fn survive_unquoted_infinity() {
4060        // Read a minimal glyphs file that reproduces the error
4061        Font::load(&glyphs3_dir().join("infinity.glyphs")).unwrap();
4062    }
4063
4064    fn assert_wght_var_metrics(font: &Font) {
4065        let default_master = font.default_master();
4066        assert_eq!(737.0, default_master.ascender().unwrap());
4067        assert_eq!(-42.0, default_master.descender().unwrap());
4068    }
4069
4070    #[test]
4071    fn read_wght_var_2_metrics() {
4072        assert_wght_var_metrics(&Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap());
4073    }
4074
4075    #[test]
4076    fn read_wght_var_3_metrics() {
4077        assert_wght_var_metrics(&Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap());
4078    }
4079
4080    /// So far we don't have any package-only examples
4081    enum LoadCompare {
4082        Glyphs,
4083        GlyphsAndPackage,
4084    }
4085
4086    /// Asserts that two Font structs are equal, excluding localized name entries.
4087    /// This is useful for comparing v2 and v3 Glyphs files where localized names
4088    /// may differ due to format changes. Only default (dflt/default/ENG) names are compared.
4089    fn assert_fonts_equal_sans_localized_names(f1: &Font, f2: &Font, context: &str) {
4090        let mut f1_clone = f1.clone();
4091        let mut f2_clone = f2.clone();
4092        // Filter all_names to keep only default entries (dflt/default/ENG)
4093        for all_names in [&mut f1_clone.all_names, &mut f2_clone.all_names] {
4094            for values in all_names.values_mut() {
4095                values.retain(|(lang, _)| matches!(lang.as_str(), "dflt" | "default" | "ENG"));
4096            }
4097        }
4098        assert_eq!(f1_clone, f2_clone, "{context}");
4099    }
4100
4101    /// Asserts that two Font structs are equal, including localized name entries.
4102    /// This is useful for comparing two v3 Glyphs files where all fields should match.
4103    fn assert_fonts_equal(f1: &Font, f2: &Font, context: &str) {
4104        assert_eq!(f1, f2, "{context}");
4105    }
4106
4107    fn assert_load_v2_matches_load_v3(name: &str, compare: LoadCompare) {
4108        let has_package = matches!(compare, LoadCompare::GlyphsAndPackage);
4109        let _ = env_logger::builder().is_test(true).try_init();
4110        let filename = format!("{name}.glyphs");
4111        let pkgname = format!("{name}.glyphspackage");
4112        let g2_file = glyphs2_dir().join(filename.clone());
4113        let g3_file = glyphs3_dir().join(filename.clone());
4114        let g2 = Font::load(&g2_file).unwrap();
4115        let g3 = Font::load(&g3_file).unwrap();
4116
4117        // Handy if troubleshooting
4118        std::fs::write("/tmp/g2.glyphs.txt", format!("{g2:#?}")).unwrap();
4119        std::fs::write("/tmp/g3.glyphs.txt", format!("{g3:#?}")).unwrap();
4120
4121        assert_fonts_equal_sans_localized_names(
4122            &g2,
4123            &g3,
4124            &format!("g2 vs g3: {g2_file:?} vs {g3_file:?}"),
4125        );
4126
4127        if has_package {
4128            let g2_pkg = Font::load(&glyphs2_dir().join(pkgname.clone())).unwrap();
4129            let g3_pkg = Font::load(&glyphs3_dir().join(pkgname.clone())).unwrap();
4130
4131            std::fs::write("/tmp/g2.glyphspackage.txt", format!("{g2_pkg:#?}")).unwrap();
4132            std::fs::write("/tmp/g3.glyphspackage.txt", format!("{g3_pkg:#?}")).unwrap();
4133
4134            assert_fonts_equal_sans_localized_names(&g2_pkg, &g3_pkg, "g2_pkg vs g3_pkg");
4135            assert_fonts_equal(&g3_pkg, &g3, "g3_pkg vs g3");
4136        }
4137    }
4138
4139    #[test]
4140    fn read_wght_var_2_and_3() {
4141        assert_load_v2_matches_load_v3("WghtVar", LoadCompare::GlyphsAndPackage);
4142    }
4143
4144    #[test]
4145    fn read_wght_var_avar_2_and_3() {
4146        assert_load_v2_matches_load_v3("WghtVar_Avar", LoadCompare::GlyphsAndPackage);
4147    }
4148
4149    #[test]
4150    fn read_wght_var_instances_2_and_3() {
4151        assert_load_v2_matches_load_v3("WghtVar_Instances", LoadCompare::GlyphsAndPackage);
4152    }
4153
4154    #[test]
4155    fn read_wght_var_os2_2_and_3() {
4156        assert_load_v2_matches_load_v3("WghtVar_OS2", LoadCompare::GlyphsAndPackage);
4157    }
4158
4159    #[test]
4160    fn read_wght_var_anchors_2_and_3() {
4161        assert_load_v2_matches_load_v3("WghtVar_Anchors", LoadCompare::GlyphsAndPackage);
4162    }
4163
4164    #[test]
4165    fn read_infinity_2_and_3() {
4166        assert_load_v2_matches_load_v3("infinity", LoadCompare::GlyphsAndPackage);
4167    }
4168
4169    #[test]
4170    fn read_wght_var_noexport_2_and_3() {
4171        assert_load_v2_matches_load_v3("WghtVar_NoExport", LoadCompare::Glyphs);
4172    }
4173
4174    #[test]
4175    fn read_master_names_2_and_3() {
4176        assert_load_v2_matches_load_v3("MasterNames", LoadCompare::Glyphs);
4177    }
4178
4179    #[test]
4180    fn read_master_names_with_italic_2_and_3() {
4181        assert_load_v2_matches_load_v3("MasterNames-Italic", LoadCompare::Glyphs);
4182    }
4183
4184    fn only_shape_in_only_layer<'a>(font: &'a Font, glyph_name: &str) -> &'a Shape {
4185        let glyph = font.glyphs.get(glyph_name).unwrap();
4186        assert_eq!(1, glyph.layers.len());
4187        assert_eq!(1, glyph.layers[0].shapes.len());
4188        &glyph.layers[0].shapes[0]
4189    }
4190
4191    fn check_v2_to_v3_transform(glyphs_file: &str, glyph_name: &str, expected: Affine) {
4192        let g2 = Font::load(&glyphs2_dir().join(glyphs_file)).unwrap();
4193        let g3 = Font::load(&glyphs3_dir().join(glyphs_file)).unwrap();
4194
4195        // We're exclusively interested in the transform
4196        let g2_shape = only_shape_in_only_layer(&g2, glyph_name);
4197        let g3_shape = only_shape_in_only_layer(&g3, glyph_name);
4198
4199        let Shape::Component(g2_shape) = g2_shape else {
4200            panic!("{g2_shape:?} should be a component");
4201        };
4202        let Shape::Component(g3_shape) = g3_shape else {
4203            panic!("{g3_shape:?} should be a component");
4204        };
4205
4206        assert_eq!(expected, round(g2_shape.transform, 4));
4207        assert_eq!(expected, round(g3_shape.transform, 4));
4208    }
4209
4210    #[test]
4211    fn read_transformed_component_2_and_3_uniform_scale() {
4212        let expected = Affine::new([1.6655, 1.1611, -1.1611, 1.6655, -233.0, -129.0]);
4213        check_v2_to_v3_transform("Component.glyphs", "comma", expected);
4214    }
4215
4216    #[test]
4217    fn read_transformed_component_2_and_3_nonuniform_scale() {
4218        let expected = Affine::new([0.8452, 0.5892, -1.1611, 1.6655, -233.0, -129.0]);
4219        check_v2_to_v3_transform("Component.glyphs", "non_uniform_scale", expected);
4220    }
4221
4222    #[test]
4223    fn upgrade_2_to_3_with_implicit_axes() {
4224        let font = Font::load(&glyphs2_dir().join("WghtVar_ImplicitAxes.glyphs")).unwrap();
4225        assert_eq!(
4226            font.axes
4227                .iter()
4228                .map(|a| a.tag.as_str())
4229                .collect::<Vec<&str>>(),
4230            vec!["wght", "wdth", "XXXX"]
4231        );
4232    }
4233
4234    #[test]
4235    fn understand_v2_style_unquoted_hex_unicode() {
4236        let font = Font::load(&glyphs2_dir().join("Unicode-UnquotedHex.glyphs")).unwrap();
4237        assert_eq!(
4238            BTreeSet::from([0x1234]),
4239            font.glyphs.get("name").unwrap().unicode,
4240        );
4241        assert_eq!(1, font.glyphs.len());
4242    }
4243
4244    #[test]
4245    fn understand_v2_style_quoted_hex_unicode_sequence() {
4246        let font = Font::load(&glyphs2_dir().join("Unicode-QuotedHexSequence.glyphs")).unwrap();
4247        assert_eq!(
4248            BTreeSet::from([0x2044, 0x200D, 0x2215]),
4249            font.glyphs.get("name").unwrap().unicode,
4250        );
4251        assert_eq!(1, font.glyphs.len());
4252    }
4253
4254    #[test]
4255    fn understand_v3_style_unquoted_decimal_unicode() {
4256        let font = Font::load(&glyphs3_dir().join("Unicode-UnquotedDec.glyphs")).unwrap();
4257        assert_eq!(
4258            BTreeSet::from([182]),
4259            font.glyphs.get("name").unwrap().unicode
4260        );
4261        assert_eq!(1, font.glyphs.len());
4262    }
4263
4264    #[test]
4265    fn understand_v3_style_unquoted_decimal_unicode_sequence() {
4266        let font = Font::load(&glyphs3_dir().join("Unicode-UnquotedDecSequence.glyphs")).unwrap();
4267        assert_eq!(
4268            BTreeSet::from([1619, 1764]),
4269            font.glyphs.get("name").unwrap().unicode,
4270        );
4271        assert_eq!(1, font.glyphs.len());
4272    }
4273
4274    #[test]
4275    fn axes_not_hidden() {
4276        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
4277        assert_eq!(
4278            font.axes.iter().map(|a| a.hidden).collect::<Vec<_>>(),
4279            vec![None]
4280        );
4281    }
4282
4283    #[test]
4284    fn axis_hidden() {
4285        let font = Font::load(&glyphs3_dir().join("WghtVar_3master_CustomOrigin.glyphs")).unwrap();
4286        assert_eq!(
4287            font.axes.iter().map(|a| a.hidden).collect::<Vec<_>>(),
4288            vec![Some(true)]
4289        );
4290    }
4291
4292    #[test]
4293    fn vf_origin_single_axis_default() {
4294        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
4295        assert_eq!(0, font.default_master_idx);
4296    }
4297
4298    #[test]
4299    fn vf_origin_multi_axis_default() {
4300        let font = Font::load(&glyphs2_dir().join("WghtVar_ImplicitAxes.glyphs")).unwrap();
4301        assert_eq!(0, font.default_master_idx);
4302    }
4303
4304    #[test]
4305    fn vf_origin_multi_axis_custom() {
4306        let font = Font::load(&glyphs3_dir().join("WghtVar_3master_CustomOrigin.glyphs")).unwrap();
4307        assert_eq!(2, font.default_master_idx);
4308    }
4309
4310    #[test]
4311    fn vf_origin_unquoted_string() {
4312        // the 'Variable Font Origin' custom parameter has the value `m01`,
4313        // un un-quoted plist string, which happens to be the default master.id
4314        // that Glyphs.app assigns to the predefined 'Regular' master that any
4315        // "New Font" comes with when it is first crated.
4316        // We just test that we do not crash attempting to parse the unquoted
4317        // string as an integer.
4318        let font = Font::load(&glyphs3_dir().join("CustomOrigin.glyphs")).unwrap();
4319        assert_eq!(1, font.default_master_idx);
4320    }
4321
4322    #[rstest]
4323    #[case::base_style_without_regular(
4324        &[
4325            "Expanded Thin Italic",
4326            "Expanded Italic",
4327            "Expanded Bold Italic",
4328        ],
4329        "Expanded Italic"  // is common and exactly matches [1]
4330    )]
4331    #[case::base_style_contains_regular(
4332        &[
4333            "Regular Foo Bar",
4334            "Regular Foo Baz",
4335            "Regular Foo",
4336        ],
4337        "Regular Foo" // is common and exactly matches [2]
4338    )]
4339    #[case::base_style_with_regular_omitted(
4340        &[
4341            "Condensed Thin",
4342            "Condensed Light",
4343            "Condensed Regular",
4344        ],
4345        // "Condensed" is common and matches "Condensed Regular" when "Regular" is ignored
4346        "Condensed Regular"
4347    )]
4348    // "" is common and matches "Regular when "Regular" is ignored
4349    #[case::default_to_regular(
4350        &["Thin", "Light", "Regular", "Medium", "Bold"],
4351        "Regular"
4352    )]
4353    // "" is common, nothing matches, just take the first
4354    #[case::default_to_first(&["Foo", "Bar", "Baz"], "Foo")]
4355    fn find_default_master(#[case] master_names: &[&str], #[case] expected: &str) {
4356        let mut font = RawFont::default();
4357        for name in master_names {
4358            let master = RawFontMaster {
4359                name: Some(name.to_string()),
4360                ..Default::default()
4361            };
4362            font.font_master.push(master);
4363        }
4364
4365        let idx = default_master_idx(&mut font);
4366
4367        assert_eq!(expected, font.font_master[idx].name.as_deref().unwrap());
4368    }
4369
4370    #[test]
4371    fn glyph_order_default_is_file_order() {
4372        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
4373        assert_eq!(
4374            vec![
4375                "space",
4376                "exclam",
4377                "hyphen",
4378                "bracketleft",
4379                "bracketright",
4380                "manual-component"
4381            ],
4382            font.glyph_order
4383        );
4384    }
4385
4386    #[test]
4387    fn glyph_order_override_obeyed() {
4388        let _ = env_logger::builder().is_test(true).try_init();
4389        let font = Font::load(&glyphs3_dir().join("WghtVar_GlyphOrder.glyphs")).unwrap();
4390        assert_eq!(vec!["hyphen", "space", "exclam"], font.glyph_order);
4391    }
4392
4393    #[test]
4394    fn loads_global_axis_mappings_from_glyphs2() {
4395        let font = Font::load(&glyphs2_dir().join("OpszWghtVar_AxisMappings.glyphs")).unwrap();
4396
4397        // Did you load the mappings? DID YOU?!
4398        assert_eq!(
4399            UserToDesignMapping(BTreeMap::from([
4400                (
4401                    "Optical Size".into(),
4402                    AxisUserToDesignMap(vec![
4403                        (OrderedFloat(12.0), OrderedFloat(12.0)),
4404                        (OrderedFloat(72.0), OrderedFloat(72.0))
4405                    ])
4406                ),
4407                (
4408                    "Weight".into(),
4409                    AxisUserToDesignMap(vec![
4410                        (OrderedFloat(100.0), OrderedFloat(40.0)),
4411                        (OrderedFloat(200.0), OrderedFloat(46.0)),
4412                        (OrderedFloat(300.0), OrderedFloat(51.0)),
4413                        (OrderedFloat(400.0), OrderedFloat(57.0)),
4414                        (OrderedFloat(450.0), OrderedFloat(57.0)), // duplicate value is valid
4415                        (OrderedFloat(500.0), OrderedFloat(62.0)),
4416                        (OrderedFloat(600.0), OrderedFloat(68.0)),
4417                        (OrderedFloat(700.0), OrderedFloat(73.0)),
4418                    ])
4419                ),
4420            ])),
4421            font.axis_mappings
4422        );
4423    }
4424
4425    #[test]
4426    fn loads_global_axis_locations_from_glyphs3() {
4427        let font = Font::load(&glyphs3_dir().join("WghtVar_AxisLocation.glyphs")).unwrap();
4428
4429        // Did you load the mappings? DID YOU?!
4430        assert_eq!(
4431            UserToDesignMapping(BTreeMap::from([(
4432                "Weight".into(),
4433                AxisUserToDesignMap(vec![
4434                    (OrderedFloat(400.0), OrderedFloat(0.0)),
4435                    (OrderedFloat(500.0), OrderedFloat(8.0)),
4436                    (OrderedFloat(700.0), OrderedFloat(10.0)),
4437                    // this comes from the 'SemiBold' instance's 'Axis Location'
4438                    (OrderedFloat(600.0), OrderedFloat(8.5)),
4439                    // the 'SemiMedium' instance contains no explict 'Axis Location' so
4440                    // it doesn't show up (implicit mappings are ignored in this case)
4441                    // (OrderedFloat(450.0), OrderedFloat(4.0)),
4442                ])
4443            ),])),
4444            font.axis_mappings
4445        );
4446    }
4447
4448    #[test]
4449    fn loads_global_axis_mappings_from_instances_wght_glyphs3() {
4450        let font = Font::load(&glyphs3_dir().join("WghtVar_Avar_From_Instances.glyphs")).unwrap();
4451
4452        let wght_idx = font.axes.iter().position(|a| a.tag == "wght").unwrap();
4453        assert_eq!(
4454            vec![60.0, 80.0, 132.0],
4455            font.masters
4456                .iter()
4457                .map(|m| m.axes_values[wght_idx].into_inner())
4458                .collect::<Vec<_>>()
4459        );
4460        // the default master is the 'Bold' in this test font
4461        assert_eq!(
4462            (132.0, 2),
4463            (
4464                font.default_master().axes_values[wght_idx].into_inner(),
4465                font.default_master_idx
4466            )
4467        );
4468
4469        // Did you load the mappings? DID YOU?!
4470        assert_eq!(
4471            UserToDesignMapping(BTreeMap::from([(
4472                "Weight".into(),
4473                AxisUserToDesignMap(vec![
4474                    (OrderedFloat(300.0), OrderedFloat(60.0)),
4475                    // we expect a map 400:80 here, even though the 'Regular' instance's
4476                    // Weight Class property is omitted in the .glyphs source because it
4477                    // is equal to its default value (400):
4478                    // https://github.com/googlefonts/fontc/issues/905
4479                    (OrderedFloat(400.0), OrderedFloat(80.0)),
4480                    (OrderedFloat(500.0), OrderedFloat(100.0)),
4481                    (OrderedFloat(700.0), OrderedFloat(132.0)),
4482                ])
4483            ),])),
4484            font.axis_mappings
4485        );
4486    }
4487
4488    #[test]
4489    fn loads_global_axis_mappings_from_instances_wdth_glyphs3() {
4490        let font = Font::load(&glyphs3_dir().join("WdthVar.glyphs")).unwrap();
4491
4492        assert_eq!(font.axes.len(), 1);
4493        assert_eq!(font.axes[0].tag, "wdth");
4494        assert_eq!(
4495            vec![22.0, 62.0],
4496            font.masters
4497                .iter()
4498                .map(|m| m.axes_values[0].into_inner())
4499                .collect::<Vec<_>>()
4500        );
4501        // the default master is the 'Condensed' in this test font
4502        assert_eq!(
4503            (22.0, 0),
4504            (
4505                font.default_master().axes_values[0].into_inner(),
4506                font.default_master_idx
4507            )
4508        );
4509        // Did you load the mappings? DID YOU?!
4510        assert_eq!(
4511            UserToDesignMapping(BTreeMap::from([(
4512                "Width".into(),
4513                AxisUserToDesignMap(vec![
4514                    // The "1: Ultra-condensed" instance width class corresponds to a
4515                    // `wdth` of 50 (user-space), in turn mapped to 22 (design-space).
4516                    (OrderedFloat(50.0), OrderedFloat(22.0)),
4517                    // We expect a map 100:41 here, even though the 'Regular' instance's
4518                    // Width Class property is omitted in the .glyphs source because it
4519                    // is equal to its default value "5: Medium (normal)" (or wdth=100):
4520                    // https://github.com/googlefonts/fontc/issues/905
4521                    (OrderedFloat(100.0), OrderedFloat(41.0)),
4522                    // The "9: Ultra-expanded" instance width class corresponds to a
4523                    // `wdth` of 200 (user-space), in turn mapped to 62 (design-space).
4524                    (OrderedFloat(200.0), OrderedFloat(62.0)),
4525                ])
4526            ),])),
4527            font.axis_mappings
4528        );
4529    }
4530
4531    #[test]
4532    fn fea_for_class() {
4533        let font = Font::load(&glyphs2_dir().join("Fea_Class.glyphs")).unwrap();
4534        assert_eq!(
4535            vec![
4536                concat!("# automatic\n", "@Uppercase = [ A B C\n", "];",),
4537                concat!("@Lowercase = [ a b c\n", "];",),
4538            ],
4539            font.features
4540                .iter()
4541                .filter_map(|f| f.str_if_enabled())
4542                .collect::<Vec<_>>()
4543        )
4544    }
4545
4546    #[test]
4547    fn fea_for_prefix() {
4548        let font = Font::load(&glyphs2_dir().join("Fea_Prefix.glyphs")).unwrap();
4549        assert_eq!(
4550            vec![
4551                "\
4552# Prefix: Languagesystems
4553# automatic
4554languagesystem DFLT dflt;
4555
4556languagesystem latn dflt;
4557and more;
4558",
4559                "# Prefix: \n# automatic\nthanks for all the fish;",
4560            ],
4561            font.features
4562                .iter()
4563                .filter_map(|f| f.str_if_enabled())
4564                .collect::<Vec<_>>()
4565        )
4566    }
4567
4568    #[test]
4569    fn fea_for_feature() {
4570        let font = Font::load(&glyphs2_dir().join("Fea_Feature.glyphs")).unwrap();
4571        assert_eq!(
4572            vec![
4573                "\
4574feature aalt {
4575feature locl;
4576feature tnum;
4577} aalt;",
4578                "\
4579feature ccmp {
4580# automatic
4581lookup ccmp_Other_2 {
4582  sub @Markscomb' @MarkscombCase by @MarkscombCase;
4583  sub @MarkscombCase @Markscomb' by @MarkscombCase;
4584} ccmp_Other_2;
4585
4586etc;
4587} ccmp;",
4588            ],
4589            font.features
4590                .iter()
4591                .filter_map(|f| f.str_if_enabled())
4592                .collect::<Vec<_>>()
4593        )
4594    }
4595
4596    #[test]
4597    fn fea_order() {
4598        let font = Font::load(&glyphs2_dir().join("Fea_Order.glyphs")).unwrap();
4599        assert_eq!(
4600            vec![
4601                "@class_first = [ meh\n];",
4602                "# Prefix: second\nmeh",
4603                "feature third {\nmeh\n} third;",
4604            ],
4605            font.features
4606                .iter()
4607                .filter_map(|f| f.str_if_enabled())
4608                .collect::<Vec<_>>()
4609        )
4610    }
4611
4612    #[test]
4613    fn fea_labels() {
4614        let font = Font::load(&glyphs3_dir().join("Fea_Labels.glyphs")).unwrap();
4615        assert_eq!(
4616            vec![
4617                concat!(
4618                    "feature ss01 {\n",
4619                    "# automatic\n",
4620                    "featureNames {\n",
4621                    "  name 3 1 0x0409 \"Test 1\";\n",
4622                    "  name 3 1 0x0C01 \"اختبار ١\";\n",
4623                    "};\n",
4624                    "sub a by a.ss01;\n",
4625                    "sub b by b.ss01;\n\n",
4626                    "} ss01;",
4627                ),
4628                concat!(
4629                    "feature ss02 {\n",
4630                    "featureNames {\n",
4631                    "  name 3 1 0x0409 \"Test 2\";\n",
4632                    "};\n",
4633                    "sub c by c.alt;\n",
4634                    "} ss02;",
4635                ),
4636            ],
4637            font.features
4638                .iter()
4639                .filter_map(|f| f.str_if_enabled())
4640                .collect::<Vec<_>>()
4641        )
4642    }
4643
4644    #[test]
4645    fn tags_make_excellent_names() {
4646        let raw = RawFeature {
4647            tag: Some("aalt".to_string()),
4648            code: "blah".to_string(),
4649            ..Default::default()
4650        };
4651        assert_eq!("aalt", raw.name().unwrap());
4652    }
4653
4654    #[test]
4655    fn manual_kern_always_gets_insert_mark() {
4656        let feature = RawFeature {
4657            tag: Some("kern".into()),
4658            ..Default::default()
4659        };
4660
4661        assert!(
4662            feature
4663                .raw_feature_to_feature()
4664                .unwrap()
4665                .content
4666                .contains("# Automatic Code")
4667        )
4668    }
4669
4670    #[test]
4671    fn but_automatic_does_not() {
4672        let feature = RawFeature {
4673            tag: Some("kern".into()),
4674            automatic: Some(1),
4675            ..Default::default()
4676        };
4677
4678        assert!(
4679            !feature
4680                .raw_feature_to_feature()
4681                .unwrap()
4682                .content
4683                .contains("# Automatic Code")
4684        )
4685    }
4686
4687    #[test]
4688    fn v2_to_v3_simple_names() {
4689        let v2 = Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap();
4690        let v3 = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
4691        // Compare only default names - v2 and v3 may have different localized entries
4692        let v2_defaults: BTreeMap<_, _> = v2.default_names().collect();
4693        let v3_defaults: BTreeMap<_, _> = v3.default_names().collect();
4694        assert_eq!(v3_defaults, v2_defaults);
4695    }
4696
4697    #[test]
4698    fn v2_to_v3_more_names() {
4699        let v2 = Font::load(&glyphs2_dir().join("TheBestNames.glyphs")).unwrap();
4700        let v3 = Font::load(&glyphs3_dir().join("TheBestNames.glyphs")).unwrap();
4701
4702        // V2 root-level fields (designer, manufacturer, copyright, etc.) are converted
4703        // to properties format by v2_to_v3_names(), so v2 and v3 should have identical defaults
4704        let v2_defaults: BTreeMap<_, _> = v2.default_names().collect();
4705        let v3_defaults: BTreeMap<_, _> = v3.default_names().collect();
4706
4707        assert_eq!(v2_defaults, v3_defaults);
4708    }
4709
4710    #[test]
4711    fn v2_long_param_names() {
4712        let v2 = Font::load(&glyphs2_dir().join("LongParamNames.glyphs")).unwrap();
4713        assert_eq!(v2.get_default_name("vendorID"), Some("DERP"));
4714        assert_eq!(
4715            v2.get_default_name("descriptions"),
4716            Some("legacy description")
4717        );
4718        assert_eq!(
4719            v2.get_default_name("licenseURL"),
4720            Some("www.example.com/legacy")
4721        );
4722        assert_eq!(v2.get_default_name("version"), Some("legacy version"));
4723        assert_eq!(v2.get_default_name("licenses"), Some("legacy license"));
4724        assert_eq!(v2.get_default_name("uniqueID"), Some("legacy unique id"));
4725        assert_eq!(
4726            v2.get_default_name("sampleTexts"),
4727            Some("legacy sample text")
4728        );
4729    }
4730
4731    #[test]
4732    fn v2_style_names_in_a_v3_file() {
4733        let v3_mixed_with_v2 =
4734            Font::load(&glyphs3_dir().join("TheBestV2NamesInAV3File.glyphs")).unwrap();
4735        let v3 = Font::load(&glyphs3_dir().join("TheBestNames.glyphs")).unwrap();
4736
4737        // V3 file with v2-style root-level fields (copyright, designer, etc.) gets those
4738        // converted to properties format by v2_to_v3_names(), so should match the pure v3 file
4739        let v3_mixed_defaults: BTreeMap<_, _> = v3_mixed_with_v2.default_names().collect();
4740        let v3_defaults: BTreeMap<_, _> = v3.default_names().collect();
4741
4742        assert_eq!(v3_mixed_defaults, v3_defaults);
4743    }
4744
4745    #[test]
4746    fn no_english_names_fallback() {
4747        // Test file with NO dflt/default/ENG entries, only FRA, DEU, ITA, ESP, JPN
4748        // This validates that the fallback uses insertion-order first, and not
4749        // alphabetical first, and that the overlap between default_names() and
4750        // localized_names() is as intended.
4751        let font = Font::load(&glyphs3_dir().join("NoEnglishNames.glyphs")).unwrap();
4752
4753        // copyrights: FRA (1st), DEU (2nd), JPN (3rd) => should use FRA
4754        assert_eq!(
4755            font.get_default_name("copyrights"),
4756            Some("Droit d'auteur français")
4757        );
4758
4759        // designers: FRA (1st), DEU (2nd) => should use FRA
4760        assert_eq!(
4761            font.get_default_name("designers"),
4762            Some("Concepteur français")
4763        );
4764
4765        // manufacturers: ITA (1st), ESP (2nd) => should use ITA
4766        assert_eq!(
4767            font.get_default_name("manufacturers"),
4768            Some("Produttore italiano")
4769        );
4770
4771        let localized: Vec<_> = font.localized_names().collect();
4772
4773        // The FRA copyright should appear in both default and localized
4774        assert!(localized.contains(&("copyrights", "FRA", "Droit d'auteur français")));
4775        // The DEU and JPN copyright should only appear in localized
4776        assert!(localized.contains(&("copyrights", "DEU", "Deutsches Urheberrecht")));
4777        assert!(localized.contains(&("copyrights", "JPN", "日本の著作権")));
4778    }
4779
4780    fn assert_wghtvar_avar_master_and_axes(glyphs_file: &Path) {
4781        let font = Font::load(glyphs_file).unwrap();
4782        let wght_idx = font.axes.iter().position(|a| a.tag == "wght").unwrap();
4783        assert_eq!(
4784            vec![300.0, 400.0, 700.0],
4785            font.masters
4786                .iter()
4787                .map(|m| m.axes_values[wght_idx].into_inner())
4788                .collect::<Vec<_>>()
4789        );
4790        assert_eq!(
4791            (400.0, 1),
4792            (
4793                font.default_master().axes_values[wght_idx].into_inner(),
4794                font.default_master_idx
4795            )
4796        );
4797    }
4798
4799    #[test]
4800    fn favor_regular_as_origin_glyphs2() {
4801        assert_wghtvar_avar_master_and_axes(&glyphs2_dir().join("WghtVar_Avar.glyphs"));
4802    }
4803
4804    #[test]
4805    fn favor_regular_as_origin_glyphs3() {
4806        assert_wghtvar_avar_master_and_axes(&glyphs3_dir().join("WghtVar_Avar.glyphs"));
4807    }
4808
4809    #[test]
4810    fn have_all_the_best_instances() {
4811        let font = Font::load(&glyphs3_dir().join("WghtVar_Instances.glyphs")).unwrap();
4812        assert_eq!(
4813            vec![
4814                ("Regular", vec![("Weight", 400.0)]),
4815                ("Bold", vec![("Weight", 700.0)])
4816            ],
4817            font.instances
4818                .iter()
4819                .map(|inst| (
4820                    inst.name.as_str(),
4821                    font.axes
4822                        .iter()
4823                        .zip(&inst.axes_values)
4824                        .map(|(a, v)| (a.name.as_str(), v.0))
4825                        .collect::<Vec<_>>()
4826                ))
4827                .collect::<Vec<_>>()
4828        );
4829    }
4830
4831    #[test]
4832    fn read_typo_whatsits() {
4833        let font = Font::load(&glyphs2_dir().join("WghtVar_OS2.glyphs")).unwrap();
4834        let master = font.default_master();
4835        assert_eq!(Some(1193), master.custom_parameters.typo_ascender);
4836        assert_eq!(Some(-289), master.custom_parameters.typo_descender);
4837    }
4838
4839    #[test]
4840    fn read_os2_flags_default_set() {
4841        let font = Font::load(&glyphs2_dir().join("WghtVar.glyphs")).unwrap();
4842        assert_eq!(font.custom_parameters.use_typo_metrics, Some(true));
4843        assert_eq!(font.custom_parameters.has_wws_names, Some(true));
4844    }
4845
4846    #[test]
4847    fn read_os2_flags_default_unset() {
4848        let font = Font::load(&glyphs2_dir().join("WghtVar_OS2.glyphs")).unwrap();
4849        assert_eq!(font.custom_parameters.use_typo_metrics, None);
4850        assert_eq!(font.custom_parameters.has_wws_names, None);
4851    }
4852
4853    #[test]
4854    fn read_simple_kerning() {
4855        let font = Font::load(&glyphs3_dir().join("WghtVar.glyphs")).unwrap();
4856        assert_eq!(
4857            HashSet::from(["m01", "E09E0C54-128D-4FEA-B209-1B70BEFE300B",]),
4858            font.kerning_ltr
4859                .keys()
4860                .map(|k| k.as_str())
4861                .collect::<HashSet<_>>()
4862        );
4863
4864        let actual_groups: Vec<_> = font
4865            .glyphs
4866            .iter()
4867            .filter_map(|(name, glyph)| {
4868                if glyph.left_kern.is_some() || glyph.right_kern.is_some() {
4869                    Some((
4870                        name.as_str(),
4871                        glyph.left_kern.as_deref(),
4872                        glyph.right_kern.as_deref(),
4873                    ))
4874                } else {
4875                    None
4876                }
4877            })
4878            .collect();
4879
4880        let actual_kerning = font
4881            .kerning_ltr
4882            .get("m01")
4883            .unwrap()
4884            .iter()
4885            .map(|((n1, n2), value)| (n1.as_str(), n2.as_str(), value.0))
4886            .collect::<Vec<_>>();
4887
4888        assert_eq!(
4889            (
4890                vec![
4891                    ("bracketleft", Some("bracketleft_L"), Some("bracketleft_R")),
4892                    (
4893                        "bracketright",
4894                        Some("bracketright_L"),
4895                        Some("bracketright_R")
4896                    ),
4897                ],
4898                vec![
4899                    ("@MMK_L_bracketleft_R", "exclam", -165.),
4900                    ("bracketleft", "bracketright", -300.),
4901                    ("exclam", "@MMK_R_bracketright_L", -160.),
4902                    ("exclam", "exclam", -360.),
4903                    ("exclam", "hyphen", 20.),
4904                    ("hyphen", "hyphen", -150.),
4905                ],
4906            ),
4907            (actual_groups, actual_kerning),
4908            "{:?}",
4909            font.kerning_ltr
4910        );
4911    }
4912
4913    #[test]
4914    fn kern_floats() {
4915        let font = Font::load(&glyphs3_dir().join("KernFloats.glyphs")).unwrap();
4916
4917        let kerns = font.kerning_ltr.get("m01").unwrap();
4918        let key = ("space".to_smolstr(), "space".to_smolstr());
4919        assert_eq!(kerns.get(&key), Some(&OrderedFloat(4.2001)));
4920    }
4921
4922    #[test]
4923    fn read_simple_anchor() {
4924        let font = Font::load(&glyphs3_dir().join("WghtVar_Anchors.glyphs")).unwrap();
4925        assert_eq!(
4926            vec![
4927                ("m01", "top", Point::new(300.0, 700.0)),
4928                ("l2", "top", Point::new(325.0, 725.0))
4929            ],
4930            font.glyphs
4931                .get("A")
4932                .unwrap()
4933                .layers
4934                .iter()
4935                .flat_map(|l| l.anchors.iter().map(|a| (
4936                    l.layer_id.as_str(),
4937                    a.name.as_str(),
4938                    a.pos
4939                )))
4940                .collect::<Vec<_>>()
4941        );
4942    }
4943
4944    #[test]
4945    fn read_export_glyph() {
4946        let font = Font::load(&glyphs3_dir().join("WghtVar_NoExport.glyphs")).unwrap();
4947        assert_eq!(
4948            vec![
4949                ("bracketleft", true),
4950                ("bracketright", true),
4951                ("exclam", true),
4952                ("hyphen", false),
4953                ("manual-component", true),
4954                ("space", true),
4955            ],
4956            font.glyphs
4957                .iter()
4958                .map(|(name, glyph)| (name.as_str(), glyph.export))
4959                .collect::<Vec<_>>()
4960        );
4961    }
4962
4963    #[test]
4964    fn read_fstype_none() {
4965        let font = Font::load(&glyphs3_dir().join("infinity.glyphs")).unwrap();
4966        assert!(font.custom_parameters.fs_type.is_none());
4967    }
4968
4969    #[test]
4970    fn read_fstype_zero() {
4971        let font = Font::load(&glyphs3_dir().join("fstype_0x0000.glyphs")).unwrap();
4972        assert_eq!(Some(0), font.custom_parameters.fs_type);
4973    }
4974
4975    #[test]
4976    fn read_fstype_bits() {
4977        let font = Font::load(&glyphs3_dir().join("fstype_0x0104.glyphs")).unwrap();
4978        assert_eq!(Some(0x104), font.custom_parameters.fs_type);
4979    }
4980
4981    #[test]
4982    fn anchor_components() {
4983        let font = Font::load(&glyphs3_dir().join("ComponentAnchor.glyphs")).unwrap();
4984        let glyph = font.glyphs.get("A_Aacute").unwrap();
4985        let acute_comb = glyph.layers[0]
4986            .shapes
4987            .iter()
4988            .find_map(|shape| match shape {
4989                Shape::Component(c) if c.name == "acutecomb" => Some(c),
4990                _ => None,
4991            })
4992            .unwrap();
4993        assert_eq!(acute_comb.anchor.as_deref(), Some("top_2"));
4994    }
4995
4996    #[test]
4997    fn parse_alignment_zone_smoke_test() {
4998        assert_eq!(
4999            super::parse_alignment_zone("{1, -12}").map(|x| (x.0.0, x.1.0)),
5000            Some((1., -12.))
5001        );
5002        assert_eq!(
5003            super::parse_alignment_zone("{-5001, 12}").map(|x| (x.0.0, x.1.0)),
5004            Some((-5001., 12.))
5005        );
5006    }
5007
5008    // a little helper used in tests below
5009    impl FontMaster {
5010        fn get_metric(&self, name: &str) -> Option<(f64, f64)> {
5011            self.metric_values
5012                .get(name)
5013                .map(|raw| (raw.pos.0, raw.over.0))
5014        }
5015    }
5016
5017    #[test]
5018    fn v2_alignment_zones_to_metrics() {
5019        let font = Font::load(&glyphs2_dir().join("alignment_zones_v2.glyphs")).unwrap();
5020        let master = font.default_master();
5021
5022        assert_eq!(master.get_metric("ascender"), Some((800., 17.)));
5023        assert_eq!(master.get_metric("cap height"), Some((700., 16.)));
5024        assert_eq!(master.get_metric("baseline"), Some((0., -16.)));
5025        assert_eq!(master.get_metric("descender"), Some((-200., -17.)));
5026        assert_eq!(master.get_metric("x-height"), Some((500., 15.)));
5027        assert_eq!(master.get_metric("italic angle"), None);
5028    }
5029
5030    #[test]
5031    fn v3_duplicate_metrics_first_wins() {
5032        // In this test font, the default master contains two 'x-height' metric values,
5033        // the first (501) that applies globally, and a second one (450) that applies
5034        // only to small-caps, using GSMetric's `filter` attribute which we ignore as
5035        // it is not relevant to build OS/2 and MVAR tables.
5036        // We match glyphsLib and only consider the first metric with a given name.
5037        let font = Font::load(&glyphs3_dir().join("WghtVar_OS2.glyphs")).unwrap();
5038        let master = font.default_master();
5039
5040        assert_eq!(master.get_metric("x-height"), Some((501., 0.)));
5041    }
5042
5043    #[test]
5044    fn v2_preserve_custom_alignment_zones() {
5045        let font = Font::load(&glyphs2_dir().join("alignment_zones_v2.glyphs")).unwrap();
5046        let master = font.default_master();
5047        assert_eq!(master.get_metric("zone 1"), Some((1000., 20.)));
5048        assert_eq!(master.get_metric("zone 2"), Some((-100., -15.)));
5049    }
5050
5051    // If category is unknown, we should ignore and compute it
5052    #[test]
5053    fn unknown_glyph_category() {
5054        let raw = super::RawGlyph {
5055            glyphname: "A".into(),
5056            category: Some("Fake".into()),
5057            ..Default::default()
5058        };
5059
5060        let cooked = raw.build(FormatVersion::V2, &GlyphData::default()).unwrap();
5061        assert_eq!(
5062            (cooked.category, cooked.sub_category),
5063            (Some(Category::Letter), None)
5064        );
5065    }
5066
5067    #[test]
5068    fn custom_params_disable() {
5069        let font = Font::load(&glyphs3_dir().join("custom_param_disable.glyphs")).unwrap();
5070
5071        assert!(font.custom_parameters.fs_type.is_none())
5072    }
5073
5074    #[test]
5075    fn parse_numbers() {
5076        let font = Font::load(&glyphs3_dir().join("number_value.glyphs")).unwrap();
5077        assert_eq!(
5078            font.masters[0].number_values.get("foo"),
5079            Some(&OrderedFloat(12.4f64))
5080        );
5081        assert_eq!(
5082            font.masters[1].number_values.get("foo"),
5083            Some(&OrderedFloat(0f64))
5084        );
5085    }
5086
5087    #[test]
5088    fn read_font_metrics() {
5089        let font =
5090            Font::load(&glyphs3_dir().join("GlobalMetrics_font_customParameters.glyphs")).unwrap();
5091        assert_eq!(Some(950), font.custom_parameters.typo_ascender);
5092        assert_eq!(Some(-350), font.custom_parameters.typo_descender);
5093        assert_eq!(Some(0), font.custom_parameters.typo_line_gap);
5094        assert_eq!(Some(950), font.custom_parameters.hhea_ascender);
5095        assert_eq!(Some(-350), font.custom_parameters.hhea_descender);
5096        assert_eq!(Some(0), font.custom_parameters.hhea_line_gap);
5097        assert_eq!(Some(1185), font.custom_parameters.win_ascent);
5098        assert_eq!(Some(420), font.custom_parameters.win_descent);
5099        assert_eq!(
5100            Some(OrderedFloat(42_f64)),
5101            font.custom_parameters.underline_thickness
5102        );
5103        assert_eq!(
5104            Some(OrderedFloat(-300_f64)),
5105            font.custom_parameters.underline_position
5106        );
5107    }
5108
5109    #[test]
5110    fn parse_legacy_stylistic_set_name() {
5111        let font = Font::load(&glyphs2_dir().join("FeaLegacyName.glyphs")).unwrap();
5112        assert_eq!(font.features.len(), 2);
5113        let [ss01, ss02] = font.features.as_slice() else {
5114            panic!("wrong number of features");
5115        };
5116
5117        assert!(
5118            ss01.content
5119                .contains("name 3 1 0x409 \"Alternate placeholder\"")
5120        );
5121        assert!(!ss02.content.contains("name 3 1"))
5122    }
5123
5124    // <https://github.com/googlefonts/fontc/issues/1175>
5125    #[test]
5126    fn one_italic_is_enough() {
5127        let font = Font::load(&glyphs2_dir().join("ItalicItalic.glyphs")).unwrap();
5128        for master in font.masters {
5129            let mut fragments = master.name.split_ascii_whitespace().collect::<Vec<_>>();
5130            fragments.sort();
5131            for words in fragments.windows(2) {
5132                assert!(
5133                    words[0] != words[1],
5134                    "Multiple instances of {} in {}",
5135                    words[0],
5136                    master.name
5137                );
5138            }
5139        }
5140    }
5141
5142    // We had a bug where if a master wasn't at a mapping point the Axis Mapping was modified
5143    #[test]
5144    fn ignore_masters_if_axis_mapping() {
5145        let font = Font::load(&glyphs2_dir().join("MasterNotMapped.glyphs")).unwrap();
5146        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
5147        assert_eq!(
5148            vec![
5149                (OrderedFloat(400_f64), OrderedFloat(40.0)),
5150                (OrderedFloat(700_f64), OrderedFloat(70.0))
5151            ],
5152            *mapping
5153        );
5154    }
5155
5156    #[rstest]
5157    #[case::rotate_0(0.0, Affine::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]))]
5158    #[case::rotate_360(360.0, Affine::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]))]
5159    #[case::rotate_90(90.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
5160    #[case::rotate_minus_90(-90.0, Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]))]
5161    #[case::rotate_180(180.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
5162    #[case::rotate_minus_180(-180.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
5163    #[case::rotate_270(270.0, Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]))]
5164    #[case::rotate_minus_270(-270.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
5165    #[case::rotate_450(450.0, Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]))]
5166    #[case::rotate_540(540.0, Affine::new([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]))]
5167    fn cardinal_rotation_contain_exact_zeros_and_ones(
5168        #[case] angle: f64,
5169        #[case] expected: Affine,
5170    ) {
5171        // When Glyphs 3 components are rotated by a 90, 180 or 270 degree angle,
5172        // we want to match fontTools Transform.rotate() and have the sine and
5173        // cosine terms rounded exactly to 0.0 or ±1.0 to avoid unnecessary diffs
5174        // upon rounding transformed coordinates to integer:
5175        // https://github.com/googlefonts/fontc/issues/1218
5176        assert_eq!(expected, normalized_rotation(angle));
5177    }
5178
5179    #[rstest]
5180    #[case::rotate_30(30.0, 4, Affine::new([0.866, 0.5, -0.5, 0.866, 0.0, 0.0]))]
5181    #[case::rotate_minus_30(-30.0, 4, Affine::new([0.866, -0.5, 0.5, 0.866, 0.0, 0.0]))]
5182    #[case::rotate_almost_90(
5183        90.01, 8, Affine::new([-0.00017453, 0.99999998, -0.99999998, -0.00017453, 0.0, 0.0])
5184    )]
5185    fn non_cardinal_rotation_left_untouched(
5186        #[case] angle: f64,
5187        #[case] precision: u8,
5188        #[case] expected: Affine,
5189    ) {
5190        // any other angles' sin and cos != (0, ±1) are passed through unchanged
5191        assert_eq!(expected, round(normalized_rotation(angle), precision));
5192    }
5193
5194    #[test]
5195    fn parse_colrv1_identify_colr_glyphs() {
5196        let font = Font::load(&glyphs3_dir().join("COLRv1-gradient.glyphs")).unwrap();
5197        let expected_colr = HashSet::from(["A", "B", "C", "D", "K", "L", "M", "N"]);
5198        assert_eq!(
5199            expected_colr,
5200            font.glyphs
5201                .values()
5202                .filter(|g| g.layers.iter().all(|l| l.attributes.color))
5203                .map(|g| g.name.as_str())
5204                .collect::<HashSet<_>>()
5205        );
5206    }
5207
5208    #[test]
5209    fn parse_colrv1_gradients() {
5210        let font = Font::load(&glyphs3_dir().join("COLRv1-gradient.glyphs")).unwrap();
5211        let expected_colr = HashSet::from([
5212            (
5213                "A",
5214                Gradient {
5215                    start: vec![OrderedFloat(0.1), OrderedFloat(0.1)],
5216                    end: vec![OrderedFloat(0.9), OrderedFloat(0.9)],
5217                    colors: vec![
5218                        ColorStop {
5219                            color: Color::rgba(255, 0, 0, 255),
5220                            stop_offset: 0.into(),
5221                        },
5222                        ColorStop {
5223                            color: Color::rgba(0, 0, 255, 255),
5224                            stop_offset: 1.into(),
5225                        },
5226                    ],
5227                    ..Default::default()
5228                },
5229            ),
5230            (
5231                "N",
5232                Gradient {
5233                    start: vec![OrderedFloat(1.0), OrderedFloat(1.0)],
5234                    end: vec![OrderedFloat(0.0), OrderedFloat(0.0)],
5235                    colors: vec![
5236                        ColorStop {
5237                            color: Color::rgba(255, 0, 0, 255),
5238                            stop_offset: 0.into(),
5239                        },
5240                        ColorStop {
5241                            color: Color::rgba(0, 0, 255, 255),
5242                            stop_offset: 1.into(),
5243                        },
5244                    ],
5245                    style: "circle".to_string(),
5246                },
5247            ),
5248        ]);
5249        assert_eq!(
5250            expected_colr,
5251            font.glyphs
5252                .values()
5253                .filter(|g| expected_colr.iter().any(|(name, _)| *name == g.name))
5254                .flat_map(|g| g.layers.iter().flat_map(|l| l.shapes.iter()).map(|s| (
5255                    g.name.as_str(),
5256                    s.attributes().gradient.as_ref().unwrap().clone()
5257                )))
5258                .collect::<HashSet<_>>()
5259        );
5260    }
5261
5262    #[test]
5263    fn parse_grayscale_colors() {
5264        let font = Font::load(&glyphs3_dir().join("COLRv1-grayscale.glyphs")).unwrap();
5265        assert_eq!(
5266            vec![
5267                ColorStop {
5268                    color: Color::rgba(64, 64, 64, 255),
5269                    stop_offset: 0.into(),
5270                },
5271                ColorStop {
5272                    color: Color::rgba(0, 0, 0, 255),
5273                    stop_offset: 1.into(),
5274                },
5275            ],
5276            font.glyphs
5277                .values()
5278                .flat_map(
5279                    |g| g.layers.iter().flat_map(|l| l.shapes.iter()).flat_map(|s| s
5280                        .attributes()
5281                        .gradient
5282                        .iter()
5283                        .flat_map(|g| g.colors.iter())
5284                        .cloned())
5285                )
5286                .collect::<Vec<_>>()
5287        );
5288    }
5289
5290    // we want to match glyphsLib and use a value of 600 if the width is missing.
5291    #[test]
5292    fn missing_width_no_problem() {
5293        let font = Font::load(&glyphs3_dir().join("MissingWidth.glyphs")).unwrap();
5294        let glyph = font.glyphs.get("widthless").unwrap();
5295        assert_eq!(glyph.layers[0].width, 600.);
5296    }
5297
5298    #[test]
5299    fn read_preferred_names_from_properties() {
5300        let font = Font::load(&glyphs3_dir().join("PreferableNames.glyphs")).unwrap();
5301        assert_eq!(
5302            vec![
5303                Some("Pref Family Name"),
5304                Some("Pref Regular"),
5305                Some("Name 25?!")
5306            ],
5307            vec![
5308                font.get_default_name("preferredFamilyNames"),
5309                font.get_default_name("preferredSubfamilyNames"),
5310                font.get_default_name("variationsPostScriptNamePrefix"),
5311            ]
5312        );
5313    }
5314
5315    #[test]
5316    fn zero_value_metrics() {
5317        let font = Font::load(&glyphs3_dir().join("ZeroMetrics.glyphs")).unwrap();
5318        let master = font.default_master();
5319        assert_eq!(master.ascender(), Some(789.));
5320        // this has no value set, but has overshoot set, so this should be a 0
5321        assert_eq!(master.cap_height(), Some(0.));
5322        // this has neither value nor overshoot, but since the metric is defined,
5323        // it's still here and still zero
5324        assert_eq!(master.x_height(), Some(0.));
5325        // but this metric is not defined in the font, so it it's `None`
5326        assert_eq!(master.italic_angle(), None);
5327    }
5328
5329    /// If there is a variable instance and it does not have a set familyName,
5330    /// we should still use names stored in that instance.
5331    #[test]
5332    fn names_from_instances() {
5333        let font = Font::load(&glyphs3_dir().join("InstanceNames.glyphs")).unwrap();
5334        assert_eq!(
5335            font.get_default_name("preferredSubfamilyNames").unwrap(),
5336            "Italic"
5337        )
5338    }
5339
5340    #[rstest]
5341    #[case::v2(glyphs2_dir())]
5342    #[case::v3(glyphs3_dir())]
5343    fn glyph_production_names(#[case] glyphs_dir: PathBuf) {
5344        let font = Font::load(&glyphs_dir.join("ProductionNames.glyphs")).unwrap();
5345        let glyphs = font.glyphs;
5346
5347        // this glyph has no production name in GlyphData.xml nor in the .glyphs file
5348        assert_eq!(glyphs.get("A").unwrap().production_name, None);
5349
5350        // this one would have 'dotlessi' in GlyphData.xml, but the .glyphs file overrides it
5351        assert_eq!(
5352            glyphs.get("idotless").unwrap().production_name,
5353            Some("uni0131".into())
5354        );
5355
5356        // this has no custom production_name in the .glyphs file, and the one from
5357        // GlyphData.xml is used
5358        assert_eq!(
5359            glyphs.get("nbspace").unwrap().production_name,
5360            Some("uni00A0".into())
5361        );
5362    }
5363
5364    #[test]
5365    fn parse_axis_rules() {
5366        let raw = RawFont::load(&glyphs3_dir().join("AxisRules.glyphs")).unwrap();
5367        let space = &raw.glyphs[0];
5368        let axes = &space.layers[0].attributes.axis_rules;
5369        assert_eq!(
5370            axes,
5371            &[
5372                AxisRule {
5373                    min: None,
5374                    max: Some(400)
5375                },
5376                AxisRule {
5377                    min: Some(100),
5378                    max: None,
5379                },
5380                AxisRule {
5381                    min: None,
5382                    max: None,
5383                },
5384            ]
5385        )
5386    }
5387
5388    #[test]
5389    fn drop_axis_rules_if_no_assoc_master() {
5390        // as in, if it's a bracket layer it doesn't need axis rules
5391        let font = Font::load(&glyphs3_dir().join("AxisRules.glyphs")).unwrap();
5392        let space = font.glyphs.get("space").unwrap();
5393        assert!(space.layers[0].attributes.axis_rules.is_empty());
5394    }
5395
5396    #[test]
5397    fn parse_v2_bracket_layers() {
5398        let font = Font::load(&glyphs2_dir().join("AxisRules.glyphs")).unwrap();
5399        let glyph = font.glyphs.get("space").unwrap();
5400        assert_eq!(
5401            glyph.bracket_layers[0].attributes.axis_rules,
5402            [
5403                AxisRule {
5404                    min: None,
5405                    max: Some(141)
5406                },
5407                AxisRule::default()
5408            ]
5409        );
5410    }
5411
5412    #[test]
5413    fn parse_layer_normal() {
5414        for name in &["[60]", "Light Extended [ 60 ]"] {
5415            let rule = AxisRule::from_layer_name(name);
5416            assert_eq!(
5417                rule,
5418                Some(AxisRule {
5419                    min: Some(60),
5420                    max: None
5421                }),
5422                "{name}"
5423            )
5424        }
5425    }
5426
5427    #[test]
5428    fn parse_layer_reversed() {
5429        for name in &["]60]", "Light Extended ] 60 ]"] {
5430            let rule = AxisRule::from_layer_name(name);
5431            assert_eq!(
5432                rule,
5433                Some(AxisRule {
5434                    min: None,
5435                    max: Some(60)
5436                })
5437            )
5438        }
5439    }
5440
5441    #[test]
5442    fn parse_layer_fails() {
5443        for name in &["[hi]", "[45opsz]", "Medium [499‹wg]"] {
5444            assert!(AxisRule::from_layer_name(name).is_none(), "{name}")
5445        }
5446    }
5447
5448    #[test]
5449    fn v2_bracket_layers() {
5450        let font = Font::load(&glyphs2_dir().join("WorkSans-minimal-bracketlayer.glyphs")).unwrap();
5451        let glyph = font.glyphs.get("colonsign").unwrap();
5452        assert_eq!(glyph.layers.len(), 3);
5453        assert_eq!(glyph.bracket_layers.len(), 3);
5454
5455        assert!(
5456            glyph
5457                .layers
5458                .iter()
5459                .all(|l| l.attributes.axis_rules.is_empty())
5460        );
5461        assert!(
5462            glyph
5463                .bracket_layers
5464                .iter()
5465                .all(|l| !l.attributes.axis_rules.is_empty())
5466        );
5467    }
5468
5469    #[test]
5470    fn v3_bracket_layers() {
5471        let font = Font::load(&glyphs3_dir().join("LibreFranklin-bracketlayer.glyphs")).unwrap();
5472        let glyph = font.glyphs.get("peso").unwrap();
5473        assert_eq!(glyph.layers.len(), 2);
5474        assert_eq!(glyph.bracket_layers.len(), 2);
5475
5476        assert!(
5477            glyph
5478                .layers
5479                .iter()
5480                .all(|l| l.attributes.axis_rules.is_empty())
5481        );
5482        assert!(
5483            glyph
5484                .bracket_layers
5485                .iter()
5486                .all(|l| !l.attributes.axis_rules.is_empty())
5487        );
5488    }
5489
5490    #[test]
5491    fn bracket_layers_where_only_brackets_have_a_component_and_it_has_anchors() {
5492        let font = Font::load(&glyphs2_dir().join("AlumniSans-wononly.glyphs")).unwrap();
5493        let glyph = font.glyphs.get("won").unwrap();
5494
5495        assert_eq!(glyph.layers.len(), 2);
5496        assert_eq!(glyph.bracket_layers.len(), 2);
5497
5498        for layer in glyph.layers.iter().chain(glyph.bracket_layers.iter()) {
5499            assert_eq!(layer.anchors.len(), 2, "{}", layer.layer_id);
5500        }
5501    }
5502
5503    #[test]
5504    fn glyphs2_weight_class_custom_instance_parameter() {
5505        // older glyphs sources can use a custom param in the instance
5506        // in order to pass a custom value for the weight class.
5507        let font = Font::load(&glyphs2_dir().join("WeightClassCustomParam.glyphs")).unwrap();
5508        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
5509        assert_eq!(
5510            mapping.as_slice(),
5511            &[
5512                (OrderedFloat(200f64), OrderedFloat(42f64)),
5513                (OrderedFloat(700.), OrderedFloat(125.)),
5514                (OrderedFloat(1000.), OrderedFloat(208.))
5515            ]
5516        )
5517    }
5518
5519    #[test]
5520    fn glyphs2_avoid_adding_mapping_from_master() {
5521        // this source has a master which does not correspond to an instance,
5522        // but the instances provide a valid non-identity mapping; we should
5523        // avoid adding the mapping from the master.
5524
5525        let font = Font::load(&glyphs2_dir().join("master_missing_from_instances.glyphs")).unwrap();
5526
5527        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
5528        assert_eq!(
5529            mapping.as_slice(),
5530            &[
5531                (OrderedFloat(100f64), OrderedFloat(20f64)),
5532                (OrderedFloat(300.), OrderedFloat(50.)),
5533                (OrderedFloat(400.), OrderedFloat(71.)),
5534                (OrderedFloat(700.), OrderedFloat(156.)),
5535                (OrderedFloat(900.), OrderedFloat(226.)),
5536            ]
5537        )
5538    }
5539
5540    #[test]
5541    fn does_not_truncate_axis_location() {
5542        let font = Font::load(&glyphs3_dir().join("WghtVar_AxisLocationFloat.glyphs")).unwrap();
5543        let mapping = &font.axis_mappings.0.get("Weight").unwrap().0;
5544
5545        // The user location 400.75 must *NOT* be truncated
5546        assert_eq!(
5547            mapping.as_slice(),
5548            &[(OrderedFloat(400.75), OrderedFloat(0f64)),]
5549        )
5550    }
5551
5552    fn make_axis_location_params(values: &[(&str, i64)]) -> RawCustomParameterValue {
5553        RawCustomParameterValue {
5554            name: "Axis Location".into(),
5555            value: Plist::Array(
5556                values
5557                    .iter()
5558                    .map(|(name, loc)| {
5559                        Plist::Dictionary(BTreeMap::from([
5560                            ("Axis".into(), Plist::String(name.to_string())),
5561                            ("Location".into(), Plist::Integer(*loc)),
5562                        ]))
5563                    })
5564                    .collect(),
5565            ),
5566            disabled: None,
5567        }
5568    }
5569
5570    #[test]
5571    fn user_to_design_with_no_axes() {
5572        let _ = env_logger::builder().is_test(true).try_init();
5573        let mut myfont = RawFont {
5574            font_master: vec![RawFontMaster {
5575                custom_parameters: RawCustomParameters(vec![make_axis_location_params(&[(
5576                    "Weight", 400,
5577                )])]),
5578                ..Default::default()
5579            }],
5580            ..Default::default()
5581        };
5582
5583        let _just_dont_crash_plz = user_to_design_from_axis_location(&mut myfont);
5584    }
5585
5586    #[test]
5587    fn user_to_design_with_unknown_axis_location() {
5588        let _ = env_logger::builder().is_test(true).try_init();
5589        let mut myfont = RawFont {
5590            axes: vec![Axis {
5591                name: "Weight".into(),
5592                tag: "wght".into(),
5593                hidden: None,
5594            }],
5595            font_master: vec![RawFontMaster {
5596                custom_parameters: RawCustomParameters(vec![make_axis_location_params(&[
5597                    ("Weight", 400),
5598                    ("Italic", 0),
5599                ])]),
5600                axes_values: vec![60.0.into()],
5601                ..Default::default()
5602            }],
5603            ..Default::default()
5604        };
5605
5606        let _just_dont_crash_plz = user_to_design_from_axis_location(&mut myfont);
5607    }
5608
5609    #[test]
5610    fn axis_location_string_value() {
5611        let raw = Plist::Dictionary(BTreeMap::from([
5612            ("Axis".into(), Plist::String("Weight".into())),
5613            ("Location".into(), Plist::String("42".into())),
5614        ]));
5615        assert_eq!(
5616            Some(AxisLocation {
5617                axis_name: "Weight".into(),
5618                location: 42.0.into(),
5619            }),
5620            raw.as_axis_location()
5621        );
5622    }
5623
5624    #[test]
5625    fn glyphs2_unicode_ranges() {
5626        let font = Font::load(&glyphs2_dir().join("UnicodeRanges.glyphs")).unwrap();
5627        assert_eq!(
5628            Some(BTreeSet::from([8])),
5629            font.custom_parameters.unicode_range_bits
5630        );
5631    }
5632
5633    #[test]
5634    fn parse_smart_components_v3() {
5635        // we load from raw directly so that we can skip preprocessing, which would
5636        // replace the components with the actual outlines
5637        let font = RawFont::load(&glyphs3_dir().join("SmartComponents.glyphs")).unwrap();
5638        let font = Font::try_from(font).unwrap();
5639        let shoulder = font.glyphs.get("_part.shoulder").unwrap();
5640        assert_eq!(
5641            shoulder.smart_component_axes,
5642            BTreeMap::from([
5643                ("shoulderWidth".into(), 0..=100),
5644                ("crotchDepth".into(), -100..=0)
5645            ])
5646        );
5647
5648        let m = font.glyphs.get("m").unwrap();
5649        let expected = [
5650            Component {
5651                name: "_part.stem".into(),
5652                ..Default::default()
5653            },
5654            Component {
5655                name: "_part.shoulder".into(),
5656                transform: Affine::translate((17., 0.)),
5657                smart_component_values: BTreeMap::from([
5658                    ("crotchDepth".into(), -44.28304),
5659                    ("shoulderWidth".into(), 28.78011),
5660                ]),
5661                ..Default::default()
5662            },
5663            Component {
5664                name: "_part.shoulder".into(),
5665                transform: Affine::translate((180., 1.)),
5666                smart_component_values: BTreeMap::from([
5667                    ("crotchDepth".into(), -81.24796),
5668                    ("shoulderWidth".into(), 0.0),
5669                ]),
5670                ..Default::default()
5671            },
5672        ];
5673        assert_eq!(
5674            m.layers[0].components().cloned().collect::<Vec<_>>(),
5675            expected
5676        );
5677    }
5678
5679    fn instantiate_shoulder_at(location: &[(&str, f64)]) -> Shape {
5680        fn make_component_values(inp: &[(&str, f64)]) -> BTreeMap<SmolStr, f64> {
5681            inp.iter().map(|(k, v)| (k.to_smolstr(), *v)).collect()
5682        }
5683        let font = RawFont::load(&glyphs3_dir().join("SmartComponents.glyphs")).unwrap();
5684        let font = Font::try_from(font).unwrap();
5685
5686        let smart_comp = font.glyphs.get("_part.shoulder").unwrap();
5687        let default_pos = Component {
5688            name: "_part.shoulder".into(),
5689            smart_component_values: make_component_values(location),
5690            ..Default::default()
5691        };
5692
5693        let shapes =
5694            smart_components::instantiate_for_layer("m01", &default_pos, smart_comp).unwrap();
5695        assert_eq!(shapes.len(), 1);
5696        shapes[0].clone()
5697    }
5698
5699    #[test]
5700    fn instantiate_smart_component_at_default() {
5701        let font = Font::load(&glyphs3_dir().join("SmartComponents.glyphs")).unwrap();
5702        let smart_comp = font.glyphs.get("_part.shoulder").unwrap();
5703        let shape = instantiate_shoulder_at(&[("shoulderWidth", 100.), ("crotchDepth", 0.)]);
5704
5705        assert_eq!(smart_comp.layers[1].layer_id, "m01");
5706        assert_eq!(shape, smart_comp.layers[1].shapes[0]);
5707    }
5708
5709    #[test]
5710    fn instantiate_smart_component_at_not_default() {
5711        let Shape::Path(path) =
5712            instantiate_shoulder_at(&[("shoulderWidth", 0.), ("crotchDepth", -100.)])
5713        else {
5714            panic!("wat")
5715        };
5716        // the first point is at its lowest value (crotch -100) (first point is at end!)
5717        assert_eq!(path.nodes.last().unwrap().pt.round().y, 267.0);
5718        // the shoulder width means the max x is also at its lowest value
5719        assert_eq!(
5720            path.nodes.iter().map(|nd| nd.pt.x.round() as i64).max(),
5721            Some(335)
5722        );
5723    }
5724
5725    impl crate::Path {
5726        fn bbox(&self) -> Option<Rect> {
5727            let points = self.to_points();
5728            let (head, rest) = points.as_slice().split_first()?;
5729            Some(
5730                rest.iter()
5731                    .fold(Rect::ZERO.with_origin(*head), |bbox, pt| bbox.union_pt(*pt)),
5732            )
5733        }
5734    }
5735
5736    #[test]
5737    fn smart_component_v2() {
5738        let font = Font::load(&glyphs2_dir().join("SmartComponent.glyphs")).unwrap();
5739
5740        let glyph = font.glyphs.get("composite").unwrap();
5741        let shapes = &glyph.layers[0].shapes;
5742        let paths = shapes
5743            .iter()
5744            .map(|s| s.as_path().unwrap().to_points())
5745            .collect::<Vec<_>>();
5746        let expected1 = [(9., 405.), (9., 300.), (276., 300.), (276.0, 405.0)]
5747            .into_iter()
5748            .map(Point::from)
5749            .collect::<Vec<_>>();
5750
5751        let expected2 = [(9., 405.), (9., 383.), (276., 383.), (276.0, 405.0)]
5752            .into_iter()
5753            .map(|pt| Point::from(pt) + Vec2::new(100., 100.))
5754            .collect::<Vec<_>>();
5755
5756        assert_eq!(paths, [expected1, expected2]);
5757    }
5758
5759    #[test]
5760    fn nested_smart_components() {
5761        // ensure that if a smart component has its own smart components, those
5762        // get instantiated first.
5763        let font = Font::load(&glyphs2_dir().join("NestedSmartComponent.glyphs")).unwrap();
5764        let glyph = font.glyphs.get("w").unwrap();
5765        let shapes = &glyph.layers[0].shapes;
5766        let rects = shapes
5767            .iter()
5768            .map(|s| s.as_path().unwrap().bbox().unwrap())
5769            .collect::<Vec<_>>();
5770        assert_eq!(
5771            rects,
5772            [
5773                Rect::new(0., 200., 400., 400.),
5774                Rect::new(0., 0., 400., 100.),
5775                Rect::new(500., 200., 550., 400.),
5776                Rect::new(500., 0., 550., 100.),
5777            ]
5778        );
5779    }
5780
5781    #[test]
5782    fn parse_basic_corner_component_hint() {
5783        let font = Font::load_raw(glyphs3_dir().join("CornerComponents.glyphs")).unwrap();
5784
5785        let glyph = font.glyphs.get("aa_simple_angleinstroke").unwrap();
5786        let hints = &glyph.layers[0].hints;
5787
5788        assert_eq!(hints.len(), 1);
5789        assert_eq!(hints[0].name, "_corner.one");
5790        assert_eq!(hints[0].shape_index, 0);
5791        assert_eq!(hints[0].node_index, 0);
5792        assert_eq!(hints[0].type_, HintType::Corner);
5793        assert_eq!(hints[0].scale, Default::default());
5794        assert_eq!(hints[0].alignment, Alignment::OutStroke);
5795    }
5796
5797    #[test]
5798    fn parse_corner_component_hint_with_scale() {
5799        let font = Font::load_raw(glyphs3_dir().join("CornerComponents.glyphs")).unwrap();
5800
5801        let glyph = font.glyphs.get("ac_scale").unwrap();
5802        let hints = &glyph.layers[0].hints;
5803
5804        assert_eq!(hints.len(), 1);
5805        assert_eq!(hints[0].name, "_corner.one");
5806        assert_eq!(hints[0].shape_index, 0);
5807        assert_eq!(hints[0].node_index, 0);
5808        assert_eq!(hints[0].type_, HintType::Corner);
5809        assert_eq!(hints[0].scale, (Scale::new(1.2, 1.5)));
5810        assert_eq!(hints[0].alignment, Alignment::OutStroke);
5811    }
5812
5813    #[test]
5814    fn parse_corner_component_hint_with_alignment() {
5815        let font = Font::load_raw(glyphs3_dir().join("CornerComponents.glyphs")).unwrap();
5816
5817        let glyph = font.glyphs.get("aj_right_alignment").unwrap();
5818        let hints = &glyph.layers[0].hints;
5819
5820        assert_eq!(hints.len(), 1);
5821        assert_eq!(hints[0].name, "_corner.one");
5822        assert_eq!(hints[0].shape_index, 0);
5823        assert_eq!(hints[0].node_index, 1);
5824        assert_eq!(hints[0].type_, HintType::Corner);
5825        assert_eq!(hints[0].scale, Default::default());
5826        assert_eq!(hints[0].alignment, Alignment::InStroke);
5827    }
5828
5829    #[test]
5830    fn parse_v2_corner_component() {
5831        let hint_plist = r#"
5832{
5833origin = "{0, 0}";
5834scale = "{0.7, 1}";
5835type = Corner;
5836name = _corner.hi;
5837}
5838        "#;
5839
5840        let hint = RawHint::parse_plist(hint_plist).unwrap().to_hint().unwrap();
5841        assert_eq!(hint.scale.x, 0.7);
5842        assert_eq!(hint.scale.y, 1.0);
5843    }
5844
5845    #[test]
5846    fn link_metrics_with_first_master_parsing() {
5847        let font = Font::load(&glyphs3_dir().join("LinkMetricsWithFirstMaster.glyphs")).unwrap();
5848        assert_eq!(font.masters.len(), 2);
5849
5850        // First master should NOT have linked metrics
5851        assert_eq!(
5852            font.masters[0]
5853                .custom_parameters
5854                .link_metrics_with_first_master,
5855            None
5856        );
5857        // Second master should have "Link Metrics With First Master" set to true
5858        assert_eq!(
5859            font.masters[1]
5860                .custom_parameters
5861                .link_metrics_with_first_master,
5862            Some(true)
5863        );
5864    }
5865
5866    #[test]
5867    fn link_metrics_with_master_parsing() {
5868        let font = Font::load(&glyphs3_dir().join("LinkMetricsWithMaster.glyphs")).unwrap();
5869        assert_eq!(font.masters.len(), 3);
5870
5871        // First two masters should NOT have linked metrics
5872        assert_eq!(
5873            font.masters[0].custom_parameters.link_metrics_with_master,
5874            None
5875        );
5876        assert_eq!(
5877            font.masters[1].custom_parameters.link_metrics_with_master,
5878            None
5879        );
5880        // Third master (Black) should link to Bold's master ID
5881        let link_target = font.masters[2]
5882            .custom_parameters
5883            .link_metrics_with_master
5884            .as_deref();
5885        assert_eq!(link_target, Some("m02"));
5886        assert_eq!(font.masters[1].id, "m02");
5887        assert_eq!(font.masters[1].name, "Bold");
5888    }
5889
5890    #[test]
5891    fn metrics_source_id_with_first_master() {
5892        let font = Font::load(&glyphs3_dir().join("LinkMetricsWithFirstMaster.glyphs")).unwrap();
5893
5894        // First master should return None (uses its own metrics)
5895        assert_eq!(font.masters[0].metrics_source_id, None);
5896        // Second master should return first master's ID
5897        assert_eq!(
5898            font.masters[1].metrics_source_id.as_ref(),
5899            Some(&font.masters[0].id)
5900        );
5901    }
5902
5903    #[test]
5904    fn metrics_source_id_with_master_by_id() {
5905        let font = Font::load(&glyphs3_dir().join("LinkMetricsWithMaster.glyphs")).unwrap();
5906
5907        assert_eq!(font.masters[0].metrics_source_id, None);
5908        assert_eq!(font.masters[1].metrics_source_id, None);
5909        // Third master (Black) should return second master's ID (Bold)
5910        assert_eq!(
5911            font.masters[2].metrics_source_id.as_ref(),
5912            Some(&font.masters[1].id)
5913        );
5914    }
5915
5916    #[test]
5917    fn metrics_source_id_with_master_by_name() {
5918        // Glyphs 2 format uses master names instead of IDs for "Link Metrics With Master"
5919        let font = Font::load(&glyphs2_dir().join("LinkMetricsWithMasterByName.glyphs")).unwrap();
5920
5921        assert_eq!(font.masters[0].metrics_source_id, None);
5922        assert_eq!(font.masters[1].metrics_source_id, None);
5923        // Third master (Black) links by name "Bold", should resolve to second master's ID
5924        assert_eq!(
5925            font.masters[2]
5926                .custom_parameters
5927                .link_metrics_with_master
5928                .as_deref(),
5929            Some("Bold")
5930        );
5931        assert_eq!(
5932            font.masters[2].metrics_source_id.as_ref(),
5933            Some(&font.masters[1].id)
5934        );
5935    }
5936
5937    #[test]
5938    fn metrics_source_id_with_missing_master() {
5939        let font = Font::load(&glyphs3_dir().join("LinkMetricsWithMissingMaster.glyphs")).unwrap();
5940
5941        // Second master links to "NonExistent" which doesn't exist
5942        assert_eq!(
5943            font.masters[1]
5944                .custom_parameters
5945                .link_metrics_with_master
5946                .as_deref(),
5947            Some("NonExistent")
5948        );
5949        // it should return None and log a warning like glyphsLib does
5950        assert_eq!(font.masters[1].metrics_source_id, None);
5951    }
5952
5953    #[test]
5954    fn duplicate_custom_params_uses_first_value() {
5955        // Test that when duplicate custom parameters exist, the first value is used.
5956        // This matches glyphsLib's behavior where customParameters["key"] returns the
5957        // first matching item via a simple for-loop iteration.
5958        let raw_params = RawCustomParameters(vec![
5959            RawCustomParameterValue {
5960                name: "Link Metrics With Master".into(),
5961                value: Plist::String("first_master_id".into()),
5962                disabled: None,
5963            },
5964            RawCustomParameterValue {
5965                name: "Link Metrics With Master".into(),
5966                value: Plist::String("second_master_id".into()),
5967                disabled: None,
5968            },
5969        ]);
5970
5971        let params = raw_params.to_custom_params().unwrap();
5972
5973        // The first value should win, not the second
5974        assert_eq!(
5975            params.link_metrics_with_master.as_deref(),
5976            Some("first_master_id")
5977        );
5978    }
5979}