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