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