Skip to main content

sr_aef/
lib.rs

1//! # sr-aef
2//!
3//! `sr-aef` "uncompiles" an OpenType/TrueType font back into Adobe Feature File
4//! (`fea`) source code. It reads the OpenType layout tables (GSUB, GPOS and,
5//! optionally, GDEF) out of a compiled binary font and reconstructs an equivalent
6//! feature file using the [`fea_rs_ast`] AST.
7//!
8//! Because binary fonts lose the original structure of their layout code (lookup
9//! splitting, class naming, comments, ...), the output is a faithful *functional*
10//! reconstruction rather than a byte-for-byte copy of the original source.
11//!
12//! The main entry points are [`uncompile`], [`uncompile_bytes`] and
13//! [`uncompile_context`]:
14//!
15//! ```no_run
16//! use sr_aef::{fea_rs_ast::AsFea, uncompile_bytes};
17//!
18//! fn main() -> Result<(), Box<dyn std::error::Error>> {
19//!     let data = std::fs::read("MyFont.ttf")?;
20//!     // `true` also uncompiles the GDEF table.
21//!     let feature_file = uncompile_bytes(&data, true)?;
22//!     println!("{}", feature_file.as_fea(""));
23//!     Ok(())
24//! }
25//! ```
26//!
27//! The crate also powers the TrueType round-trip in
28//! [babelfont](https://crates.io/crates/babelfont).
29use std::collections::{HashMap, HashSet};
30
31/// A handle to the version of fea-rs-ast that sr-aef is using.
32///
33/// The return value of uncompile() will be a [fea_rs_ast::FeatureFile]; you will probably want to call `.as_fea()` on it.
34pub use fea_rs_ast;
35use fea_rs_ast::ordered_float::OrderedFloat;
36use fea_rs_ast::{
37    Anchor, AsFea, AttachStatement, CharacterStatement, Comment, ConditionSet, GdefStatement,
38    GlyphClass, GlyphClassDefStatement, GlyphClassDefinition, GlyphContainer, GlyphName,
39    LanguageStatement, LanguageSystemStatement, LigatureCaretByIndexStatement,
40    LigatureCaretByPosStatement, LookupBlock, LookupFlagStatement, LookupReferenceStatement,
41    MarkClass, MarkClassDefinition, Metric, NameRecord, NameRecordKind, NestedBlock, Pos,
42    ScriptStatement, SizeParameters, Statement, SubOrPos, Subst, SubtableStatement, Table,
43    ToplevelItem, VariationBlock,
44};
45use indexmap::{IndexMap, IndexSet};
46/// A handle to the version of Skrifa that sr-eaf is using. Pass a skrifa::FontRef to uncompile()
47pub use skrifa;
48use skrifa::{
49    GlyphId, GlyphId16, GlyphNames, Tag,
50    metrics::GlyphMetrics,
51    prelude::{LocationRef, Size},
52    raw::{
53        ReadError, TableProvider,
54        tables::{
55            gdef::{CaretValue, Gdef},
56            gpos::Gpos,
57            gsub::{ClassDef, Gsub},
58            layout::{
59                CharacterVariantParams, Condition, CoverageTable, FeatureList, FeatureParams,
60                FeatureVariations, LookupFlag, ScriptList, SizeParams,
61            },
62            name::{Name, NameId},
63        },
64    },
65};
66use smol_str::SmolStr;
67
68mod contextual;
69mod gpos;
70mod gsub;
71#[cfg(feature = "cli")]
72mod serialize;
73mod variations;
74
75pub(crate) type SimpleUserLocation = IndexMap<SmolStr, OrderedFloat<f64>>; // as used by fea-rs-ast metrics
76type LanguageSystem = (Tag, Tag); // script, language
77type FeatureLanguageSystems = Vec<(LanguageSystem, bool)>; // and whether the feature is required there
78
79const PROMOTE_TO_NAMED_CLASS_THRESHOLD: usize = 5;
80
81/// The context object that holds all the information we need when uncompiling a font.
82#[cfg_attr(feature = "cli", derive(serde::Serialize))]
83pub struct UncompileContext<'a> {
84    /// All the lookups we have uncompiled so far, keyed by their name.
85    pub lookups: IndexMap<SmolStr, LookupBlock>,
86    /// A mapping from ("sub"|"pos", lookup_list_index) to the name of the lookup block we created for it. This is used to generate the correct lookup names in contextual lookups.
87    #[cfg_attr(
88        feature = "cli",
89        serde(serialize_with = "crate::serialize::serialize_lookup_map")
90    )]
91    pub lookup_map: HashMap<(String, u16), SmolStr>,
92    /// A mapping from script tags to the language system tags that are present in the font.
93    #[cfg_attr(
94        feature = "cli",
95        serde(serialize_with = "crate::serialize::serialize_language_systems")
96    )]
97    pub language_systems: IndexMap<Tag, IndexSet<Tag>>,
98    /// Anchors on a glyph which we haven't worked out what they should be called.
99    /// class -> glyphname -> anchor
100    pub unnamed_anchors: IndexMap<SmolStr, Vec<Anchor>>,
101    /// Anchors on a glyph which we have worked out what they should be called.
102    pub anchors: IndexMap<SmolStr, IndexMap<SmolStr, Anchor>>,
103    /// Mark classes, indexed by class name.
104    pub mark_classes: IndexMap<SmolStr, Vec<MarkClassDefinition>>,
105    /// Named glyph classes, indexed by class name.
106    #[cfg_attr(
107        feature = "cli",
108        serde(serialize_with = "crate::serialize::serialize_named_classes")
109    )]
110    pub named_classes: IndexMap<SmolStr, GlyphClass>,
111    /// Features, indexed by feature name.
112    #[cfg_attr(
113        feature = "cli",
114        serde(serialize_with = "crate::serialize::serialize_features")
115    )]
116    pub features: IndexMap<SmolStr, Vec<Statement>>,
117    /// Condition sets and their feature variation blocks.
118    pub feature_variations: Vec<(ConditionSet, Vec<VariationBlock>)>,
119    #[cfg_attr(feature = "cli", serde(skip))]
120    symbols: IndexMap<SmolStr, usize>,
121    #[cfg_attr(feature = "cli", serde(skip))]
122    gpos: Option<Gpos<'a>>,
123    #[cfg_attr(feature = "cli", serde(skip))]
124    gsub: Option<Gsub<'a>>,
125    #[cfg_attr(feature = "cli", serde(skip))]
126    gdef: Option<Gdef<'a>>,
127    #[cfg_attr(feature = "cli", serde(skip))]
128    name: Option<Name<'a>>,
129    #[cfg_attr(feature = "cli", serde(skip))]
130    glyph_metrics: GlyphMetrics<'a>,
131    #[cfg_attr(feature = "cli", serde(skip))]
132    glyph_id_to_name: HashMap<GlyphId, SmolStr>,
133    #[cfg_attr(feature = "cli", serde(skip))]
134    glyph_name_to_id: HashMap<SmolStr, GlyphId>,
135    #[cfg_attr(feature = "cli", serde(skip))]
136    axis_tags: Vec<Tag>,
137    #[cfg_attr(feature = "cli", serde(skip))]
138    axes: Option<fontdrasil::types::Axes>,
139    num_glyphs: u16,
140    /// Lookups whose rules were written into aalt
141    aalt_lookups: IndexSet<SmolStr>,
142}
143
144impl<'a> UncompileContext<'a> {
145    fn new(font: &'a skrifa::FontRef) -> Result<Self, ReadError> {
146        let glyph_names = GlyphNames::new(font);
147        let default = LocationRef::default();
148        let glyph_metrics = GlyphMetrics::new(font, Size::unscaled(), default);
149        let glyph_id_to_name: HashMap<GlyphId, SmolStr> = unique_glyph_names(font)
150            .into_iter()
151            .enumerate()
152            .map(|(gid, name)| (GlyphId::new(gid as u32), name))
153            .collect();
154        let glyph_name_to_id = glyph_id_to_name
155            .iter()
156            .map(|(gid, name): (&GlyphId, &SmolStr)| (name.clone(), *gid))
157            .collect();
158        let mut slf = Self {
159            lookups: IndexMap::new(),
160            lookup_map: HashMap::new(),
161            symbols: IndexMap::new(),
162            gpos: font.gpos().ok(),
163            gsub: font.gsub().ok(),
164            gdef: font.gdef().ok(),
165            name: font.name().ok(),
166            language_systems: IndexMap::new(),
167            unnamed_anchors: IndexMap::new(),
168            anchors: IndexMap::new(),
169            mark_classes: IndexMap::new(),
170            named_classes: IndexMap::new(),
171            features: IndexMap::new(),
172            feature_variations: vec![],
173            glyph_metrics,
174            glyph_id_to_name,
175            glyph_name_to_id,
176            axis_tags: font
177                .fvar()
178                .ok()
179                .and_then(|fvar| fvar.axes().ok())
180                .map(|axes| axes.iter().map(|axis| axis.axis_tag()).collect())
181                .unwrap_or_default(),
182            axes: variations::fontdrasil_axes(font)?,
183            num_glyphs: glyph_names.num_glyphs() as u16,
184            aalt_lookups: IndexSet::new(),
185        };
186        slf.gather_language_systems()?;
187        slf.uncompile_gsub_lookups()?;
188        slf.uncompile_gpos_lookups()?;
189        slf.uncompile_feature_table()?;
190        slf.uncompile_feature_variations()?;
191        slf.order_lookups();
192        Ok(slf)
193    }
194
195    fn register_anchor(&mut self, glyphname: &SmolStr, anchor: &Anchor, anchor_name: Option<&str>) {
196        if let Some(anchor_name) = anchor_name {
197            self.anchors
198                .entry(anchor_name.to_string().into())
199                .or_default()
200                .insert(glyphname.clone(), anchor.clone());
201        } else {
202            self.unnamed_anchors
203                .entry(glyphname.clone())
204                .or_default()
205                .push(anchor.clone());
206        }
207    }
208
209    fn register_mark_class(
210        &mut self,
211        anchors: Vec<(GlyphContainer, Anchor)>,
212        class_name: &SmolStr,
213    ) {
214        let definitions = anchors
215            .iter()
216            .map(|(members, anchor)| {
217                MarkClassDefinition::new(
218                    MarkClass::new(class_name),
219                    anchor.clone(),
220                    members.clone(),
221                )
222            })
223            .collect();
224        self.mark_classes.insert(class_name.clone(), definitions);
225    }
226
227    fn get_name(&self, id: GlyphId16) -> GlyphName {
228        let str: SmolStr = self
229            .glyph_id_to_name
230            .get(&GlyphId::new(id.to_u32()))
231            .cloned()
232            .unwrap_or_else(|| format!("gid{:04}", id.to_u32()).into());
233        if str == "NULL" {
234            return GlyphName::new("\\NULL");
235        }
236        GlyphName::new(&str)
237    }
238
239    fn gather_language_systems(&mut self) -> Result<(), ReadError> {
240        let mut systems = IndexMap::new();
241        for script_list in [
242            self.gsub.as_ref().and_then(|g| g.script_list().ok()),
243            self.gpos.as_ref().and_then(|g| g.script_list().ok()),
244        ]
245        .into_iter()
246        .flatten()
247        {
248            for script_record in script_list.script_records() {
249                let script_tag = script_record.script_tag();
250                let languages: &mut IndexSet<Tag> = systems.entry(script_tag).or_default();
251                let script = script_record.script(script_list.offset_data())?;
252                if script.default_lang_sys().is_some() {
253                    languages.insert(Tag::new(b"dflt"));
254                }
255                for lang_sys in script.lang_sys_records() {
256                    let lang_sys_tag = lang_sys.lang_sys_tag();
257                    languages.insert(lang_sys_tag);
258                }
259            }
260        }
261        if let Some(index) = systems.get_index_of(&Tag::new(b"DFLT")) {
262            systems.move_index(index, 0);
263            let languages = &mut systems[0];
264            if let Some(index) = languages.get_index_of(&Tag::new(b"dflt")) {
265                languages.move_index(index, 0);
266            }
267        }
268        self.language_systems = systems;
269        Ok(())
270    }
271
272    fn dump_language_systems(&self) -> Vec<ToplevelItem> {
273        let mut items = vec![];
274        for (script_tag, lang_sys_tags) in &self.language_systems {
275            for lang_sys_tag in lang_sys_tags {
276                let lss =
277                    LanguageSystemStatement::new(script_tag.to_string(), lang_sys_tag.to_string());
278                items.push(ToplevelItem::LanguageSystem(lss));
279            }
280        }
281        items
282    }
283
284    fn resolve_coverage(&self, coverage: &CoverageTable) -> Vec<GlyphContainer> {
285        coverage
286            .iter()
287            .map(|g| GlyphContainer::GlyphName(self.get_name(g)))
288            .collect()
289    }
290    fn resolve_coverage_to_class(
291        &mut self,
292        coverage: &CoverageTable,
293        collapse_single: bool,
294    ) -> GlyphContainer {
295        let glyphs = self.resolve_coverage(coverage);
296        if collapse_single && glyphs.len() == 1 {
297            return glyphs.into_iter().next().unwrap();
298        }
299        let glyphclass = GlyphClass::new(glyphs.clone(), 0..0);
300
301        if glyphs.len() >= PROMOTE_TO_NAMED_CLASS_THRESHOLD {
302            // Have we seen this exact set of glyphs before (in order)? If so, reuse the same class. Otherwise, make a new one.
303            if let Some(_class_name) = self
304                .named_classes
305                .iter()
306                .find(|(_, class)| class.glyphs == glyphclass.glyphs)
307                .map(|(name, _)| name)
308            {
309                GlyphContainer::GlyphClass(GlyphClass::new(glyphclass.glyphs.clone(), 0..0))
310            } else {
311                let class_name = format!("class_{}", self.named_classes.len());
312                self.named_classes
313                    .insert(class_name.clone().into(), glyphclass.clone());
314                GlyphContainer::GlyphClass(GlyphClass::new(glyphclass.glyphs.clone(), 0..0))
315            }
316        } else {
317            GlyphContainer::GlyphClass(glyphclass)
318        }
319    }
320
321    fn resolve_classes(&self, class_def: Option<&ClassDef>) -> HashMap<u16, Vec<GlyphContainer>> {
322        let mut classes: HashMap<u16, Vec<GlyphContainer>> = HashMap::new();
323        let mut used_glyphs = HashSet::new();
324        for (glyph_id, class_id) in class_def.into_iter().flat_map(|class_def| class_def.iter()) {
325            used_glyphs.insert(glyph_id.to_u16());
326            classes
327                .entry(class_id)
328                .or_default()
329                .push(GlyphContainer::GlyphName(self.get_name(glyph_id)));
330        }
331
332        // Class 0 is all glyphs that are not explicitly assigned by the ClassDef.
333        for gid in 0..self.num_glyphs {
334            if !used_glyphs.contains(&gid) {
335                classes
336                    .entry(0)
337                    .or_default()
338                    .push(GlyphContainer::GlyphName(
339                        self.get_name(GlyphId16::new(gid)),
340                    ));
341            }
342        }
343        classes
344    }
345
346    pub(crate) fn gensym(&mut self, prefix: &str) -> SmolStr {
347        let symbol_index = self.symbols.entry(prefix.into()).or_insert(1);
348        let name = SmolStr::new(format!("{}_{}", prefix, symbol_index));
349        *symbol_index += 1;
350        name
351    }
352
353    /// A made-up name for a mark class's anchor. It must not end in `_<digits>`,
354    /// which denotes a ligature component's anchor.
355    pub(crate) fn mark_class_symbol(&mut self, class_number: u16) -> SmolStr {
356        let prefix = SmolStr::new(format!("mark_class_{}x", class_number));
357        let symbol_index = self.symbols.entry(prefix.clone()).or_insert(1);
358        let name = SmolStr::new(format!("{}{}", prefix, symbol_index));
359        *symbol_index += 1;
360        name
361    }
362
363    fn assign_lookup_name<T: SubOrPos>(&mut self, prefix: &str, index: u16, phase: T) {
364        let name = self.gensym(prefix);
365        self.lookup_map.insert((phase.to_string(), index), name);
366    }
367
368    fn create_lookup_block<T: SubOrPos>(&self, index: u16, phase: T) -> LookupBlock {
369        LookupBlock::new(self.get_lookup_name(index, phase), vec![], false, 0..0)
370    }
371
372    fn get_lookup_name<T: SubOrPos>(&self, lookup_list_index: u16, phase: T) -> SmolStr {
373        self.lookup_map
374            .get(&(phase.to_string(), lookup_list_index))
375            .cloned()
376            .unwrap_or_else(|| format!("{}_lookup_{}", phase, lookup_list_index).into())
377    }
378
379    /// Feature files require lookups to be defined before use. Reorder them while
380    /// keeping the original order where possible.
381    fn order_lookups(&mut self) {
382        let used_by_features: HashSet<SmolStr> = self
383            .features
384            .values()
385            .flatten()
386            .filter_map(|statement| match statement {
387                Statement::LookupReference(reference) => Some(SmolStr::new(&reference.lookup_name)),
388                _ => None,
389            })
390            .collect();
391        let calls: HashMap<SmolStr, Vec<SmolStr>> = self
392            .lookups
393            .iter()
394            .map(|(name, lookup)| (name.clone(), called_lookups(lookup)))
395            .collect();
396        let contextual_only: HashSet<SmolStr> = calls
397            .values()
398            .flatten()
399            .filter(|name| !used_by_features.contains(*name))
400            .cloned()
401            .collect();
402        let feature_dependencies: HashMap<SmolStr, Vec<SmolStr>> = calls
403            .keys()
404            .map(|name| {
405                (
406                    name.clone(),
407                    feature_dependencies(name, &calls, &contextual_only),
408                )
409            })
410            .collect();
411        let count_pending_dependencies =
412            |name: &SmolStr, pending: &IndexMap<SmolStr, LookupBlock>| {
413                feature_dependencies[name]
414                    .iter()
415                    .filter(|dependency| pending.contains_key(*dependency))
416                    .count()
417            };
418
419        let mut pending = std::mem::take(&mut self.lookups);
420        let mut deferred: Vec<SmolStr> = vec![];
421        let names: Vec<SmolStr> = pending.keys().cloned().collect();
422        for name in names {
423            if !pending.contains_key(&name) {
424                continue;
425            }
426            let pending_dependencies = count_pending_dependencies(&name, &pending);
427            if contextual_only.contains(&name) {
428                if pending_dependencies == 0 {
429                    self.place_lookup(&name, &mut pending, &calls);
430                }
431                continue;
432            }
433            if pending_dependencies > 1 {
434                deferred.push(name);
435                continue;
436            }
437            self.place_lookup(&name, &mut pending, &calls);
438            while let Some(index) = deferred
439                .iter()
440                .position(|name| count_pending_dependencies(name, &pending) == 0)
441            {
442                let name = deferred.remove(index);
443                self.place_lookup(&name, &mut pending, &calls);
444            }
445        }
446        let remaining: Vec<SmolStr> = pending.keys().cloned().collect();
447        for name in deferred.into_iter().chain(remaining) {
448            self.place_lookup(&name, &mut pending, &calls);
449        }
450    }
451
452    fn place_lookup(
453        &mut self,
454        name: &SmolStr,
455        pending: &mut IndexMap<SmolStr, LookupBlock>,
456        calls: &HashMap<SmolStr, Vec<SmolStr>>,
457    ) {
458        let Some(lookup) = pending.shift_remove(name) else {
459            return;
460        };
461        for dependency in calls.get(name).into_iter().flatten() {
462            self.place_lookup(dependency, pending, calls);
463        }
464        self.lookups.insert(name.clone(), lookup);
465    }
466
467    fn uncompile_gdef(&mut self) -> Result<Vec<ToplevelItem>, ReadError> {
468        let mut statements = vec![];
469        let mut base_glyphs = vec![];
470        let mut mark_glyphs = vec![];
471        let mut ligature_glyphs = vec![];
472        let mut component_glyphs = vec![];
473        let make_class =
474            |v: Vec<GlyphContainer>| Some(GlyphContainer::GlyphClass(GlyphClass::new(v, 0..0)));
475        if let Some(gdef) = &self.gdef {
476            // Uncompile glyph categories
477            if let Some(Ok(glyph_class_def)) = gdef.glyph_class_def() {
478                for (gid, class) in glyph_class_def.iter() {
479                    let name = self.get_name(gid);
480                    match class {
481                        1 => base_glyphs.push(GlyphContainer::GlyphName(name)),
482                        2 => ligature_glyphs.push(GlyphContainer::GlyphName(name)),
483                        3 => mark_glyphs.push(GlyphContainer::GlyphName(name)),
484                        4 => component_glyphs.push(GlyphContainer::GlyphName(name)),
485                        _ => {}
486                    }
487                }
488                statements.push(GdefStatement::GlyphClassDef(GlyphClassDefStatement::new(
489                    make_class(base_glyphs),
490                    make_class(ligature_glyphs),
491                    make_class(mark_glyphs),
492                    make_class(component_glyphs),
493                    0..0,
494                )));
495            }
496            if let Some(Ok(attach_list)) = gdef.attach_list() {
497                let coverage = attach_list.coverage()?;
498                for (gid, attach_point) in coverage.iter().zip(attach_list.attach_points().iter()) {
499                    let point_indices: Vec<usize> = attach_point?
500                        .point_indices()
501                        .iter()
502                        .map(|index| index.get() as usize)
503                        .collect();
504                    if !point_indices.is_empty() {
505                        statements.push(GdefStatement::Attach(AttachStatement::new(
506                            GlyphContainer::GlyphName(self.get_name(gid)),
507                            point_indices,
508                            0..0,
509                        )));
510                    }
511                }
512            }
513            if let Some(Ok(lig_caret_list)) = gdef.lig_caret_list() {
514                let coverage = lig_caret_list.coverage()?;
515                for (gid, lig_glyph) in coverage.iter().zip(lig_caret_list.lig_glyphs().iter()) {
516                    let mut positions = vec![];
517                    let mut point_indices = vec![];
518                    for caret in lig_glyph?.caret_values().iter() {
519                        match caret? {
520                            CaretValue::Format1(caret) => {
521                                positions.push(Metric::Scalar(caret.coordinate()))
522                            }
523                            CaretValue::Format2(caret) => {
524                                point_indices.push(caret.caret_value_point_index() as usize)
525                            }
526                            CaretValue::Format3(caret) => {
527                                positions.push(self.resolve_pos_with_variations(
528                                    caret.coordinate(),
529                                    Some(caret.device()),
530                                )?)
531                            }
532                        }
533                    }
534                    let glyph = GlyphContainer::GlyphName(self.get_name(gid));
535                    if !positions.is_empty() {
536                        statements.push(GdefStatement::LigatureCaretByPos(
537                            LigatureCaretByPosStatement::new(glyph.clone(), positions, 0..0),
538                        ));
539                    }
540                    if !point_indices.is_empty() {
541                        statements.push(GdefStatement::LigatureCaretByIndex(
542                            LigatureCaretByIndexStatement::new(glyph, point_indices, 0..0),
543                        ));
544                    }
545                }
546            }
547        }
548        if statements.is_empty() {
549            return Ok(vec![]);
550        }
551        Ok(vec![ToplevelItem::Gdef(Table { statements })])
552    }
553
554    fn uncompile_feature_table(&mut self) -> Result<(), ReadError> {
555        let mut registered_features = vec![];
556        let mut unregistered_features = vec![];
557        let mut feature_name_ids = IndexMap::new();
558        let mut size_params = None;
559        let mut cv_params = IndexMap::new();
560        if let Some(feature_list) = self.gsub.as_ref().and_then(|gsub| gsub.feature_list().ok()) {
561            let systems =
562                feature_language_systems(self.gsub.as_ref().and_then(|g| g.script_list().ok()))?;
563            for (index, feature_record) in feature_list.feature_records().iter().enumerate() {
564                let feature_tag = feature_record.feature_tag();
565                let feature = feature_record.feature(feature_list.offset_data())?;
566                if let Some(Ok(FeatureParams::StylisticSet(params))) = feature.feature_params() {
567                    feature_name_ids
568                        .entry(feature_tag)
569                        .or_insert(params.ui_name_id());
570                }
571                if let Some(Ok(FeatureParams::CharacterVariant(params))) = feature.feature_params()
572                {
573                    cv_params.entry(feature_tag).or_insert(params);
574                }
575                let lookup_indices = feature.lookup_list_indices();
576                let lookups = lookup_indices
577                    .iter()
578                    .map(|i| lookup_reference(&self.get_lookup_name(i.get(), Subst)))
579                    .collect();
580                if let Some(language_systems) = systems.get(&(index as u16)) {
581                    registered_features.push((feature_tag, language_systems.clone(), lookups));
582                } else {
583                    unregistered_features.push((feature_tag, lookups));
584                }
585            }
586        }
587
588        if let Some(feature_list) = self.gpos.as_ref().and_then(|gpos| gpos.feature_list().ok()) {
589            let systems =
590                feature_language_systems(self.gpos.as_ref().and_then(|g| g.script_list().ok()))?;
591            for (index, feature_record) in feature_list.feature_records().iter().enumerate() {
592                let feature_tag = feature_record.feature_tag();
593                let feature = feature_record.feature(feature_list.offset_data())?;
594                if let Some(Ok(FeatureParams::StylisticSet(params))) = feature.feature_params() {
595                    feature_name_ids
596                        .entry(feature_tag)
597                        .or_insert(params.ui_name_id());
598                }
599                if let Some(Ok(FeatureParams::Size(params))) = feature.feature_params() {
600                    size_params.get_or_insert(params);
601                }
602                let lookup_indices = feature.lookup_list_indices();
603                let lookups = lookup_indices
604                    .iter()
605                    .map(|i| lookup_reference(&self.get_lookup_name(i.get(), Pos)))
606                    .collect();
607                if let Some(language_systems) = systems.get(&(index as u16)) {
608                    registered_features.push((feature_tag, language_systems.clone(), lookups));
609                } else {
610                    unregistered_features.push((feature_tag, lookups));
611                }
612            }
613        }
614        self.add_registered_features(registered_features);
615        for (feature_tag, name_id) in feature_name_ids {
616            let names = self.uncompile_feature_names("featureNames", name_id);
617            if let Some(statements) = self.features.get_mut(feature_tag.to_string().as_str()) {
618                statements.insert(0, names);
619            }
620        }
621        for (feature_tag, params) in cv_params {
622            let parameters = self.uncompile_cv_parameters(&params);
623            if let Some(statements) = self.features.get_mut(feature_tag.to_string().as_str()) {
624                statements.insert(0, parameters);
625            }
626        }
627        if let Some(params) = size_params {
628            let parameters = self.uncompile_size_parameters(&params);
629            if let Some(statements) = self.features.get_mut("size") {
630                statements.splice(0..0, parameters);
631            }
632        }
633
634        let referenced_lookups: Vec<Statement> = self
635            .features
636            .values()
637            .flatten()
638            .cloned()
639            .chain(
640                self.lookups
641                    .values()
642                    .flat_map(called_lookups)
643                    .map(|name| lookup_reference(&name)),
644            )
645            .collect();
646        for (feature_tag, lookups) in unregistered_features {
647            let lookups = lookups
648                .into_iter()
649                .filter(|lookup| !referenced_lookups.contains(lookup))
650                .map(comment_out)
651                .collect();
652            self.add_feature_lookups(feature_tag, lookups);
653        }
654
655        Ok(())
656    }
657
658    fn uncompile_feature_variations(&mut self) -> Result<(), ReadError> {
659        if let Some((feature_list, feature_variations, script_list)) =
660            self.gsub.as_ref().and_then(|gsub| {
661                Some((
662                    gsub.feature_list().ok()?,
663                    gsub.feature_variations()?,
664                    gsub.script_list().ok(),
665                ))
666            })
667        {
668            self.add_feature_variations(feature_list, feature_variations?, script_list, Subst)?;
669        }
670        if let Some((feature_list, feature_variations, script_list)) =
671            self.gpos.as_ref().and_then(|gpos| {
672                Some((
673                    gpos.feature_list().ok()?,
674                    gpos.feature_variations()?,
675                    gpos.script_list().ok(),
676                ))
677            })
678        {
679            self.add_feature_variations(feature_list, feature_variations?, script_list, Pos)?;
680        }
681        Ok(())
682    }
683
684    fn add_feature_variations<T: SubOrPos + Copy>(
685        &mut self,
686        feature_list: FeatureList,
687        feature_variations: FeatureVariations,
688        script_list: Option<ScriptList>,
689        phase: T,
690    ) -> Result<(), ReadError> {
691        let systems = feature_language_systems(script_list)?;
692        let data = feature_variations.offset_data();
693        for record in feature_variations.feature_variation_records() {
694            let (Some(condition_set), Some(substitution)) = (
695                record.condition_set(data),
696                record.feature_table_substitution(data),
697            ) else {
698                continue;
699            };
700            let Some(conditions) = condition_set?
701                .conditions()
702                .iter()
703                .map(|condition| match condition {
704                    Ok(Condition::Format1AxisRange(condition)) => {
705                        self.resolve_condition(&condition)
706                    }
707                    _ => None,
708                })
709                .collect::<Option<Vec<_>>>()
710            else {
711                continue;
712            };
713            let name = self.gensym("condition").to_string();
714            let substitution = substitution?;
715            // Additional lookups by feature and language system.
716            let mut registrations: IndexMap<Tag, IndexMap<LanguageSystem, (bool, Vec<Statement>)>> =
717                IndexMap::new();
718            for substitution_record in substitution.substitutions() {
719                let feature_index = substitution_record.feature_index();
720                let Some(feature_record) =
721                    feature_list.feature_records().get(feature_index as usize)
722                else {
723                    continue;
724                };
725                let feature = feature_record.feature(feature_list.offset_data())?;
726                let alternate =
727                    substitution_record.alternate_feature(substitution.offset_data())?;
728                let Some(additional_lookups) = alternate
729                    .lookup_list_indices()
730                    .strip_prefix(feature.lookup_list_indices())
731                else {
732                    continue;
733                };
734                let lookups: Vec<Statement> = additional_lookups
735                    .iter()
736                    .map(|index| lookup_reference(&self.get_lookup_name(index.get(), phase)))
737                    .collect();
738                for (language_system, _) in systems.get(&feature_index).into_iter().flatten() {
739                    let (_, registered_lookups) = registrations
740                        .entry(feature_record.feature_tag())
741                        .or_default()
742                        .entry(*language_system)
743                        .or_default();
744                    for lookup in &lookups {
745                        if !registered_lookups.contains(lookup) {
746                            registered_lookups.push(lookup.clone());
747                        }
748                    }
749                }
750            }
751            let variations = registrations
752                .into_iter()
753                .map(|(feature_tag, systems)| {
754                    let statements = self
755                        .registration_statements(&systems)
756                        .unwrap_or_else(|| systems.into_values().next().unwrap_or_default().1);
757                    VariationBlock::new(
758                        feature_tag.to_string().into(),
759                        name.clone(),
760                        statements,
761                        false,
762                        0..0,
763                    )
764                })
765                .collect();
766            self.feature_variations
767                .push((ConditionSet::new(name, conditions, 0..0), variations));
768        }
769        Ok(())
770    }
771
772    fn add_registered_features(
773        &mut self,
774        features: Vec<(Tag, FeatureLanguageSystems, Vec<Statement>)>,
775    ) {
776        let mut registrations: IndexMap<Tag, IndexMap<LanguageSystem, (bool, Vec<Statement>)>> =
777            IndexMap::new();
778        for (feature_tag, language_systems, lookups) in features {
779            for (language_system, required) in language_systems {
780                let (is_required, registered) = registrations
781                    .entry(feature_tag)
782                    .or_default()
783                    .entry(language_system)
784                    .or_default();
785                *is_required |= required;
786                for lookup in &lookups {
787                    if !registered.contains(lookup) {
788                        registered.push(lookup.clone());
789                    }
790                }
791            }
792        }
793
794        for (feature_tag, systems) in registrations {
795            let statements = if [Tag::new(b"aalt"), Tag::new(b"size")].contains(&feature_tag) {
796                None
797            } else {
798                self.registration_statements(&systems)
799            };
800            if let Some(statements) = statements {
801                self.features
802                    .insert(feature_tag.to_string().into(), statements);
803            } else {
804                for (_, lookups) in systems.into_values() {
805                    self.add_feature_lookups(feature_tag, lookups);
806                }
807            }
808        }
809        // A lookup written into aalt is dropped unless something else uses it
810        let used: HashSet<SmolStr> = self
811            .features
812            .values()
813            .flatten()
814            .filter_map(|statement| match statement {
815                Statement::LookupReference(reference) => Some(SmolStr::new(&reference.lookup_name)),
816                _ => None,
817            })
818            .chain(self.lookups.values().flat_map(called_lookups))
819            .collect();
820        for name in std::mem::take(&mut self.aalt_lookups) {
821            if !used.contains(&name) {
822                self.lookups.shift_remove(&name);
823            }
824        }
825    }
826
827    /// Build script, language, and lookup statements for feature registrations.
828    /// Return None when every language system in the font uses the same lookups
829    /// and none requires the feature.
830    fn registration_statements(
831        &self,
832        systems: &IndexMap<LanguageSystem, (bool, Vec<Statement>)>,
833    ) -> Option<Vec<Statement>> {
834        let dflt = Tag::new(b"dflt");
835        let system_count: usize = self.language_systems.values().map(|l| l.len()).sum();
836        let (_, first) = &systems[0];
837        let everywhere = systems.len() == system_count
838            && systems.values().all(|(required, lookups)| {
839                !required
840                    && lookups.len() == first.len()
841                    && lookups.iter().all(|l| first.contains(l))
842            });
843        if everywhere {
844            return None;
845        }
846        let mut statements = vec![];
847        for (script_tag, languages) in &self.language_systems {
848            if !languages
849                .iter()
850                .any(|language| systems.contains_key(&(*script_tag, *language)))
851            {
852                continue;
853            }
854            statements.push(Statement::Script(ScriptStatement::new(
855                script_tag.to_string().trim_end().into(),
856            )));
857            let (default_required, default_lookups) = systems
858                .get(&(*script_tag, dflt))
859                .cloned()
860                .unwrap_or_default();
861            if default_required {
862                statements.push(Statement::Language(LanguageStatement::new(
863                    dflt.to_string(),
864                    true,
865                    true,
866                )));
867            }
868            statements.extend(default_lookups.iter().cloned());
869            for language in languages.iter().filter(|language| **language != dflt) {
870                let Some((required, lookups)) = systems.get(&(*script_tag, *language)) else {
871                    continue;
872                };
873                let include_dflt = default_lookups.iter().all(|l| lookups.contains(l));
874                statements.push(Statement::Language(LanguageStatement::new(
875                    language.to_string().trim_end().into(),
876                    include_dflt,
877                    *required,
878                )));
879                statements.extend(
880                    lookups
881                        .iter()
882                        .filter(|l| !include_dflt || !default_lookups.contains(l))
883                        .cloned(),
884                );
885            }
886        }
887        Some(statements)
888    }
889
890    fn uncompile_feature_names(&self, tag: &str, name_id: NameId) -> Statement {
891        let statements = self
892            .name
893            .iter()
894            .flat_map(|name| {
895                name.name_record()
896                    .iter()
897                    .filter(move |record| record.name_id() == name_id)
898                    .filter_map(move |record| {
899                        let string = record.string(name.string_data()).ok()?;
900                        Some(Statement::FeatureNameStatement(NameRecord::new(
901                            record.platform_id(),
902                            record.encoding_id(),
903                            record.language_id(),
904                            string.to_string(),
905                            NameRecordKind::FeatureName,
906                            0..0,
907                        )))
908                    })
909            })
910            .collect();
911        Statement::NestedBlock(NestedBlock {
912            tag: tag.into(),
913            statements,
914            pos: 0..0,
915        })
916    }
917
918    fn uncompile_cv_parameters(&self, params: &CharacterVariantParams) -> Statement {
919        let mut statements = vec![];
920        for (tag, name_id) in [
921            ("FeatUILabelNameID", params.feat_ui_label_name_id()),
922            (
923                "FeatUITooltipTextNameID",
924                params.feat_ui_tooltip_text_name_id(),
925            ),
926            ("SampleTextNameID", params.sample_text_name_id()),
927        ] {
928            statements.push(self.uncompile_feature_names(tag, name_id));
929        }
930        let first = params.first_param_ui_label_name_id().to_u16();
931        for index in 0..params.num_named_parameters() {
932            statements.push(
933                self.uncompile_feature_names("ParamUILabelNameID", NameId::new(first + index)),
934            );
935        }
936        statements.extend(params.character().iter().map(|character| {
937            Statement::Character(CharacterStatement::new(character.get().to_u32(), 0..0))
938        }));
939        Statement::NestedBlock(NestedBlock {
940            tag: "cvParameters".into(),
941            statements,
942            pos: 0..0,
943        })
944    }
945
946    fn uncompile_size_parameters(&self, params: &SizeParams) -> Vec<Statement> {
947        let mut statements = vec![Statement::SizeParameters(SizeParameters::new(
948            params.design_size() as f64 / 10.0,
949            params.identifier(),
950            params.range_start() as f64 / 10.0,
951            params.range_end() as f64 / 10.0,
952            0..0,
953        ))];
954        let name_id = NameId::new(params.name_entry());
955        if name_id != NameId::new(0) {
956            statements.extend(self.name.iter().flat_map(|name| {
957                name.name_record()
958                    .iter()
959                    .filter(move |record| record.name_id() == name_id)
960                    .filter_map(move |record| {
961                        let string = record.string(name.string_data()).ok()?;
962                        Some(Statement::SizeMenuName(NameRecord::new(
963                            record.platform_id(),
964                            record.encoding_id(),
965                            record.language_id(),
966                            string.to_string(),
967                            NameRecordKind::FeatureSizeMenuName,
968                            0..0,
969                        )))
970                    })
971            }));
972        }
973        statements
974    }
975
976    fn add_feature_lookups(&mut self, feature_tag: Tag, lookups: Vec<Statement>) {
977        // aalt may contain only single and alternate substitutions, so its
978        // lookups are written out as their rules
979        let lookups = if feature_tag == Tag::new(b"aalt") {
980            lookups
981                .into_iter()
982                .flat_map(|statement| match &statement {
983                    Statement::LookupReference(reference) => self
984                        .lookups
985                        .get(reference.lookup_name.as_str())
986                        .map(|block| {
987                            self.aalt_lookups.insert(block.name.clone());
988                            block
989                                .statements
990                                .iter()
991                                .filter(|s| {
992                                    matches!(
993                                        s,
994                                        Statement::SingleSubst(_) | Statement::AlternateSubst(_)
995                                    )
996                                })
997                                .cloned()
998                                .collect()
999                        })
1000                        .unwrap_or_else(|| vec![statement]),
1001                    _ => vec![statement],
1002                })
1003                .collect()
1004        } else {
1005            lookups
1006        };
1007        let statements = self
1008            .features
1009            .entry(feature_tag.to_string().into())
1010            .or_default();
1011        for lookup in lookups {
1012            if !statements.contains(&lookup) {
1013                statements.push(lookup);
1014            }
1015        }
1016    }
1017
1018    fn add_lookup_flags(
1019        &mut self,
1020        lookupblock: &mut LookupBlock,
1021        flags: LookupFlag,
1022        mark_filtering_set: Option<u16>,
1023    ) {
1024        if flags == LookupFlag::empty() {
1025            return;
1026        }
1027        let mark_glyph_sets = self.gdef.as_ref().and_then(|x| {
1028            let mark_glyph_sets = x.mark_glyph_sets_def();
1029            if let Some(Ok(mark_glyph_sets)) = mark_glyph_sets {
1030                Some(mark_glyph_sets)
1031            } else {
1032                None
1033            }
1034        });
1035        let mark_attachment_classes = self.gdef.as_ref().and_then(|x| {
1036            let mark_attachment_classes = x.mark_attach_class_def();
1037            if let Some(Ok(mark_attachment_classes)) = mark_attachment_classes {
1038                Some(mark_attachment_classes)
1039            } else {
1040                None
1041            }
1042        });
1043        let set = mark_filtering_set.and_then(|set| {
1044            mark_glyph_sets
1045                .and_then(|mgss| mgss.coverages().get(set as usize).ok())
1046                .map(|coverage| self.resolve_coverage_to_class(&coverage, false))
1047        });
1048        let mark_attachment_class = flags.mark_attachment_class().map(|class| {
1049            let classes = mark_attachment_classes
1050                .and_then(|mac| self.resolve_classes(Some(&mac)).get(&class).cloned())
1051                .unwrap_or_default();
1052            GlyphContainer::GlyphClass(GlyphClass::new(classes, 0..0))
1053        });
1054
1055        lookupblock.statements.insert(
1056            0,
1057            Statement::LookupFlag(LookupFlagStatement::new(
1058                flags.to_bits(),
1059                mark_attachment_class,
1060                set,
1061                0..0,
1062            )),
1063        );
1064    }
1065}
1066
1067fn lookup_reference(name: &SmolStr) -> Statement {
1068    Statement::LookupReference(LookupReferenceStatement::new(name.to_string(), 0..0))
1069}
1070
1071pub(crate) fn add_subtable_break(lookupblock: &mut LookupBlock) {
1072    if lookupblock
1073        .statements
1074        .last()
1075        .is_some_and(|statement| !matches!(statement, Statement::Subtable(_)))
1076    {
1077        lookupblock
1078            .statements
1079            .push(Statement::Subtable(SubtableStatement::new()));
1080    }
1081}
1082
1083fn comment_out(statement: Statement) -> Statement {
1084    Statement::Comment(Comment::new(format!("# {}", statement.as_fea(""))))
1085}
1086
1087fn feature_language_systems(
1088    script_list: Option<ScriptList>,
1089) -> Result<HashMap<u16, FeatureLanguageSystems>, ReadError> {
1090    let mut systems: HashMap<u16, FeatureLanguageSystems> = HashMap::new();
1091    let Some(script_list) = script_list else {
1092        return Ok(systems);
1093    };
1094    for script_record in script_list.script_records() {
1095        let script = script_record.script(script_list.offset_data())?;
1096        let mut language_systems = vec![];
1097        if let Some(lang_sys) = script.default_lang_sys() {
1098            language_systems.push((Tag::new(b"dflt"), lang_sys?));
1099        }
1100        for lang_sys_record in script.lang_sys_records() {
1101            language_systems.push((
1102                lang_sys_record.lang_sys_tag(),
1103                lang_sys_record.lang_sys(script.offset_data())?,
1104            ));
1105        }
1106        for (lang_sys_tag, lang_sys) in language_systems {
1107            let required_feature_index = lang_sys.required_feature_index();
1108            let indices = lang_sys.feature_indices().iter().map(|index| index.get());
1109            for index in
1110                indices.chain((required_feature_index != 0xFFFF).then_some(required_feature_index))
1111            {
1112                systems.entry(index).or_default().push((
1113                    (script_record.script_tag(), lang_sys_tag),
1114                    index == required_feature_index,
1115                ));
1116            }
1117        }
1118    }
1119    Ok(systems)
1120}
1121
1122fn called_lookups(lookup: &LookupBlock) -> Vec<SmolStr> {
1123    lookup
1124        .statements
1125        .iter()
1126        .flat_map(|statement| match statement {
1127            Statement::ChainedContextSubst(statement) => statement.lookups.concat(),
1128            Statement::ChainedContextPos(statement) => statement.lookups.concat(),
1129            _ => vec![],
1130        })
1131        .collect()
1132}
1133
1134/// Find dependencies used by features, following calls through lookups unused by features.
1135fn feature_dependencies(
1136    name: &SmolStr,
1137    calls: &HashMap<SmolStr, Vec<SmolStr>>,
1138    contextual_only: &HashSet<SmolStr>,
1139) -> Vec<SmolStr> {
1140    let mut dependencies = vec![];
1141    let mut seen = HashSet::new();
1142    let mut stack: Vec<&SmolStr> = calls.get(name).into_iter().flatten().collect();
1143    while let Some(dependency) = stack.pop() {
1144        if !seen.insert(dependency) {
1145            continue;
1146        }
1147        if contextual_only.contains(dependency) {
1148            stack.extend(calls.get(dependency).into_iter().flatten());
1149        } else {
1150            dependencies.push(dependency.clone());
1151        }
1152    }
1153    dependencies
1154}
1155
1156/// Uncompile a TTF font into a fea file.
1157///
1158/// If do_gdef is true, also uncompile the GDEF table and include it in the output.
1159/// Returns a [fea_rs_ast::FeatureFile] representing the uncompiled font, or a ReadError if something went wrong during reading.
1160pub fn uncompile(
1161    font: &skrifa::FontRef,
1162    do_gdef: bool,
1163) -> Result<fea_rs_ast::FeatureFile, ReadError> {
1164    let mut context = UncompileContext::new(font)?;
1165
1166    let mut ff = fea_rs_ast::FeatureFile::new(vec![]);
1167    ff.statements.extend(context.dump_language_systems());
1168
1169    if do_gdef {
1170        ff.statements.extend(context.uncompile_gdef()?);
1171    }
1172
1173    // Add mark classes to the feature file
1174    for definitions in context.mark_classes.values() {
1175        for definition in definitions {
1176            ff.statements
1177                .push(ToplevelItem::MarkClassDefinition(definition.clone()));
1178        }
1179    }
1180    // Add named class definitions
1181    for (name, contents) in context.named_classes.iter() {
1182        ff.statements.push(ToplevelItem::GlyphClassDefinition(
1183            GlyphClassDefinition::new(name.to_string(), contents.clone(), 0..0),
1184        ));
1185    }
1186    // Add all lookups to the feature file
1187    for lookup in context.lookups.values() {
1188        ff.statements.push(ToplevelItem::Lookup(lookup.clone()));
1189    }
1190    // Add all feature references to the feature file
1191    for (feature_name, lookup_refs) in context.features.iter() {
1192        ff.statements
1193            .push(ToplevelItem::Feature(fea_rs_ast::FeatureBlock::new(
1194                feature_name.clone(),
1195                lookup_refs.clone(),
1196                false,
1197                0..0,
1198            )));
1199    }
1200    for (condition_set, variations) in context.feature_variations.iter() {
1201        ff.statements
1202            .push(ToplevelItem::ConditionSet(condition_set.clone()));
1203        for variation in variations {
1204            ff.statements
1205                .push(ToplevelItem::VariationBlock(variation.clone()));
1206        }
1207    }
1208
1209    Ok(ff)
1210}
1211
1212/// Uncompile a TTF font from a byte slice into a fea file. See uncompile() for details.
1213pub fn uncompile_bytes(
1214    font_data: &[u8],
1215    do_gdef: bool,
1216) -> Result<fea_rs_ast::FeatureFile, ReadError> {
1217    let fontref = skrifa::FontRef::new(font_data)?;
1218    uncompile(&fontref, do_gdef)
1219}
1220
1221/// Uncompile a TTF font to a context object.
1222///
1223/// This partially decompiles the font, giving you the component parts so that you can
1224/// put them where you want them. Useful for font editors and other tools that want the
1225/// data but don't want to go all the way to a fea file.
1226pub fn uncompile_context<'a>(font: &'a skrifa::FontRef) -> Result<UncompileContext<'a>, ReadError> {
1227    UncompileContext::new(font)
1228}
1229
1230/// The glyph names the uncompiled features use: the post table's, valid in a
1231/// feature file and unique.
1232pub fn unique_glyph_names(font: &skrifa::FontRef) -> Vec<SmolStr> {
1233    let glyph_names = GlyphNames::new(font);
1234    unique_names(
1235        (0..glyph_names.num_glyphs())
1236            .map(GlyphId::new)
1237            .map(|gid| fea_glyph_name(&glyph_name(&glyph_names, gid)))
1238            .collect(),
1239    )
1240}
1241
1242/// A glyph's post table name; an unnamed glyph is named as fontTools names it.
1243fn glyph_name(glyph_names: &GlyphNames, gid: GlyphId) -> String {
1244    match glyph_names.get(gid) {
1245        Some(name) if !name.is_synthesized() => name.to_string(),
1246        _ if gid.to_u32() == 0 => ".notdef".to_string(),
1247        _ => format!("glyph{:05}", gid.to_u32()),
1248    }
1249}
1250
1251/// A glyph name with each character a feature file does not allow replaced by
1252/// `_`, and a leading `_` if it starts with a digit or a hyphen.
1253fn fea_glyph_name(name: &str) -> SmolStr {
1254    let mut out: String = name
1255        .chars()
1256        .map(|c| {
1257            if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
1258                c
1259            } else {
1260                '_'
1261            }
1262        })
1263        .collect();
1264    if out.is_empty() || out.starts_with(|c: char| c.is_ascii_digit() || c == '-') {
1265        out.insert(0, '_');
1266    }
1267    SmolStr::new(out)
1268}
1269
1270/// Each name, or if an earlier glyph has it, the first free `.1`, `.2`, ...
1271/// suffix, as fontTools does. A suffixed name loses a leading period, which a
1272/// feature file allows only in `.notdef` and `.null`.
1273fn unique_names(names: Vec<SmolStr>) -> Vec<SmolStr> {
1274    let all: HashSet<SmolStr> = names.iter().cloned().collect();
1275    let mut taken: HashSet<SmolStr> = HashSet::new();
1276    names
1277        .into_iter()
1278        .map(|name| {
1279            if taken.insert(name.clone()) {
1280                return name;
1281            }
1282            let base = match name.strip_prefix('.') {
1283                Some(rest) => format!("_{rest}"),
1284                None => name.to_string(),
1285            };
1286            (1..)
1287                .map(|n| SmolStr::new(format!("{base}.{n}")))
1288                .find(|candidate| !all.contains(candidate) && taken.insert(candidate.clone()))
1289                .unwrap_or(name)
1290        })
1291        .collect()
1292}
1293
1294#[cfg(test)]
1295mod tests {
1296    use fea_rs_ast::AsFea;
1297
1298    use super::*;
1299    #[test]
1300    fn test_uncompile_static() {
1301        let data = std::fs::read("resources/test.ttf").unwrap();
1302        let fontref = skrifa::FontRef::new(&data).unwrap();
1303        let ff = uncompile(&fontref, true).unwrap();
1304        assert_eq!(
1305            ff.as_fea(""),
1306            "\
1307languagesystem DFLT dflt;
1308table GDEF {
1309GlyphClassDef [A], [], [grave acute dotbelowcomb], [];
1310} GDEF;
1311
1312markClass grave <anchor 200 150> @bottomleft;
1313markClass acute <anchor 350 0> @bottomleft;
1314markClass dotbelowcomb <anchor 200 -200> @mark_class_1x1;
1315lookup gsub_single_1 {
1316    sub a by b;
1317} gsub_single_1;
1318lookup gsub_multiple_1 {
1319    sub a by b c;
1320} gsub_multiple_1;
1321lookup gsub_alternate_1 {
1322    sub a from [b c d e f];
1323} gsub_alternate_1;
1324lookup gsub_ligature_1 {
1325    sub b c by a;
1326} gsub_ligature_1;
1327lookup gsub_contextual_1 {
1328    sub [one a]' lookup gsub_single_1 b' [two c]' lookup gsub_multiple_1;
1329} gsub_contextual_1;
1330lookup gsub_chain_contextual_1 {
1331    sub one two three a' lookup gsub_single_1 b' c' lookup gsub_multiple_1 x y z;
1332} gsub_chain_contextual_1;
1333lookup gsub_single_2 {
1334    sub [a b] by [b c];
1335} gsub_single_2;
1336lookup gsub_single_3 {
1337    sub [a b] by [d c];
1338} gsub_single_3;
1339lookup gsub_single_4 {
1340    sub a by c;
1341} gsub_single_4;
1342lookup gsub_chain_contextual_2 {
1343    sub one a' lookup gsub_single_4;
1344} gsub_chain_contextual_2;
1345lookup gsub_multiple_2 {
1346    sub e by NULL;
1347} gsub_multiple_2;
1348lookup gsub_chain_contextual_3 {
1349    ignore sub one a' b;
1350    sub a' lookup gsub_single_1;
1351} gsub_chain_contextual_3;
1352lookup gsub_contextual_2 {
1353    ignore sub a' b';
1354    sub a' lookup gsub_single_1 c';
1355} gsub_contextual_2;
1356lookup gsub_single_5 {
1357    lookupflag UseMarkFilteringSet [acute];
1358    sub b by c;
1359} gsub_single_5;
1360lookup gsub_reverse_1 {
1361    rsub one two [a b]' c by [d e];
1362} gsub_reverse_1;
1363lookup gsub_single_6 {
1364    sub one by two;
1365} gsub_single_6;
1366lookup gpos_mark_to_base_1 {
1367    pos base A
1368        <anchor 150 100> mark @bottomleft
1369        <anchor -200 -200> mark @mark_class_1x1;
1370} gpos_mark_to_base_1;
1371lookup gpos_single_1 {
1372    pos A 10;
1373} gpos_single_1;
1374lookup gpos_chain_contextual_1 {
1375    pos one A' lookup gpos_single_1;
1376} gpos_chain_contextual_1;
1377lookup gpos_single_2 {
1378    pos A 20;
1379} gpos_single_2;
1380lookup gpos_chain_contextual_2 {
1381    ignore pos one A';
1382    pos A' lookup gpos_single_2;
1383} gpos_chain_contextual_2;
1384lookup gpos_single_3 {
1385    pos [a b] -50;
1386} gpos_single_3;
1387lookup gpos_single_4 {
1388    pos a -50;
1389    pos b -60;
1390} gpos_single_4;
1391lookup gpos_single_5 {
1392    pos one 30;
1393} gpos_single_5;
1394feature ss01 {
1395lookup gsub_single_6;
1396    lookup gpos_single_5;
1397} ss01;
1398"
1399        );
1400    }
1401
1402    #[test]
1403    fn test_uncompile_language_systems() {
1404        let data = std::fs::read("resources/languages.ttf").unwrap();
1405        let fontref = skrifa::FontRef::new(&data).unwrap();
1406        let ff = uncompile(&fontref, true).unwrap();
1407        assert_eq!(
1408            ff.as_fea(""),
1409            "\
1410languagesystem DFLT dflt;
1411languagesystem latn dflt;
1412languagesystem latn TRK;
1413lookup gsub_single_1 {
1414    sub b by c;
1415} gsub_single_1;
1416lookup gsub_single_2 {
1417    sub a by d;
1418} gsub_single_2;
1419lookup gsub_single_3 {
1420    sub a by e;
1421} gsub_single_3;
1422feature calt {
1423script latn;
1424    lookup gsub_single_2;
1425    language TRK;
1426    lookup gsub_single_3;
1427} calt;
1428feature locl {
1429script latn;
1430    lookup gsub_single_2;
1431    language TRK exclude_dflt;
1432    lookup gsub_single_3;
1433} locl;
1434feature ss01 {
1435lookup gsub_single_1;
1436} ss01;
1437"
1438        );
1439    }
1440
1441    #[test]
1442    fn test_uncompile_unregistered_features() {
1443        let data = std::fs::read("resources/unregistered.ttf").unwrap();
1444        let fontref = skrifa::FontRef::new(&data).unwrap();
1445        let ff = uncompile(&fontref, true).unwrap();
1446        assert_eq!(
1447            ff.as_fea(""),
1448            "\
1449languagesystem DFLT dflt;
1450lookup gsub_single_1 {
1451    sub b by c;
1452} gsub_single_1;
1453lookup gsub_single_2 {
1454    sub b by d;
1455} gsub_single_2;
1456feature ss01 {
1457lookup gsub_single_1;
1458} ss01;
1459feature ss02 {
1460# lookup gsub_single_2;
1461} ss02;
1462feature ss03 {
1463
1464} ss03;
1465"
1466        );
1467    }
1468
1469    #[test]
1470    fn test_uncompile_required_features() {
1471        let data = std::fs::read("resources/required.ttf").unwrap();
1472        let fontref = skrifa::FontRef::new(&data).unwrap();
1473        let ff = uncompile(&fontref, true).unwrap();
1474        assert_eq!(
1475            ff.as_fea(""),
1476            "\
1477languagesystem DFLT dflt;
1478lookup gsub_single_1 {
1479    sub a by b;
1480} gsub_single_1;
1481feature rlig {
1482script DFLT;
1483    language dflt required;
1484    lookup gsub_single_1;
1485} rlig;
1486"
1487        );
1488    }
1489
1490    #[test]
1491    fn test_uncompile_ligature_carets() {
1492        let data = std::fs::read("resources/carets.ttf").unwrap();
1493        let fontref = skrifa::FontRef::new(&data).unwrap();
1494        let ff = uncompile(&fontref, true).unwrap();
1495        assert_eq!(
1496            ff.as_fea(""),
1497            "\
1498table GDEF {
1499LigatureCaretByPos a 100 200;
1500LigatureCaretByIndex b 3;
1501} GDEF;
1502
1503"
1504        );
1505    }
1506
1507    #[test]
1508    fn test_uncompile_empty_mark_attachment_class() {
1509        let data = std::fs::read("resources/markattach.ttf").unwrap();
1510        let fontref = skrifa::FontRef::new(&data).unwrap();
1511        let ff = uncompile(&fontref, true).unwrap();
1512        assert_eq!(
1513            ff.as_fea(""),
1514            "\
1515languagesystem DFLT dflt;
1516lookup gsub_single_1 {
1517    lookupflag MarkAttachmentType [];
1518    sub a by b;
1519} gsub_single_1;
1520feature ss01 {
1521lookup gsub_single_1;
1522} ss01;
1523"
1524        );
1525    }
1526
1527    #[test]
1528    fn test_uncompile_attachment_points() {
1529        let data = std::fs::read("resources/attach.ttf").unwrap();
1530        let fontref = skrifa::FontRef::new(&data).unwrap();
1531        let ff = uncompile(&fontref, true).unwrap();
1532        assert_eq!(
1533            ff.as_fea(""),
1534            "\
1535table GDEF {
1536Attach a 1 2;
1537Attach b 3;
1538} GDEF;
1539
1540"
1541        );
1542    }
1543
1544    #[test]
1545    fn test_uncompile_variable_advances() {
1546        let data = std::fs::read("resources/advances.ttf").unwrap();
1547        let fontref = skrifa::FontRef::new(&data).unwrap();
1548        let ff = uncompile(&fontref, true).unwrap();
1549        assert_eq!(
1550            ff.as_fea(""),
1551            "\
1552languagesystem DFLT dflt;
1553lookup gpos_single_1 {
1554    pos a <0 0 (wght=100:-10 wght=400:0 wght=900:20) (wght=100:30 wght=400:0 wght=900:-40)>;
1555} gpos_single_1;
1556feature kern {
1557lookup gpos_single_1;
1558} kern;
1559"
1560        );
1561    }
1562
1563    #[test]
1564    fn test_uncompile_axis_order() {
1565        let data = std::fs::read("resources/axisorder.ttf").unwrap();
1566        let fontref = skrifa::FontRef::new(&data).unwrap();
1567        let ff = uncompile(&fontref, true).unwrap();
1568        assert_eq!(
1569            ff.as_fea(""),
1570            "\
1571languagesystem DFLT dflt;
1572lookup gpos_single_1 {
1573    pos a (wdth=100,wght=100:-10 wdth=100,wght=400:0 wdth=100,wght=900:20);
1574} gpos_single_1;
1575feature kern {
1576lookup gpos_single_1;
1577} kern;
1578"
1579        );
1580    }
1581
1582    #[test]
1583    fn test_uncompile_class_kerning() {
1584        let data = std::fs::read("resources/kerning.ttf").unwrap();
1585        let fontref = skrifa::FontRef::new(&data).unwrap();
1586        let ff = uncompile(&fontref, true).unwrap();
1587        assert_eq!(
1588            ff.as_fea(""),
1589            "\
1590languagesystem DFLT dflt;
1591lookup gpos_pair_1 {
1592    pos [a b] [c d] -10;
1593} gpos_pair_1;
1594feature kern {
1595lookup gpos_pair_1;
1596} kern;
1597"
1598        );
1599    }
1600
1601    #[test]
1602    fn test_uncompile_feature_names() {
1603        let data = std::fs::read("resources/featurenames.ttf").unwrap();
1604        let fontref = skrifa::FontRef::new(&data).unwrap();
1605        let ff = uncompile(&fontref, true).unwrap();
1606        assert_eq!(
1607            ff.as_fea(""),
1608            "\
1609languagesystem DFLT dflt;
1610lookup gsub_single_1 {
1611    sub a by b;
1612} gsub_single_1;
1613feature ss01 {
1614    featureNames {
1615        name 1 \"Alternate a\";
1616        name \"Alternate a\";
1617    };
1618
1619    lookup gsub_single_1;
1620} ss01;
1621"
1622        );
1623    }
1624
1625    #[test]
1626    fn test_uncompile_feature_variations() {
1627        let data = std::fs::read("resources/variations.ttf").unwrap();
1628        let fontref = skrifa::FontRef::new(&data).unwrap();
1629        let ff = uncompile(&fontref, true).unwrap();
1630        assert_eq!(
1631            ff.as_fea(""),
1632            "\
1633languagesystem DFLT dflt;
1634lookup gsub_single_1 {
1635    sub a by b;
1636} gsub_single_1;
1637lookup gsub_single_2 {
1638    sub b by c;
1639} gsub_single_2;
1640feature rclt {
1641lookup gsub_single_1;
1642} rclt;
1643conditionset condition_1 {
1644\twght 700 900;
1645} condition_1;
1646
1647variation rclt condition_1 {
1648lookup gsub_single_2;
1649} rclt;
1650
1651"
1652        );
1653    }
1654
1655    #[test]
1656    fn test_uncompile_variation_regions() {
1657        let data = std::fs::read("resources/regions.ttf").unwrap();
1658        let fontref = skrifa::FontRef::new(&data).unwrap();
1659        let ff = uncompile(&fontref, true).unwrap();
1660        assert_eq!(
1661            ff.as_fea(""),
1662            "\
1663languagesystem DFLT dflt;
1664lookup gpos_single_1 {
1665    pos a (wght=100:-10 wght=400:0 wght=650:15 wght=900:20);
1666    pos b (wght=100:-10 wght=400:0 wght=900:20);
1667    pos c (wght=400:0 wght=650:0 wght=900:20);
1668} gpos_single_1;
1669feature kern {
1670lookup gpos_single_1;
1671} kern;
1672"
1673        );
1674    }
1675
1676    #[test]
1677    fn test_uncompile_null_glyph() {
1678        let data = std::fs::read("resources/nullglyph.ttf").unwrap();
1679        let fontref = skrifa::FontRef::new(&data).unwrap();
1680        let ff = uncompile(&fontref, true).unwrap();
1681        assert_eq!(
1682            ff.as_fea(""),
1683            "\
1684languagesystem DFLT dflt;
1685lookup gsub_single_1 {
1686    sub a by \\NULL;
1687} gsub_single_1;
1688feature ss01 {
1689lookup gsub_single_1;
1690} ss01;
1691"
1692        );
1693    }
1694
1695    #[test]
1696    fn test_uncompile_single_subst_modulo() {
1697        let data = std::fs::read("resources/gsub1_1_modulo_f1.otf").unwrap();
1698        let fontref = skrifa::FontRef::new(&data).unwrap();
1699        let ff = uncompile(&fontref, true).unwrap();
1700        assert_eq!(
1701            ff.as_fea(""),
1702            "\
1703languagesystem latn dflt;
1704lookup gsub_single_1 {
1705    sub [g21 g22] by [gid32787 gid32788];
1706    subtable;
1707    sub [g19 g20] by [gid32789 gid32790];
1708} gsub_single_1;
1709lookup gsub_single_2 {
1710    sub gid32787 by g23;
1711    subtable;
1712    sub gid32788 by g18;
1713    subtable;
1714    sub gid32789 by g17;
1715    subtable;
1716    sub gid32790 by g24;
1717} gsub_single_2;
1718feature test {
1719lookup gsub_single_1;
1720    lookup gsub_single_2;
1721} test;
1722"
1723        );
1724    }
1725
1726    #[test]
1727    fn test_uncompile_no_variation_index() {
1728        let data = std::fs::read("resources/novariation.ttf").unwrap();
1729        let fontref = skrifa::FontRef::new(&data).unwrap();
1730        let ff = uncompile(&fontref, true).unwrap();
1731        assert_eq!(
1732            ff.as_fea(""),
1733            "\
1734languagesystem DFLT dflt;
1735lookup gpos_single_1 {
1736    pos a (wght=100:-10 wght=400:0);
1737    pos b 10;
1738} gpos_single_1;
1739feature kern {
1740lookup gpos_single_1;
1741} kern;
1742"
1743        );
1744    }
1745
1746    #[test]
1747    fn test_uncompile_subtable_breaks() {
1748        let data = std::fs::read("resources/subtables.ttf").unwrap();
1749        let fontref = skrifa::FontRef::new(&data).unwrap();
1750        let ff = uncompile(&fontref, true).unwrap();
1751        assert_eq!(
1752            ff.as_fea(""),
1753            "\
1754languagesystem DFLT dflt;
1755lookup gsub_single_1 {
1756    sub a by b;
1757    subtable;
1758    sub c by d;
1759} gsub_single_1;
1760lookup gpos_pair_1 {
1761    pos [a b] [c d] -40;
1762    subtable;
1763    pos [a b] [c d] 0;
1764} gpos_pair_1;
1765feature salt {
1766lookup gsub_single_1;
1767} salt;
1768feature kern {
1769lookup gpos_pair_1;
1770} kern;
1771"
1772        );
1773    }
1774
1775    #[test]
1776    fn test_fea_glyph_name() {
1777        assert_eq!(fea_glyph_name("Numero sign"), "Numero_sign");
1778        assert_eq!(fea_glyph_name("uni0300"), "uni0300");
1779        assert_eq!(fea_glyph_name("1st"), "_1st");
1780        assert_eq!(fea_glyph_name("-x"), "_-x");
1781        assert_eq!(fea_glyph_name(""), "_");
1782        assert_eq!(fea_glyph_name(".notdef"), ".notdef");
1783    }
1784
1785    #[test]
1786    fn test_unique_names() {
1787        let names = [".notdef", ".notdef", "a", "a", "a.1"].map(SmolStr::new);
1788        assert_eq!(
1789            unique_names(names.to_vec()),
1790            [".notdef", "_notdef.1", "a", "a.2", "a.1"]
1791        );
1792    }
1793
1794    #[test]
1795    fn test_unique_glyph_names() {
1796        // the post table names glyphs 2 and 3 both "dupe"; salt substitutes glyph 3
1797        let data = std::fs::read("resources/dupnames.ttf").unwrap();
1798        let fontref = skrifa::FontRef::new(&data).unwrap();
1799        assert_eq!(
1800            unique_glyph_names(&fontref),
1801            [".notdef", "a", "dupe", "dupe.1"]
1802        );
1803        let fea = uncompile(&fontref, true).unwrap().as_fea("");
1804        assert!(fea.contains("sub a by dupe.1;"), "{fea}");
1805    }
1806
1807    #[test]
1808    fn test_unnamed_glyph_names() {
1809        // post format 3 names no glyph
1810        let data = std::fs::read("resources/noname.ttf").unwrap();
1811        let fontref = skrifa::FontRef::new(&data).unwrap();
1812        assert_eq!(
1813            unique_glyph_names(&fontref),
1814            [".notdef", "glyph00001", "glyph00002"]
1815        );
1816    }
1817
1818    #[test]
1819    fn test_uncompile_aalt_rules() {
1820        let data = std::fs::read("resources/aalt.ttf").unwrap();
1821        let fontref = skrifa::FontRef::new(&data).unwrap();
1822        let fea = uncompile(&fontref, true).unwrap().as_fea("");
1823        let aalt = &fea[fea.find("feature aalt").unwrap()..fea.find("} aalt;").unwrap()];
1824        assert!(!aalt.contains("lookup "), "{fea}");
1825        assert!(aalt.contains("sub a by a.alt1;"), "{fea}");
1826        assert!(aalt.contains("sub b from [b.alt1 b.alt2];"), "{fea}");
1827        // salt also uses the single substitution; nothing else uses the alternate one
1828        assert!(fea.contains("lookup gsub_single_1 {"), "{fea}");
1829        assert!(!fea.contains("lookup gsub_alternate_1 {"), "{fea}");
1830    }
1831
1832    #[test]
1833    fn test_uncompile_size_feature() {
1834        let data = std::fs::read("resources/size.ttf").unwrap();
1835        let fontref = skrifa::FontRef::new(&data).unwrap();
1836        let ff = uncompile(&fontref, true).unwrap();
1837        assert_eq!(
1838            ff.as_fea(""),
1839            "\
1840languagesystem DFLT dflt;
1841feature size {
1842parameters 10.0 3 80 139;
1843    sizemenuname \"Text\";
1844} size;
1845"
1846        );
1847    }
1848
1849    #[test]
1850    fn test_uncompile_variable_ligature_carets() {
1851        let data = std::fs::read("resources/varcarets.ttf").unwrap();
1852        let fontref = skrifa::FontRef::new(&data).unwrap();
1853        let ff = uncompile(&fontref, true).unwrap();
1854        assert_eq!(
1855            ff.as_fea(""),
1856            "\
1857table GDEF {
1858LigatureCaretByPos a (wght=100:100 wght=400:150 wght=900:200) 300;
1859} GDEF;
1860
1861"
1862        );
1863    }
1864
1865    #[test]
1866    fn test_uncompile_fractional_locations() {
1867        let data = std::fs::read("resources/fraclocations.ttf").unwrap();
1868        let fontref = skrifa::FontRef::new(&data).unwrap();
1869        let ff = uncompile(&fontref, true).unwrap();
1870        assert_eq!(
1871            ff.as_fea(""),
1872            "\
1873languagesystem DFLT dflt;
1874lookup gpos_single_1 {
1875    pos a (wdth=75:-10 wdth=85:-8 wdth=87.5:-6 wdth=112.5:0);
1876} gpos_single_1;
1877feature kern {
1878lookup gpos_single_1;
1879} kern;
1880"
1881        );
1882    }
1883
1884    #[test]
1885    fn test_uncompile_cv_parameters() {
1886        let data = std::fs::read("resources/cvparams.ttf").unwrap();
1887        let fontref = skrifa::FontRef::new(&data).unwrap();
1888        let ff = uncompile(&fontref, true).unwrap();
1889        assert_eq!(
1890            ff.as_fea(""),
1891            "\
1892languagesystem DFLT dflt;
1893lookup gsub_alternate_1 {
1894    sub a from [b c];
1895} gsub_alternate_1;
1896feature cv01 {
1897    cvParameters {
1898                FeatUILabelNameID {
1899            name \"Alternate a\";
1900        };
1901
1902                FeatUITooltipTextNameID {
1903            name \"Alternate forms of a\";
1904        };
1905
1906                SampleTextNameID {
1907            name \"aaa\";
1908        };
1909
1910                ParamUILabelNameID {
1911            name \"Round\";
1912        };
1913
1914                ParamUILabelNameID {
1915            name \"Square\";
1916        };
1917
1918        Character 0x61;
1919    };
1920
1921    lookup gsub_alternate_1;
1922} cv01;
1923"
1924        );
1925    }
1926
1927    #[test]
1928    fn test_uncompile_empty_avar_map() {
1929        let data = std::fs::read("resources/emptyavar.ttf").unwrap();
1930        let fontref = skrifa::FontRef::new(&data).unwrap();
1931        let ff = uncompile(&fontref, true).unwrap();
1932        assert_eq!(
1933            ff.as_fea(""),
1934            "\
1935languagesystem DFLT dflt;
1936lookup gpos_single_1 {
1937    pos a (wght=100:-10 wght=400:0 wght=900:20);
1938} gpos_single_1;
1939feature kern {
1940lookup gpos_single_1;
1941} kern;
1942"
1943        );
1944    }
1945
1946    #[test]
1947    fn test_uncompile_null_anchors() {
1948        let data = std::fs::read("resources/nullanchors.ttf").unwrap();
1949        let fontref = skrifa::FontRef::new(&data).unwrap();
1950        let ff = uncompile(&fontref, true).unwrap();
1951        assert_eq!(
1952            ff.as_fea(""),
1953            "\
1954languagesystem DFLT dflt;
1955table GDEF {
1956GlyphClassDef [b], [a], [c d], [];
1957} GDEF;
1958
1959markClass c <anchor 0 500> @left;
1960markClass c <anchor 0 500> @mark_class_0x1;
1961markClass d <anchor 0 0> @mark_class_1x1;
1962markClass c <anchor 0 500> @topleft;
1963lookup gpos_mark_to_base_1 {
1964    pos base a
1965        <anchor 250 500> mark @left;
1966    pos base b
1967        <anchor NULL> mark @left;
1968} gpos_mark_to_base_1;
1969lookup gpos_mark_to_ligature_1 {
1970    pos ligature a
1971        <anchor 100 500> mark @mark_class_0x1
1972        <anchor NULL> mark @mark_class_1x1
1973        ligComponent
1974        <anchor 400 500> mark @mark_class_0x1
1975        <anchor NULL> mark @mark_class_1x1;
1976} gpos_mark_to_ligature_1;
1977lookup gpos_mark_to_mark_1 {
1978    pos mark c
1979        <anchor 0 800> mark @topleft;
1980    pos mark d
1981        <anchor NULL> mark @topleft;
1982} gpos_mark_to_mark_1;
1983feature mark {
1984lookup gpos_mark_to_base_1;
1985    lookup gpos_mark_to_ligature_1;
1986} mark;
1987feature mkmk {
1988lookup gpos_mark_to_mark_1;
1989} mkmk;
1990"
1991        );
1992    }
1993
1994    #[test]
1995    fn test_uncompile_extension_lookups() {
1996        let data = std::fs::read("resources/extension.ttf").unwrap();
1997        let fontref = skrifa::FontRef::new(&data).unwrap();
1998        let ff = uncompile(&fontref, true).unwrap();
1999        assert_eq!(
2000            ff.as_fea(""),
2001            "\
2002languagesystem DFLT dflt;
2003lookup gsub_single_1 useExtension {
2004    sub a by b;
2005} gsub_single_1;
2006lookup gsub_single_2 {
2007    sub c by d;
2008} gsub_single_2;
2009lookup gpos_pair_1 useExtension {
2010    pos a b -10;
2011} gpos_pair_1;
2012feature ss01 {
2013lookup gsub_single_1;
2014    lookup gsub_single_2;
2015} ss01;
2016feature kern {
2017lookup gpos_pair_1;
2018} kern;
2019"
2020        );
2021    }
2022
2023    #[test]
2024    fn test_uncompile_variation_scripts() {
2025        let data = std::fs::read("resources/variationscripts.ttf").unwrap();
2026        let fontref = skrifa::FontRef::new(&data).unwrap();
2027        let ff = uncompile(&fontref, true).unwrap();
2028        assert_eq!(
2029            ff.as_fea(""),
2030            "\
2031languagesystem DFLT dflt;
2032languagesystem latn dflt;
2033languagesystem grek dflt;
2034lookup gsub_single_1 {
2035    sub space by exclam;
2036} gsub_single_1;
2037lookup gpos_pair_1 {
2038    pos space exclam -10;
2039} gpos_pair_1;
2040feature rvrn {
2041script DFLT;
2042    script latn;
2043} rvrn;
2044feature kern {
2045lookup gpos_pair_1;
2046} kern;
2047conditionset condition_1 {
2048\twght 600 900;
2049} condition_1;
2050
2051variation rvrn condition_1 {
2052script DFLT;
2053lookup gsub_single_1;
2054script latn;
2055lookup gsub_single_1;
2056} rvrn;
2057
2058"
2059        );
2060    }
2061}