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