Skip to main content

google_fonts_axisregistry/
lib.rs

1use gf_metadata::{AxisProto, FallbackProto};
2use std::{collections::HashSet, ops::Index};
3
4#[cfg(feature = "fontations")]
5use fontations::skrifa::string::StringId;
6#[cfg(feature = "fontations")]
7use fontations::{
8    read::FontRef,
9    read::{ReadError, TableProvider},
10    write::FontBuilder,
11};
12use indexmap::IndexMap;
13
14include!(concat!(env!("OUT_DIR"), "/data.rs"));
15
16const LINKED_VALUES: [(&str, (f32, f32)); 2] = [("wght", (400.0, 700.0)), ("ital", (0.0, 1.0))];
17
18fn linked_value(axis: &str, value: f32) -> Option<f32> {
19    LINKED_VALUES
20        .iter()
21        .find(|(linked_axis, (cur, _link))| *linked_axis == axis && value == *cur)
22        .map(|(_, (_, link))| *link)
23}
24
25const GF_STATIC_STYLES: [(&str, u16); 18] = [
26    ("Thin", 100),
27    ("ExtraLight", 200),
28    ("Light", 300),
29    ("Regular", 400),
30    ("Medium", 500),
31    ("SemiBold", 600),
32    ("Bold", 700),
33    ("ExtraBold", 800),
34    ("Black", 900),
35    ("Thin Italic", 100),
36    ("ExtraLight Italic", 200),
37    ("Light Italic", 300),
38    ("Italic", 400),
39    ("Medium Italic", 500),
40    ("SemiBold Italic", 600),
41    ("Bold Italic", 700),
42    ("ExtraBold Italic", 800),
43    ("Black Italic", 900),
44];
45
46#[cfg(feature = "fontations")]
47const PROTECTED_IDS: [StringId; 9] = [
48    StringId::FAMILY_NAME,
49    StringId::SUBFAMILY_NAME,
50    StringId::UNIQUE_ID,
51    StringId::FULL_NAME,
52    StringId::VERSION_STRING,
53    StringId::POSTSCRIPT_NAME,
54    StringId::TYPOGRAPHIC_FAMILY_NAME,
55    StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
56    StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
57];
58
59pub struct AxisRegistry {
60    axes: BTreeMap<String, Box<AxisProto>>,
61}
62
63#[derive(Debug, Clone)]
64pub struct FontAxis {
65    pub tag: String,
66    pub min: f32,
67    pub max: f32,
68    pub default: f32,
69}
70
71#[derive(Debug, Clone)]
72pub struct NameParticle {
73    pub name: Option<String>,
74    pub value: f32,
75    pub elided: bool,
76}
77
78impl AxisRegistry {
79    pub fn new() -> Self {
80        Self {
81            axes: (*AXES).clone(),
82        }
83    }
84
85    pub fn get(&self, tag: &str) -> Option<&AxisProto> {
86        self.axes.get(tag).map(|v| &**v)
87    }
88
89    pub fn contains_key(&self, tag: &str) -> bool {
90        self.axes.contains_key(tag)
91    }
92
93    pub fn iter(&self) -> impl Iterator<Item = (&String, &AxisProto)> {
94        self.axes.iter().map(|(k, v)| (k, &**v))
95    }
96
97    pub fn get_fallback<'a>(&'a self, name: &str) -> Option<(&'a str, &'a FallbackProto)> {
98        self.axes
99            .iter()
100            .flat_map(|(tag, axis)| {
101                let fallback = axis
102                    .fallback
103                    .iter()
104                    .find(|f| f.name.as_deref() == Some(name));
105                fallback.map(|f| (tag.as_str(), f))
106            })
107            .next()
108    }
109
110    // This is fallbacks_in_fvar, but without assuming any particular font representation
111    pub fn fallbacks<'a>(
112        &'a self,
113        font_axes: &'a [FontAxis],
114    ) -> impl Iterator<Item = (String, Vec<FallbackProto>)> + 'a {
115        font_axes.iter().filter_map(|axis| {
116            self.get(&axis.tag).map(|registry_axis| {
117                (
118                    registry_axis.tag.clone().unwrap_or_default(),
119                    registry_axis
120                        .fallback
121                        .iter()
122                        .filter(|f| f.value() >= axis.min && f.value() <= axis.max)
123                        .cloned()
124                        .collect(),
125                )
126            })
127        })
128    }
129
130    // This is fallbacks_in_name_table, but without assuming any particular font representation
131    pub fn name_table_fallbacks<'a>(
132        &'a self,
133        family_name: &'a str,
134        subfamily_name: &'a str,
135        font_axes: &'a [FontAxis],
136    ) -> impl Iterator<Item = (&'a str, &'a FallbackProto)> + 'a {
137        let axis_names: HashSet<&str> = font_axes.iter().map(|axis| axis.tag.as_ref()).collect();
138        let tokens = family_name
139            .split_whitespace()
140            .skip(1)
141            .chain(subfamily_name.split_whitespace());
142        tokens
143            .flat_map(|token| self.get_fallback(token))
144            .filter(move |(tag, _)| !axis_names.contains(tag))
145    }
146
147    pub fn fallback_for_value<'a>(
148        &'a self,
149        axis_tag: &str,
150        value: f32,
151    ) -> Option<&'a FallbackProto> {
152        self.get(axis_tag)
153            .and_then(|axis| axis.fallback.iter().find(|f| f.value == Some(value)))
154    }
155
156    pub fn axis_order(&self) -> Vec<&str> {
157        let mut axis_tags: Vec<&str> = self
158            .axes
159            .keys()
160            .filter(|k| k.chars().all(|c| c.is_ascii_uppercase()))
161            .map(|k| k.as_str())
162            .collect();
163        axis_tags.sort();
164        axis_tags.extend(vec!["opsz", "wdth", "wght", "ital", "slnt"]);
165        axis_tags
166    }
167
168    // This is the old "_fvar_dflts"
169    pub fn name_particles<'a>(&self, font_axes: &'a [FontAxis]) -> IndexMap<&'a str, NameParticle> {
170        let mut particles = IndexMap::new();
171        for axis in font_axes {
172            if axis.tag == "opsz" {
173                particles.insert(
174                    "opsz",
175                    NameParticle {
176                        name: Some(format!("{}pt", axis.default)),
177                        value: axis.default,
178                        elided: true,
179                    },
180                );
181            } else if let Some(fallback) = self.fallback_for_value(&axis.tag, axis.default) {
182                particles.insert(
183                    &axis.tag,
184                    NameParticle {
185                        name: fallback.name.clone(),
186                        value: axis.default,
187                        elided: (fallback.value() == self.get(&axis.tag).unwrap().default_value())
188                            && !(["Regular", "Italic", "14pt"].contains(&fallback.name())),
189                    },
190                );
191            } else {
192                particles.insert(
193                    &axis.tag,
194                    NameParticle {
195                        name: None,
196                        value: axis.default,
197                        elided: true,
198                    },
199                );
200            };
201        }
202        particles
203    }
204}
205
206impl Default for AxisRegistry {
207    fn default() -> Self {
208        Self::new()
209    }
210}
211
212impl Index<&str> for AxisRegistry {
213    type Output = AxisProto;
214
215    fn index(&self, tag: &str) -> &Self::Output {
216        self.get(tag).expect("No such axis")
217    }
218}
219
220#[cfg(feature = "fontations")]
221mod monkeypatching;
222#[cfg(feature = "fontations")]
223mod nametable;
224#[cfg(feature = "fontations")]
225mod stat;
226#[cfg(feature = "fontations")]
227mod fontations_impl {
228    use super::*;
229    use fontations::{
230        skrifa::{string::StringId, MetadataProvider, Tag},
231        write::{
232            from_obj::ToOwnedTable,
233            tables::{
234                fvar::{Fvar, InstanceRecord},
235                name::{Name, NameRecord},
236                os2::Os2,
237                stat::Stat,
238            },
239            types::Fixed,
240        },
241    };
242    use monkeypatching::{AxisValueNameId, SetAxisValueNameId};
243    use nametable::{
244        add_name, best_familyname, best_subfamilyname, find_or_add_name, rewrite_or_insert,
245    };
246    use stat::{AxisLocation, AxisRecord, AxisValue, StatBuilder};
247    use std::{cmp::Reverse, collections::HashMap};
248
249    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
250    pub enum RenameAggressiveness {
251        #[default]
252        Aggressive,
253        Conservative,
254    }
255
256    pub fn build_name_table(
257        font: FontRef,
258        family_name: Option<&str>,
259        style_name: Option<&str>,
260        siblings: &[FontRef],
261        aggressive: Option<RenameAggressiveness>,
262    ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
263        let mut new_font = FontBuilder::new();
264        let family_name = family_name
265            .map(|x| x.to_string())
266            .unwrap_or_else(|| best_familyname(&font).unwrap_or("Unknown".to_string()));
267        let style_name = style_name
268            .map(|x| x.to_string())
269            .unwrap_or_else(|| best_subfamilyname(&font).unwrap_or("Regular".to_string()));
270
271        let mut new_name = if font.table_data(Tag::new(b"fvar")).is_some() {
272            build_vf_name_table(&mut new_font, &font, &family_name, siblings, aggressive)?
273        } else {
274            build_static_name_table_v1(&mut new_font, &font, &family_name, &style_name, aggressive)?
275        };
276
277        let mut styles: Vec<_> = GF_STATIC_STYLES.iter().collect();
278        styles.sort_by_key(|(name, _weight)| Reverse(name.len()));
279        for (name, weight) in styles.iter() {
280            if style_name.contains(name) {
281                let mut new_os2: Os2 = font.os2()?.to_owned_table();
282                new_os2.us_weight_class = *weight;
283                new_font.add_table(&new_os2)?;
284                break;
285            }
286        }
287        // Set RIBBI bits
288        new_name.name_record.sort();
289        new_font.add_table(&new_name)?;
290        Ok(new_font.copy_missing_tables(font).build())
291    }
292
293    fn fvar_instance_collisions(font: &FontRef, siblings: &[FontRef]) -> bool {
294        let fonts = siblings.iter().chain(std::iter::once(font));
295        let is_italic = fonts
296            .map(|f| {
297                f.post()
298                    .map(|post| post.italic_angle().abs() != Fixed::from_f64(0.0))
299                    .unwrap_or(false)
300            })
301            .collect::<Vec<_>>();
302        is_italic.len() != is_italic.iter().collect::<HashSet<_>>().len()
303    }
304
305    fn build_vf_name_table(
306        newfont: &mut FontBuilder,
307        font: &FontRef,
308        family_name: &str,
309        siblings: &[FontRef],
310        aggressive: Option<RenameAggressiveness>,
311    ) -> Result<Name, Box<dyn std::error::Error>> {
312        let style_name = vf_style_name(font, family_name)?;
313        let mut new_name: Name = (if fvar_instance_collisions(font, siblings) {
314            build_static_name_table_v1(newfont, font, family_name, &style_name, aggressive)
315        } else {
316            build_static_name_table(newfont, font, family_name, style_name, aggressive)
317        })?;
318        // println!("Records: {:#?}", new_name.name_record);
319        build_variations_ps_name(&mut new_name, font, Some(family_name));
320
321        // Ensure table records are sorted
322        new_name.name_record.sort();
323        Ok(new_name)
324    }
325
326    pub fn build_variations_ps_name(newname: &mut Name, font: &FontRef, family_name: Option<&str>) {
327        let fallback = best_familyname(font);
328        let family_name = family_name.or(fallback.as_deref()).unwrap_or("New Font");
329        let subfamily_name = best_subfamilyname(font).unwrap_or("Regular".to_string());
330        let font_axes = font_axes(font).unwrap_or_default();
331        let registry = AxisRegistry::new();
332        let font_styles = registry.name_table_fallbacks(family_name, &subfamily_name, &font_axes);
333        let mut var_ps = family_name.replace(" ", "");
334        for (_, fallback) in font_styles {
335            let fallback_name = fallback.name();
336            if !var_ps.contains(fallback_name) {
337                var_ps.push_str(fallback_name);
338            }
339        }
340        rewrite_or_insert(
341            &mut newname.name_record,
342            StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
343            &var_ps,
344        );
345    }
346
347    fn build_static_name_table_v1(
348        newfont: &mut FontBuilder,
349        font: &FontRef,
350        family_name: &str,
351        style_name: &str,
352        aggressive: Option<RenameAggressiveness>,
353    ) -> Result<Name, Box<dyn std::error::Error>> {
354        let (v1_tokens, non_weight) = style_name
355            .split_whitespace()
356            .partition::<Vec<_>, _>(|token| GF_STATIC_STYLES.iter().any(|(name, _)| name == token));
357        let family_tokens = family_name.split_whitespace();
358        let mut new_family_name = vec![];
359        for token in family_tokens {
360            if non_weight.contains(&token) || new_family_name.contains(&token) {
361                continue;
362            }
363            new_family_name.push(token);
364        }
365        new_family_name.extend(non_weight);
366        let family_name = new_family_name.join(" ");
367        let mut style_name = v1_tokens
368            .join(" ")
369            .replace("Regular Italic", "Italic")
370            .trim()
371            .to_string();
372        if style_name.is_empty() {
373            style_name = "Regular".to_string();
374        }
375        build_static_name_table(newfont, font, &family_name, style_name, aggressive)
376    }
377
378    fn build_static_name_table(
379        newfont: &mut FontBuilder,
380        font: &FontRef,
381        family_name: &str,
382        style_name: String,
383        aggressive: Option<RenameAggressiveness>,
384    ) -> Result<Name, Box<dyn std::error::Error>> {
385        let mut name: Name = font.name()?.to_owned_table();
386        let mut records = name.name_record.into_iter().collect::<Vec<NameRecord>>();
387        records.retain(|record| record.platform_id != 1);
388        let existing_name = best_familyname(font).unwrap_or("New Font".to_string());
389        let mut removed_names: HashMap<StringId, String> = HashMap::new();
390        let full_name = family_name.to_string() + " " + &style_name;
391        let ps_name = (family_name.to_string() + "-" + &style_name).replace(" ", "");
392        let removeable_name_ids =
393            if ["Italic", "Bold Italic", "Bold", "Regular"].contains(&style_name.as_str()) {
394                rewrite_or_insert(&mut records, StringId::FAMILY_NAME, family_name);
395                rewrite_or_insert(&mut records, StringId::SUBFAMILY_NAME, &style_name);
396                rewrite_or_insert(&mut records, StringId::FULL_NAME, &full_name);
397                rewrite_or_insert(&mut records, StringId::POSTSCRIPT_NAME, &ps_name);
398                vec![
399                    StringId::TYPOGRAPHIC_FAMILY_NAME,
400                    StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
401                    StringId::new(21),
402                    StringId::new(22),
403                ]
404            } else {
405                let style_tokens = style_name.split_whitespace().collect::<Vec<_>>();
406                let mut new_family_name_tokens = family_name.split_whitespace().collect::<Vec<_>>();
407                let is_italic = style_tokens.contains(&"Italic");
408                let additional_tokens = (style_tokens.into_iter().filter(|token| {
409                    !(*token == "Regular"
410                        || *token == "Italic"
411                        || new_family_name_tokens.contains(token))
412                }))
413                .collect::<Vec<_>>();
414                new_family_name_tokens.extend(additional_tokens);
415                let new_family_name = new_family_name_tokens.join(" ");
416                let new_style_name = if is_italic { "Italic" } else { "Regular" };
417
418                rewrite_or_insert(&mut records, StringId::FAMILY_NAME, &new_family_name);
419                rewrite_or_insert(&mut records, StringId::SUBFAMILY_NAME, new_style_name);
420                rewrite_or_insert(&mut records, StringId::FULL_NAME, &full_name);
421                rewrite_or_insert(&mut records, StringId::POSTSCRIPT_NAME, &ps_name);
422                rewrite_or_insert(&mut records, StringId::TYPOGRAPHIC_FAMILY_NAME, family_name);
423                rewrite_or_insert(
424                    &mut records,
425                    StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
426                    &style_name,
427                );
428
429                vec![StringId::new(21), StringId::new(22)]
430            };
431
432        let mut to_delete = vec![];
433        for name_id in removeable_name_ids.into_iter() {
434            if let Some(existing) = records.iter_mut().position(|r| {
435                r.name_id == name_id
436                    && r.platform_id == 3
437                    && r.encoding_id == 1
438                    && r.language_id == 0x409
439            }) {
440                removed_names.insert(name_id, records[existing].string.to_string());
441                to_delete.push(existing);
442            }
443        }
444        for i in to_delete.into_iter().rev() {
445            records.remove(i);
446        }
447
448        // If STAT table was using any removed names, add then back with a new ID
449        if !removed_names.is_empty() && font.table_data(Tag::from_be_bytes(*b"STAT")).is_some() {
450            let mut stat: Stat = font.stat()?.to_owned_table();
451            for axis in stat.design_axes.iter_mut() {
452                let id = axis.axis_name_id;
453                if let Some(old_name) = removed_names.get(&id) {
454                    axis.axis_name_id = find_or_add_name(&mut records, old_name);
455                }
456            }
457            // Also do the axis value array
458            if let Some(axis_values) = stat.offset_to_axis_values.as_deref_mut() {
459                for axis_value in axis_values.iter_mut() {
460                    if let Some(name_id) = axis_value.value_name_id() {
461                        if let Some(old_name) = removed_names.get(&name_id) {
462                            axis_value.set_value_name_id(find_or_add_name(&mut records, old_name));
463                        }
464                    }
465                }
466            }
467            newfont.add_table(&stat)?;
468        }
469
470        if let Some(existing) = records.iter_mut().find(|r| {
471            r.name_id == StringId::UNIQUE_ID
472                && r.platform_id == 3
473                && r.encoding_id == 1
474                && r.language_id == 0x409
475        }) {
476            if let Some(new_unique) = new_unique_id(font, &full_name, &ps_name, &existing.string) {
477                *existing.string = new_unique;
478            }
479        }
480        if aggressive.unwrap_or_default() == RenameAggressiveness::Aggressive {
481            for record in records.iter_mut() {
482                if PROTECTED_IDS.contains(&record.name_id)
483                    || !record.string.contains(&existing_name)
484                {
485                    continue;
486                }
487                if !record.string.contains(' ') {
488                    *record.string = record
489                        .string
490                        .replace(&existing_name, family_name)
491                        .replace(" ", "");
492                } else {
493                    *record.string = record.string.replace(&existing_name, family_name);
494                }
495            }
496        }
497        name.name_record = records;
498        name.name_record.sort();
499        Ok(name)
500    }
501
502    fn new_unique_id(
503        font: &FontRef<'_>,
504        full_name: &str,
505        ps_name: &str,
506        existing: &str,
507    ) -> Option<String> {
508        let new = existing.to_string();
509        if let Some(existing_full_name) = font
510            .localized_strings(StringId::FULL_NAME)
511            .english_or_first()
512        {
513            let existing_full_name = existing_full_name.chars().collect::<String>();
514            if new.contains(&existing_full_name) {
515                return Some(new.replace(&existing_full_name, full_name));
516            }
517        }
518        if let Some(existing_ps_name) = font
519            .localized_strings(StringId::POSTSCRIPT_NAME)
520            .english_or_first()
521        {
522            let existing_ps_name = existing_ps_name.chars().collect::<String>();
523            if new.contains(&existing_ps_name) {
524                return Some(new.replace(&existing_ps_name, ps_name));
525            }
526        }
527        None
528    }
529
530    fn font_axes(font: &FontRef) -> Result<Vec<FontAxis>, ReadError> {
531        let fvar = font.fvar().unwrap();
532        let mut axes = vec![];
533        for axis in fvar.axes()? {
534            let tag = axis.axis_tag().to_string();
535            let min = axis.min_value().to_f32();
536            let max = axis.max_value().to_f32();
537            let default = axis.default_value().to_f32();
538            axes.push(FontAxis {
539                tag,
540                min,
541                max,
542                default,
543            });
544        }
545        Ok(axes)
546    }
547
548    fn vf_style_name(font: &FontRef, family_name: &str) -> Result<String, ReadError> {
549        let axisregistry = AxisRegistry::new();
550        let axes: Vec<_> = font_axes(font)?;
551        let fvar_dflts = axisregistry.name_particles(&axes);
552        let mut relevant_particles: Vec<String> = axisregistry
553            .axis_order()
554            .iter()
555            .flat_map(|tag| fvar_dflts.get(tag))
556            .filter(|particle| !particle.elided)
557            .flat_map(|particle| particle.name.clone())
558            .collect::<Vec<_>>();
559        let family_name_tokens = family_name.split_whitespace().collect::<HashSet<_>>();
560        let subfamily_name = best_subfamilyname(font);
561        let font_styles = axisregistry
562            .name_table_fallbacks(
563                family_name,
564                subfamily_name.as_deref().unwrap_or("Regular"),
565                &axes,
566            )
567            .map(|(_tag, proto)| proto.name())
568            .filter(|name| !family_name_tokens.contains(name))
569            .map(|name| name.to_string())
570            .filter(|name| !relevant_particles.contains(name))
571            .collect::<Vec<_>>();
572        relevant_particles.extend(font_styles);
573        let name = relevant_particles
574            .join(" ")
575            .replace("Regular Italic", "Italic");
576        Ok(name)
577    }
578
579    /// Return a font with an fvar table which conforms to the Google Fonts instance spec:
580    /// https://github.com/googlefonts/gf-docs/tree/main/Spec#fvar-instances
581    pub fn build_fvar_instances(
582        font: FontRef,
583        axis_dflts: Option<HashMap<String, f32>>,
584    ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
585        let axis_registry = AxisRegistry::new();
586        let mut new_font = FontBuilder::new();
587        let mut fvar: Fvar = font.fvar().unwrap().to_owned_table();
588        let mut name_table: Name = font.name().unwrap().to_owned_table();
589        let family_name = best_familyname(&font).unwrap_or("New Font".to_string());
590        let style_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
591        // Protect name IDs which are shared with the STAT table
592        let mut stat_name_ids = HashSet::new();
593        if let Ok(stat) = font.stat() {
594            for axis in stat.design_axes()?.iter() {
595                stat_name_ids.insert(axis.axis_name_id());
596            }
597            if let Some(axis_values_offset) = stat.offset_to_axis_values().transpose()? {
598                for axis_value in axis_values_offset.axis_values().iter().flatten() {
599                    stat_name_ids.insert(axis_value.value_name_id());
600                }
601            }
602        }
603
604        // Remove old fvar subfamily and ps name records
605        for instance in fvar.axis_instance_arrays.instances.iter() {
606            if instance.subfamily_name_id != StringId::SUBFAMILY_NAME
607                && instance.subfamily_name_id != StringId::TYPOGRAPHIC_SUBFAMILY_NAME
608                && !stat_name_ids.contains(&instance.subfamily_name_id)
609            {
610                name_table
611                    .name_record
612                    .retain(|record| record.name_id != instance.subfamily_name_id);
613            }
614            if let Some(psname) = instance.post_script_name_id {
615                if psname != StringId::POSTSCRIPT_NAME {
616                    name_table
617                        .name_record
618                        .retain(|record| record.name_id != psname);
619                }
620            }
621        }
622
623        let axes = font_axes(&font)?;
624        let fvar_defaults = axis_registry.name_particles(&axes);
625
626        let axis_dflts = axis_dflts.unwrap_or_else(|| {
627            fvar_defaults
628                .iter()
629                .map(|(tag, particle)| (tag.to_string(), particle.value))
630                .collect()
631        });
632
633        let is_italic = style_name.contains("Italic");
634        let is_roman_and_italic =
635            fvar_defaults.contains_key("ital") || fvar_defaults.contains_key("slnt");
636
637        let mut fallbacks: HashMap<String, Vec<FallbackProto>> =
638            axis_registry.fallbacks(&axes).collect();
639        if !fvar_defaults.contains_key("wght") {
640            fallbacks.insert(
641                "wght".to_string(),
642                axis_registry
643                    .get("wght")
644                    .unwrap()
645                    .fallback
646                    .iter()
647                    .filter(|f| f.value == Some(400.0))
648                    .take(1)
649                    .cloned()
650                    .collect(),
651            );
652        }
653        let wght_fallbacks = fallbacks.get("wght").ok_or("No wght fallbacks")?;
654        let min_ital: Option<f32> = axes
655            .iter()
656            .filter(|axis| axis.tag == "ital")
657            .map(|axis| axis.min)
658            .next();
659        let min_slnt: Option<f32> = axes
660            .iter()
661            .filter(|axis| axis.tag == "ital")
662            .map(|axis| axis.min)
663            .next();
664
665        let mut instances = vec![];
666        let do_italic = if is_roman_and_italic {
667            vec![false, true]
668        } else if is_italic {
669            vec![true]
670        } else {
671            vec![false]
672        };
673        for italic in do_italic.into_iter() {
674            for fallback in wght_fallbacks.iter() {
675                let mut name = fallback.name.as_ref().unwrap().to_string();
676                if italic {
677                    name += " Italic";
678                }
679                name = name.replace("Regular Italic", "Italic");
680                let mut coordinates = axis_dflts.clone();
681                if fvar_defaults.contains_key("wght") {
682                    coordinates.insert("wght".to_string(), fallback.value.unwrap());
683                }
684                if italic {
685                    if let Some(min) = min_ital {
686                        coordinates.insert("ital".to_string(), min);
687                    } else if let Some(min) = min_slnt {
688                        coordinates.insert("slnt".to_string(), min);
689                    }
690                }
691                let subfamily_name_id = add_name(&mut name_table.name_record, &name);
692                let post_script_name_id = add_name(
693                    &mut name_table.name_record,
694                    &format!("{}-{}", family_name, name).replace(" ", ""),
695                );
696                let coordinates = axes
697                    .iter()
698                    .map(|axis| coordinates.get(&axis.tag).cloned().unwrap_or(axis.default))
699                    .map(|val| Fixed::from_f64(val as f64))
700                    .collect();
701                instances.push(InstanceRecord {
702                    subfamily_name_id,
703                    flags: 0,
704                    coordinates,
705                    post_script_name_id: Some(post_script_name_id),
706                })
707            }
708        }
709
710        fvar.axis_instance_arrays.instances = instances;
711
712        new_font.add_table(&fvar)?;
713        name_table.name_record.sort();
714        new_font.add_table(&name_table)?;
715        Ok(new_font.copy_missing_tables(font).build())
716    }
717
718    // All right, let's do it
719    pub fn build_stat(
720        font: FontRef,
721        siblings: &[FontRef],
722    ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
723        let mut new_font = FontBuilder::new();
724        let axes = font_axes(&font)?;
725        let axis_registry = AxisRegistry::new();
726        let fallbacks_in_fvar: IndexMap<String, Vec<FallbackProto>> =
727            axis_registry.fallbacks(&axes).collect();
728
729        let mut fallbacks_in_siblings: Vec<(String, FallbackProto)> = vec![];
730        for fnt in siblings {
731            let family_name = best_familyname(fnt).unwrap_or("New Font".to_string());
732            let subfamily_name = best_subfamilyname(fnt).unwrap_or("Regular".to_string());
733            let font_axes = font_axes(fnt).unwrap_or_default();
734            fallbacks_in_siblings.extend(
735                axis_registry
736                    .name_table_fallbacks(&family_name, &subfamily_name, &font_axes)
737                    .map(|(tag, proto)| (tag.to_string(), proto.clone())),
738            )
739        }
740        // And for this font
741        let family_name = best_familyname(&font).unwrap_or("New Font".to_string());
742        let subfamily_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
743        let fallbacks_in_names =
744            axis_registry.name_table_fallbacks(&family_name, &subfamily_name, &axes);
745
746        let fvar: Fvar = font.fvar().unwrap().to_owned_table();
747        let mut name: Name = font.name().unwrap().to_owned_table();
748        let fvar_name_ids: HashSet<StringId> = fvar
749            .axis_instance_arrays
750            .instances
751            .iter()
752            .map(|x| x.subfamily_name_id)
753            .chain(
754                fvar.axis_instance_arrays
755                    .axes
756                    .iter()
757                    .map(|x| x.axis_name_id),
758            )
759            .collect();
760        let keep = |name_id: StringId| -> bool {
761            name_id.to_u16() <= 25 || fvar_name_ids.contains(&name_id)
762        };
763        let mut delete_ids = vec![];
764        if let Ok(stat) = font.stat() {
765            for axis in stat.design_axes()?.iter() {
766                let id = axis.axis_name_id.get();
767                if !keep(id) {
768                    delete_ids.push(id);
769                }
770            }
771            if let Some(axis_values) = stat.offset_to_axis_values().transpose()? {
772                for axis_value in axis_values.axis_values().iter().flatten() {
773                    let id = axis_value.value_name_id();
774                    if !keep(id) {
775                        delete_ids.push(id);
776                    }
777                }
778            }
779        }
780        name.name_record
781            .retain(|record| !delete_ids.contains(&record.name_id));
782        let mut axis_records: Vec<AxisRecord> = vec![];
783        let mut values: Vec<AxisValue> = vec![];
784        let mut seen_axes = HashSet::new();
785
786        fn make_location(axis: Tag, value: f32, linked_value: Option<f32>) -> AxisLocation {
787            if let Some(linked_value) = linked_value {
788                AxisLocation::Three {
789                    tag: axis,
790                    value: Fixed::from_f64(value as f64),
791                    linked: Fixed::from_f64(linked_value as f64),
792                }
793            } else {
794                AxisLocation::One {
795                    tag: axis,
796                    value: Fixed::from_f64(value as f64),
797                }
798            }
799        }
800
801        for (axis, fallbacks) in fallbacks_in_fvar.iter() {
802            let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
803            let ar_axis = axis_registry.get(axis).unwrap();
804            seen_axes.insert(tag);
805            axis_records.push(AxisRecord {
806                tag,
807                name: ar_axis.display_name().to_string(),
808                ordering: 0,
809            });
810            let fallback_values = fallbacks.iter().map(|f| f.value()).collect::<Vec<f32>>();
811            for fallback in fallbacks.iter() {
812                values.push(AxisValue {
813                    flags: if fallback.value() == ar_axis.default_value() {
814                        0x2
815                    } else {
816                        0x0
817                    },
818                    name: fallback.name().to_string(),
819                    location: make_location(
820                        tag,
821                        fallback.value(),
822                        linked_value(axis, fallback.value())
823                            .filter(|value| fallback_values.contains(value)),
824                    ),
825                })
826            }
827        }
828
829        for (axis, fallback) in fallbacks_in_names {
830            let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
831            if seen_axes.contains(&tag) {
832                continue;
833            }
834            // println!("Adding {} in names", axis);
835            seen_axes.insert(tag);
836
837            let ar_axis = axis_registry.get(axis).unwrap();
838            axis_records.push(AxisRecord {
839                tag,
840                name: ar_axis.display_name().to_string(),
841                ordering: 0,
842            });
843            values.push(AxisValue {
844                flags: 0x0,
845                name: fallback.name().to_string(),
846                location: make_location(
847                    tag,
848                    fallback.value(),
849                    linked_value(axis, fallback.value()),
850                ),
851            });
852        }
853
854        for (axis, _fallback) in fallbacks_in_siblings {
855            let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
856            if seen_axes.contains(&tag) {
857                continue;
858            }
859            seen_axes.insert(tag);
860
861            // println!("Adding {} in siblings", axis);
862            let ar_axis = axis_registry.get(&axis).unwrap();
863            let elided_value = ar_axis.default_value();
864            axis_records.push(AxisRecord {
865                tag,
866                name: ar_axis.display_name().to_string(),
867                ordering: 0,
868            });
869            if let Some(elided_fallback) = axis_registry.fallback_for_value(&axis, elided_value) {
870                values.push(AxisValue {
871                    flags: 0x2,
872                    name: elided_fallback.name().to_string(),
873                    location: make_location(tag, elided_value, linked_value(&axis, elided_value)),
874                })
875            }
876        }
877        axis_records.iter_mut().enumerate().for_each(|(i, record)| {
878            record.ordering = i as u16;
879        });
880
881        let stat_builder = StatBuilder {
882            records: axis_records,
883            values,
884        };
885        let stat = stat_builder.build(&mut name.name_record);
886        name.name_record.sort();
887        new_font.add_table(&name)?;
888        new_font.add_table(&stat)?;
889        Ok(new_font.copy_missing_tables(font).build())
890    }
891
892    pub fn build_filename(font: FontRef, extension: &str) -> String {
893        let family_name = best_familyname(&font)
894            .unwrap_or("New Font".to_string())
895            .replace(" ", "");
896        let style_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
897        if font.table_data(Tag::new(b"fvar")).is_some() {
898            let is_italic = style_name.contains("Italic");
899            let axes = font_axes(&font).unwrap_or_default();
900            // Sort uppercase axes first, then lowercase axes
901            let mut axes = axes
902                .iter()
903                .map(|axis| axis.tag.to_string())
904                .collect::<Vec<_>>();
905            axes.sort();
906            let axes = axes.join(",");
907            return format!(
908                "{}{}[{}].{}",
909                family_name,
910                if is_italic { "-Italic" } else { "" },
911                axes,
912                extension
913            );
914        }
915        format!("{family_name}-{style_name}.{extension}").replace(" ", "")
916    }
917}
918
919#[cfg(feature = "fontations")]
920pub use fontations_impl::*;
921
922#[cfg(test)]
923mod tests {
924    use fontations::{
925        skrifa::{string::StringId, MetadataProvider, Tag},
926        write::{
927            from_obj::ToOwnedTable,
928            tables::{
929                name::Name,
930                stat::{AxisValue, Stat},
931            },
932        },
933    };
934    use pretty_assertions::assert_eq;
935
936    use super::*;
937
938    #[test]
939    fn opsz() {
940        let ar = AxisRegistry::new();
941        assert!(ar.contains_key("opsz"));
942        assert_eq!(ar["opsz"].display_name.as_deref(), Some("Optical Size"));
943    }
944    const MAVEN_PRO: &[u8; 83576] = include_bytes!("../tests/data/MavenPro-Regular.ttf");
945    const OPEN_SANS: &[u8; 532636] = include_bytes!("../tests/data/OpenSans[wdth,wght].ttf");
946    const OPEN_SANS_ITALIC: &[u8; 584112] =
947        include_bytes!("../tests/data/OpenSans-Italic[wdth,wght].ttf");
948    const OPEN_SANS_CONDENSED: &[u8; 973780] =
949        include_bytes!("../tests/data/OpenSansCondensed[wght].ttf");
950    const OPEN_SANS_CONDENSED_ITALIC: &[u8; 1054652] =
951        include_bytes!("../tests/data/OpenSansCondensed-Italic[wght].ttf");
952    const WONKY: &[u8; 532564] = include_bytes!("../tests/data/Wonky[wdth,wght].ttf");
953    const PLAYFAIR: &[u8; 1150824] = include_bytes!("../tests/data/Playfair[opsz,wdth,wght].ttf");
954
955    struct NameTableTestCase {
956        binary: &'static [u8],
957        family_name: &'static str,
958        subfamily_name: Option<&'static str>,
959        siblings: Vec<&'static [u8]>,
960        expectations: Vec<(StringId, Option<&'static str>)>,
961    }
962
963    #[test]
964    fn test_name_table() {
965        let cases: Vec<NameTableTestCase> = vec![
966            NameTableTestCase {
967                binary: MAVEN_PRO,
968                family_name: "Maven Pro",
969                subfamily_name: Some("Regular"),
970                siblings: vec![],
971                expectations: vec![
972                    (StringId::FAMILY_NAME, Some("Maven Pro")),
973                    (StringId::SUBFAMILY_NAME, Some("Regular")),
974                    (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Regular")),
975                    (StringId::FULL_NAME, Some("Maven Pro Regular")),
976                    (StringId::POSTSCRIPT_NAME, Some("MavenPro-Regular")),
977                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
978                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
979                ],
980            },
981            NameTableTestCase {
982                binary: MAVEN_PRO,
983                family_name: "Maven Pro",
984                subfamily_name: Some("Italic"),
985                siblings: vec![],
986                expectations: vec![
987                    (StringId::FAMILY_NAME, Some("Maven Pro")),
988                    (StringId::SUBFAMILY_NAME, Some("Italic")),
989                    (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Italic")),
990                    (StringId::FULL_NAME, Some("Maven Pro Italic")),
991                    (StringId::POSTSCRIPT_NAME, Some("MavenPro-Italic")),
992                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
993                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
994                ],
995            },
996            NameTableTestCase {
997                binary: MAVEN_PRO,
998                family_name: "Maven Pro",
999                subfamily_name: Some("Bold"),
1000                siblings: vec![],
1001                expectations: vec![
1002                    (StringId::FAMILY_NAME, Some("Maven Pro")),
1003                    (StringId::SUBFAMILY_NAME, Some("Bold")),
1004                    (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Bold")),
1005                    (StringId::FULL_NAME, Some("Maven Pro Bold")),
1006                    (StringId::POSTSCRIPT_NAME, Some("MavenPro-Bold")),
1007                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1008                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1009                ],
1010            },
1011            NameTableTestCase {
1012                binary: MAVEN_PRO,
1013                family_name: "Maven Pro",
1014                subfamily_name: Some("Bold Italic"),
1015                siblings: vec![],
1016                expectations: vec![
1017                    (StringId::FAMILY_NAME, Some("Maven Pro")),
1018                    (StringId::SUBFAMILY_NAME, Some("Bold Italic")),
1019                    (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-BoldItalic")),
1020                    (StringId::FULL_NAME, Some("Maven Pro Bold Italic")),
1021                    (StringId::POSTSCRIPT_NAME, Some("MavenPro-BoldItalic")),
1022                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1023                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1024                ],
1025            },
1026            NameTableTestCase {
1027                binary: MAVEN_PRO,
1028                family_name: "Maven Pro",
1029                subfamily_name: Some("Black"),
1030                siblings: vec![],
1031                expectations: vec![
1032                    (StringId::FAMILY_NAME, Some("Maven Pro Black")),
1033                    (StringId::SUBFAMILY_NAME, Some("Regular")),
1034                    (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Black")),
1035                    (StringId::FULL_NAME, Some("Maven Pro Black")),
1036                    (StringId::POSTSCRIPT_NAME, Some("MavenPro-Black")),
1037                    (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
1038                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, Some("Black")),
1039                ],
1040            },
1041            NameTableTestCase {
1042                binary: MAVEN_PRO,
1043                family_name: "Maven Pro",
1044                subfamily_name: Some("Black Italic"),
1045                siblings: vec![],
1046                expectations: vec![
1047                    (StringId::FAMILY_NAME, Some("Maven Pro Black")),
1048                    (StringId::SUBFAMILY_NAME, Some("Italic")),
1049                    (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-BlackItalic")),
1050                    (StringId::FULL_NAME, Some("Maven Pro Black Italic")),
1051                    (StringId::POSTSCRIPT_NAME, Some("MavenPro-BlackItalic")),
1052                    (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
1053                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, Some("Black Italic")),
1054                ],
1055            },
1056            NameTableTestCase {
1057                binary: MAVEN_PRO,
1058                family_name: "Maven Pro",
1059                subfamily_name: Some("ExtraLight Italic"),
1060                siblings: vec![],
1061                expectations: vec![
1062                    (StringId::FAMILY_NAME, Some("Maven Pro ExtraLight")),
1063                    (StringId::SUBFAMILY_NAME, Some("Italic")),
1064                    (
1065                        StringId::UNIQUE_ID,
1066                        Some("2.003;NONE;MavenPro-ExtraLightItalic"),
1067                    ),
1068                    (StringId::FULL_NAME, Some("Maven Pro ExtraLight Italic")),
1069                    (StringId::POSTSCRIPT_NAME, Some("MavenPro-ExtraLightItalic")),
1070                    (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
1071                    (
1072                        StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
1073                        Some("ExtraLight Italic"),
1074                    ),
1075                ],
1076            },
1077            NameTableTestCase {
1078                binary: MAVEN_PRO,
1079                family_name: "Maven Pro",
1080                subfamily_name: Some("UltraExpanded Regular"),
1081                siblings: vec![],
1082                expectations: vec![
1083                    (StringId::FAMILY_NAME, Some("Maven Pro UltraExpanded")),
1084                    (StringId::SUBFAMILY_NAME, Some("Regular")),
1085                    (
1086                        StringId::UNIQUE_ID,
1087                        Some("2.003;NONE;MavenProUltraExpanded-Regular"),
1088                    ),
1089                    (StringId::FULL_NAME, Some("Maven Pro UltraExpanded Regular")),
1090                    (
1091                        StringId::POSTSCRIPT_NAME,
1092                        Some("MavenProUltraExpanded-Regular"),
1093                    ),
1094                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1095                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1096                ],
1097            },
1098            NameTableTestCase {
1099                binary: MAVEN_PRO,
1100                family_name: "Maven Pro",
1101                subfamily_name: Some("Condensed ExtraLight Italic"),
1102                siblings: vec![],
1103                expectations: vec![
1104                    (
1105                        StringId::FAMILY_NAME,
1106                        Some("Maven Pro Condensed ExtraLight"),
1107                    ),
1108                    (StringId::SUBFAMILY_NAME, Some("Italic")),
1109                    (
1110                        StringId::UNIQUE_ID,
1111                        Some("2.003;NONE;MavenProCondensed-ExtraLightItalic"),
1112                    ),
1113                    (
1114                        StringId::FULL_NAME,
1115                        Some("Maven Pro Condensed ExtraLight Italic"),
1116                    ),
1117                    (
1118                        StringId::POSTSCRIPT_NAME,
1119                        Some("MavenProCondensed-ExtraLightItalic"),
1120                    ),
1121                    (
1122                        StringId::TYPOGRAPHIC_FAMILY_NAME,
1123                        Some("Maven Pro Condensed"),
1124                    ),
1125                    (
1126                        StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
1127                        Some("ExtraLight Italic"),
1128                    ),
1129                ],
1130            },
1131            NameTableTestCase {
1132                binary: OPEN_SANS,
1133                family_name: "Open Sans",
1134                subfamily_name: None,
1135                siblings: vec![
1136                    OPEN_SANS_ITALIC,
1137                    OPEN_SANS_CONDENSED,
1138                    OPEN_SANS_CONDENSED_ITALIC,
1139                ],
1140                expectations: vec![
1141                    (StringId::FAMILY_NAME, Some("Open Sans")),
1142                    (StringId::SUBFAMILY_NAME, Some("Regular")),
1143                    (StringId::UNIQUE_ID, Some("3.000;GOOG;OpenSans-Regular")),
1144                    (StringId::FULL_NAME, Some("Open Sans Regular")),
1145                    (StringId::POSTSCRIPT_NAME, Some("OpenSans-Regular")),
1146                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1147                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1148                    (
1149                        StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1150                        Some("OpenSans"),
1151                    ),
1152                ],
1153            },
1154            NameTableTestCase {
1155                binary: OPEN_SANS_ITALIC,
1156                family_name: "Open Sans",
1157                subfamily_name: None,
1158                siblings: vec![OPEN_SANS, OPEN_SANS_CONDENSED, OPEN_SANS_CONDENSED_ITALIC],
1159                expectations: vec![
1160                    (StringId::FAMILY_NAME, Some("Open Sans")),
1161                    (StringId::SUBFAMILY_NAME, Some("Italic")),
1162                    (StringId::UNIQUE_ID, Some("3.000;GOOG;OpenSans-Italic")),
1163                    (StringId::FULL_NAME, Some("Open Sans Italic")),
1164                    (StringId::POSTSCRIPT_NAME, Some("OpenSans-Italic")),
1165                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1166                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1167                    (
1168                        StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1169                        Some("OpenSansItalic"),
1170                    ),
1171                ],
1172            },
1173            NameTableTestCase {
1174                binary: OPEN_SANS_CONDENSED,
1175                family_name: "Open Sans Condensed",
1176                subfamily_name: None,
1177                siblings: vec![OPEN_SANS, OPEN_SANS_ITALIC, OPEN_SANS_CONDENSED_ITALIC],
1178                expectations: vec![
1179                    (StringId::FAMILY_NAME, Some("Open Sans Condensed")),
1180                    (StringId::SUBFAMILY_NAME, Some("Regular")),
1181                    (
1182                        StringId::UNIQUE_ID,
1183                        Some("3.000;GOOG;OpenSansCondensed-Regular"),
1184                    ),
1185                    (StringId::FULL_NAME, Some("Open Sans Condensed Regular")),
1186                    (StringId::POSTSCRIPT_NAME, Some("OpenSansCondensed-Regular")),
1187                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1188                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1189                    (
1190                        StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1191                        Some("OpenSansCondensed"),
1192                    ),
1193                ],
1194            },
1195            NameTableTestCase {
1196                binary: OPEN_SANS_CONDENSED_ITALIC,
1197                family_name: "Open Sans Condensed",
1198                subfamily_name: None,
1199                siblings: vec![OPEN_SANS, OPEN_SANS_ITALIC, OPEN_SANS_CONDENSED],
1200                expectations: vec![
1201                    (StringId::FAMILY_NAME, Some("Open Sans Condensed")),
1202                    (StringId::SUBFAMILY_NAME, Some("Italic")),
1203                    (
1204                        StringId::UNIQUE_ID,
1205                        Some("3.000;GOOG;OpenSansCondensed-Italic"),
1206                    ),
1207                    (StringId::FULL_NAME, Some("Open Sans Condensed Italic")),
1208                    (StringId::POSTSCRIPT_NAME, Some("OpenSansCondensed-Italic")),
1209                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1210                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1211                    (
1212                        StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1213                        Some("OpenSansCondensedItalic"),
1214                    ),
1215                ],
1216            },
1217            // Bad names
1218            NameTableTestCase {
1219                binary: MAVEN_PRO,
1220                family_name: "Maven Pro",
1221                subfamily_name: Some("Fat"),
1222                siblings: vec![],
1223                expectations: vec![
1224                    (StringId::FAMILY_NAME, Some("Maven Pro Fat")),
1225                    (StringId::SUBFAMILY_NAME, Some("Regular")),
1226                    (StringId::UNIQUE_ID, Some("2.003;NONE;MavenProFat-Regular")),
1227                    (StringId::FULL_NAME, Some("Maven Pro Fat Regular")),
1228                    (StringId::POSTSCRIPT_NAME, Some("MavenProFat-Regular")),
1229                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1230                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1231                ],
1232            },
1233            NameTableTestCase {
1234                binary: WONKY,
1235                family_name: "Wonky",
1236                subfamily_name: None,
1237                siblings: vec![],
1238                expectations: vec![
1239                    (StringId::FAMILY_NAME, Some("Wonky")),
1240                    (StringId::SUBFAMILY_NAME, Some("Regular")),
1241                    (StringId::UNIQUE_ID, Some("3.000;GOOG;Wonky-Regular")),
1242                    (StringId::FULL_NAME, Some("Wonky Regular")),
1243                    (StringId::POSTSCRIPT_NAME, Some("Wonky-Regular")),
1244                    (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1245                    (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1246                ],
1247            },
1248            NameTableTestCase {
1249                binary: PLAYFAIR,
1250                family_name: "Playfair",
1251                subfamily_name: None,
1252                siblings: vec![],
1253                expectations: vec![
1254                    (StringId::FAMILY_NAME, Some("Playfair SemiExpanded Light")),
1255                    (StringId::SUBFAMILY_NAME, Some("Regular")),
1256                    (
1257                        StringId::UNIQUE_ID,
1258                        Some("2.000;FTH;Playfair-SemiExpandedLight"),
1259                    ),
1260                    (StringId::FULL_NAME, Some("Playfair SemiExpanded Light")),
1261                    (
1262                        StringId::POSTSCRIPT_NAME,
1263                        Some("Playfair-SemiExpandedLight"),
1264                    ),
1265                    (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Playfair")),
1266                    (
1267                        StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
1268                        Some("SemiExpanded Light"),
1269                    ),
1270                ],
1271            },
1272        ];
1273
1274        run_name_table_tests(&cases, None);
1275    }
1276
1277    fn run_name_table_tests(cases: &[NameTableTestCase], aggression: Option<RenameAggressiveness>) {
1278        for (ix, case) in cases.iter().enumerate() {
1279            let font = FontRef::new(case.binary).expect("Failed to read font");
1280            let siblings: Vec<FontRef> = case
1281                .siblings
1282                .iter()
1283                .map(|b| FontRef::new(b).unwrap())
1284                .collect();
1285            let result = build_name_table(
1286                font,
1287                Some(case.family_name),
1288                case.subfamily_name,
1289                &siblings,
1290                aggression,
1291            )
1292            .unwrap();
1293            let result_font = FontRef::new(&result).unwrap();
1294            for (id, expectation) in case.expectations.iter() {
1295                let record = result_font.localized_strings(*id).english_or_first();
1296                if let Some(expectation) = expectation {
1297                    assert_eq!(
1298                        record.unwrap().chars().collect::<String>(),
1299                        *expectation,
1300                        "Case {}, {}",
1301                        ix + 1,
1302                        id
1303                    );
1304                } else {
1305                    assert!(record.is_none());
1306                }
1307            }
1308        }
1309    }
1310
1311    #[test]
1312    fn test_name_table_aggression() {
1313        let cases = [
1314            NameTableTestCase {
1315                binary: MAVEN_PRO,
1316                family_name: "Raven Am",
1317                subfamily_name: Some("Regular"),
1318                siblings: vec![],
1319                expectations: vec![
1320                    (StringId::COPYRIGHT_NOTICE, Some("Copyright 2011 The Raven Am Project Authors (http://www.vissol.co.uk/mavenpro/), with Reserved Font Name \"Raven Am\".")),
1321                    (StringId::FULL_NAME, Some("Raven Am Regular")),
1322                ]
1323            }
1324        ];
1325        run_name_table_tests(&cases, Some(RenameAggressiveness::Aggressive));
1326        let cases = [
1327            NameTableTestCase {
1328                binary: MAVEN_PRO,
1329                family_name: "Raven Am",
1330                subfamily_name: Some("Regular"),
1331                siblings: vec![],
1332                expectations: vec![
1333                    (StringId::COPYRIGHT_NOTICE, Some("Copyright 2011 The Maven Pro Project Authors (http://www.vissol.co.uk/mavenpro/), with Reserved Font Name \"Maven Pro\".")),
1334                    (StringId::FULL_NAME, Some("Raven Am Regular")),
1335                ]
1336            }
1337        ];
1338        run_name_table_tests(&cases, Some(RenameAggressiveness::Conservative));
1339    }
1340
1341    #[derive(Debug, Clone, PartialEq)]
1342    struct DumpStatValue<'a> {
1343        axis: Tag,
1344        name: &'a str,
1345        value: f32,
1346        linked: Option<f32>,
1347    }
1348
1349    fn dump_stat_values<'a>(stat: &Stat, name: &'a Name) -> Vec<DumpStatValue<'a>> {
1350        let mut values = vec![];
1351        let tags = stat
1352            .design_axes
1353            .iter()
1354            .map(|a| a.axis_tag)
1355            .collect::<Vec<_>>();
1356        if let Some(axis_values) = stat.offset_to_axis_values.as_ref() {
1357            for axis_value in axis_values.iter() {
1358                match axis_value.as_ref() {
1359                    AxisValue::Format1(v) => values.push(DumpStatValue {
1360                        axis: tags[v.axis_index as usize],
1361                        name: name
1362                            .name_record
1363                            .iter()
1364                            .find(|record| record.name_id == v.value_name_id)
1365                            .map(|record| record.string.as_str())
1366                            .unwrap_or(""),
1367                        value: v.value.to_f32(),
1368                        linked: None,
1369                    }),
1370                    AxisValue::Format2(_) => {
1371                        panic!("I didn't produce this")
1372                    }
1373                    AxisValue::Format3(v) => values.push(DumpStatValue {
1374                        axis: tags[v.axis_index as usize],
1375                        name: name
1376                            .name_record
1377                            .iter()
1378                            .find(|record| record.name_id == v.value_name_id)
1379                            .map(|record| record.string.as_str())
1380                            .unwrap_or(""),
1381                        value: v.value.to_f32(),
1382                        linked: Some(v.linked_value.to_f32()),
1383                    }),
1384                    AxisValue::Format4(_) => {
1385                        panic!("I didn't produce this")
1386                    }
1387                }
1388            }
1389        }
1390        values
1391    }
1392
1393    fn value<'a>(axis: &str, name: &'a str, value: f32, linked: Option<f32>) -> DumpStatValue<'a> {
1394        DumpStatValue {
1395            axis: Tag::new_checked(axis.as_bytes()).unwrap(),
1396            name,
1397            value,
1398            linked,
1399        }
1400    }
1401
1402    #[test]
1403    fn test_build_stat() {
1404        let font_data = build_stat(
1405            FontRef::new(OPEN_SANS).unwrap(),
1406            &[
1407                FontRef::new(OPEN_SANS_ITALIC).unwrap(),
1408                FontRef::new(OPEN_SANS_CONDENSED).unwrap(),
1409                FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap(),
1410            ],
1411        )
1412        .unwrap();
1413        let new_font = FontRef::new(&font_data).unwrap();
1414        let new_stat: Stat = new_font.stat().unwrap().to_owned_table();
1415        let name: Name = new_font.name().unwrap().to_owned_table();
1416        // We expect three axes, wght, wdth, and ital
1417        assert_eq!(
1418            new_stat
1419                .design_axes
1420                .iter()
1421                .map(|a| a.axis_tag)
1422                .collect::<Vec<_>>(),
1423            vec![Tag::new(b"wght"), Tag::new(b"wdth"), Tag::new(b"ital")]
1424        );
1425        assert_eq!(
1426            dump_stat_values(&new_stat, &name),
1427            vec![
1428                value("wght", "Light", 300.0, None),
1429                value("wght", "Regular", 400.0, Some(700.0)),
1430                value("wght", "Medium", 500.0, None),
1431                value("wght", "SemiBold", 600.0, None),
1432                value("wght", "Bold", 700.0, None),
1433                value("wght", "ExtraBold", 800.0, None),
1434                value("wdth", "Condensed", 75.0, None),
1435                value("wdth", "SemiCondensed", 87.5, None),
1436                value("wdth", "Normal", 100.0, None),
1437                value("ital", "Roman", 0.0, Some(1.0)),
1438            ]
1439        )
1440    }
1441
1442    #[test]
1443    fn test_build_stat2() {
1444        let font_data = build_stat(
1445            FontRef::new(OPEN_SANS_ITALIC).unwrap(),
1446            &[
1447                FontRef::new(OPEN_SANS).unwrap(),
1448                FontRef::new(OPEN_SANS_CONDENSED).unwrap(),
1449                FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap(),
1450            ],
1451        )
1452        .unwrap();
1453        let new_font = FontRef::new(&font_data).unwrap();
1454        let new_stat: Stat = new_font.stat().unwrap().to_owned_table();
1455        let name: Name = new_font.name().unwrap().to_owned_table();
1456        // We expect three axes, wght, wdth, and ital
1457        assert_eq!(
1458            new_stat
1459                .design_axes
1460                .iter()
1461                .map(|a| a.axis_tag)
1462                .collect::<Vec<_>>(),
1463            vec![Tag::new(b"wght"), Tag::new(b"wdth"), Tag::new(b"ital")]
1464        );
1465        assert_eq!(
1466            dump_stat_values(&new_stat, &name),
1467            vec![
1468                value("wght", "Light", 300.0, None),
1469                value("wght", "Regular", 400.0, Some(700.0)),
1470                value("wght", "Medium", 500.0, None),
1471                value("wght", "SemiBold", 600.0, None),
1472                value("wght", "Bold", 700.0, None),
1473                value("wght", "ExtraBold", 800.0, None),
1474                value("wdth", "Condensed", 75.0, None),
1475                value("wdth", "SemiCondensed", 87.5, None),
1476                value("wdth", "Normal", 100.0, None),
1477                value("ital", "Italic", 1.0, None),
1478            ]
1479        )
1480    }
1481
1482    #[test]
1483    fn test_build_filename() {
1484        let maven_pro = FontRef::new(MAVEN_PRO).unwrap();
1485        assert_eq!(build_filename(maven_pro, "ttf"), "MavenPro-Regular.ttf");
1486        let open_sans = FontRef::new(OPEN_SANS).unwrap();
1487        assert_eq!(build_filename(open_sans, "ttf"), "OpenSans[wdth,wght].ttf");
1488        let open_sans_italic = FontRef::new(OPEN_SANS_ITALIC).unwrap();
1489        assert_eq!(
1490            build_filename(open_sans_italic, "ttf"),
1491            "OpenSans-Italic[wdth,wght].ttf"
1492        );
1493        let open_sans_condensed = FontRef::new(OPEN_SANS_CONDENSED).unwrap();
1494        assert_eq!(
1495            build_filename(open_sans_condensed, "ttf"),
1496            "OpenSansCondensed[wght].ttf"
1497        );
1498        let open_sans_condensed_italic = FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap();
1499        assert_eq!(
1500            build_filename(open_sans_condensed_italic, "ttf"),
1501            "OpenSansCondensed-Italic[wght].ttf"
1502        );
1503        let wonky = FontRef::new(WONKY).unwrap();
1504        assert_eq!(build_filename(wonky, "ttf"), "Wonky[wdth,wght].ttf");
1505        let playfair = FontRef::new(PLAYFAIR).unwrap();
1506        assert_eq!(
1507            build_filename(playfair, "ttf"),
1508            "Playfair[opsz,wdth,wght].ttf"
1509        );
1510    }
1511}