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