Skip to main content

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