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::{
36    Anchor, GlyphClass, GlyphClassDefStatement, GlyphClassDefinition, GlyphContainer, GlyphName,
37    LanguageSystemStatement, LookupBlock, LookupFlagStatement, LookupReferenceStatement, MarkClass,
38    MarkClassDefinition, Pos, Statement, SubOrPos, Subst, ToplevelItem,
39};
40use indexmap::{IndexMap, IndexSet};
41/// A handle to the version of Skrifa that sr-eaf is using. Pass a skrifa::FontRef to uncompile()
42pub use skrifa;
43use skrifa::{
44    metrics::GlyphMetrics,
45    prelude::{LocationRef, Size},
46    raw::{
47        tables::{
48            gdef::Gdef,
49            gpos::Gpos,
50            gsub::{ClassDef, Gsub},
51            layout::{CoverageTable, LookupFlag},
52        },
53        ReadError, TableProvider,
54    },
55    GlyphId, GlyphId16, GlyphNames, Tag,
56};
57use smol_str::SmolStr;
58
59mod contextual;
60mod gpos;
61mod gsub;
62#[cfg(feature = "cli")]
63mod serialize;
64mod variations;
65
66pub(crate) type SimpleUserLocation = IndexMap<SmolStr, i16>; // as used by fea-rs-ast metrics
67
68const PROMOTE_TO_NAMED_CLASS_THRESHOLD: usize = 5;
69
70/// The context object that holds all the information we need when uncompiling a font.
71#[cfg_attr(feature = "cli", derive(serde::Serialize))]
72pub struct UncompileContext<'a> {
73    /// All the lookups we have uncompiled so far, keyed by their name.
74    pub lookups: IndexMap<SmolStr, LookupBlock>,
75    /// 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.
76    #[cfg_attr(
77        feature = "cli",
78        serde(serialize_with = "crate::serialize::serialize_lookup_map")
79    )]
80    pub lookup_map: HashMap<(String, u16), SmolStr>,
81    /// A mapping from script tags to the language system tags that are present in the font.
82    #[cfg_attr(
83        feature = "cli",
84        serde(serialize_with = "crate::serialize::serialize_language_systems")
85    )]
86    pub language_systems: IndexMap<Tag, IndexSet<Tag>>,
87    /// Anchors on a glyph which we haven't worked out what they should be called.
88    /// class -> glyphname -> anchor
89    pub unnamed_anchors: IndexMap<SmolStr, Vec<Anchor>>,
90    /// Anchors on a glyph which we have worked out what they should be called.
91    pub anchors: IndexMap<SmolStr, IndexMap<SmolStr, Anchor>>,
92    /// Mark classes, indexed by class name.
93    pub mark_classes: IndexMap<SmolStr, Vec<MarkClassDefinition>>,
94    /// Named glyph classes, indexed by class name.
95    #[cfg_attr(
96        feature = "cli",
97        serde(serialize_with = "crate::serialize::serialize_named_classes")
98    )]
99    pub named_classes: IndexMap<SmolStr, GlyphClass>,
100    /// Features, indexed by feature name.
101    #[cfg_attr(
102        feature = "cli",
103        serde(serialize_with = "crate::serialize::serialize_features")
104    )]
105    pub features: IndexMap<SmolStr, Vec<LookupReferenceStatement>>,
106    #[cfg_attr(feature = "cli", serde(skip))]
107    symbols: IndexMap<SmolStr, usize>,
108    #[cfg_attr(feature = "cli", serde(skip))]
109    gpos: Option<Gpos<'a>>,
110    #[cfg_attr(feature = "cli", serde(skip))]
111    gsub: Option<Gsub<'a>>,
112    #[cfg_attr(feature = "cli", serde(skip))]
113    gdef: Option<Gdef<'a>>,
114    #[cfg_attr(feature = "cli", serde(skip))]
115    glyph_metrics: GlyphMetrics<'a>,
116    #[cfg_attr(feature = "cli", serde(skip))]
117    glyph_id_to_name: HashMap<GlyphId, SmolStr>,
118    #[cfg_attr(feature = "cli", serde(skip))]
119    glyph_name_to_id: HashMap<SmolStr, GlyphId>,
120    #[cfg_attr(feature = "cli", serde(skip))]
121    axis_tags: Vec<Tag>,
122    #[cfg_attr(feature = "cli", serde(skip))]
123    axes: Option<fontdrasil::types::Axes>,
124    num_glyphs: u16,
125}
126
127impl<'a> UncompileContext<'a> {
128    fn new(font: &'a skrifa::FontRef) -> Result<Self, ReadError> {
129        let glyph_names = GlyphNames::new(font);
130        let default = LocationRef::default();
131        let glyph_metrics = GlyphMetrics::new(font, Size::unscaled(), default);
132        let glyph_id_to_name: HashMap<GlyphId, SmolStr> = (0..glyph_names.num_glyphs())
133            .map(GlyphId::new)
134            .map(|gid| {
135                (
136                    gid,
137                    SmolStr::new(
138                        glyph_names
139                            .get(gid)
140                            .map(|s| s.to_string())
141                            .unwrap_or_else(|| format!("gid{:04}", gid)),
142                    ),
143                )
144            })
145            .collect();
146        let glyph_name_to_id = glyph_id_to_name
147            .iter()
148            .map(|(gid, name): (&GlyphId, &SmolStr)| (name.clone(), *gid))
149            .collect();
150        let mut slf = Self {
151            lookups: IndexMap::new(),
152            lookup_map: HashMap::new(),
153            symbols: IndexMap::new(),
154            gpos: font.gpos().ok(),
155            gsub: font.gsub().ok(),
156            gdef: font.gdef().ok(),
157            language_systems: IndexMap::new(),
158            unnamed_anchors: IndexMap::new(),
159            anchors: IndexMap::new(),
160            mark_classes: IndexMap::new(),
161            named_classes: IndexMap::new(),
162            features: IndexMap::new(),
163            glyph_metrics,
164            glyph_id_to_name,
165            glyph_name_to_id,
166            axis_tags: font
167                .fvar()
168                .ok()
169                .and_then(|fvar| fvar.axes().ok())
170                .map(|axes| axes.iter().map(|axis| axis.axis_tag()).collect())
171                .unwrap_or_default(),
172            axes: variations::fontdrasil_axes(font)?,
173            num_glyphs: glyph_names.num_glyphs() as u16,
174        };
175        slf.gather_language_systems()?;
176        slf.uncompile_gsub_lookups()?;
177        slf.uncompile_gpos_lookups()?;
178        slf.uncompile_feature_table()?;
179        Ok(slf)
180    }
181
182    fn register_anchor(&mut self, glyphname: &SmolStr, anchor: &Anchor, anchor_name: Option<&str>) {
183        if let Some(anchor_name) = anchor_name {
184            self.anchors
185                .entry(anchor_name.to_string().into())
186                .or_default()
187                .insert(glyphname.clone(), anchor.clone());
188        } else {
189            self.unnamed_anchors
190                .entry(glyphname.clone())
191                .or_default()
192                .push(anchor.clone());
193        }
194    }
195
196    fn register_mark_class(
197        &mut self,
198        anchors: Vec<(GlyphContainer, Anchor)>,
199        class_name: &SmolStr,
200    ) {
201        let definitions = anchors
202            .iter()
203            .map(|(members, anchor)| {
204                MarkClassDefinition::new(
205                    MarkClass::new(class_name),
206                    anchor.clone(),
207                    members.clone(),
208                )
209            })
210            .collect();
211        self.mark_classes.insert(class_name.clone(), definitions);
212    }
213
214    fn get_name(&self, id: GlyphId16) -> GlyphName {
215        let str: SmolStr = self
216            .glyph_id_to_name
217            .get(&GlyphId::new(id.to_u32()))
218            .cloned()
219            .unwrap_or_else(|| format!("gid{:04}", id.to_u32()).into());
220        GlyphName::new(&str)
221    }
222
223    fn gather_language_systems(&mut self) -> Result<(), ReadError> {
224        let mut systems = IndexMap::new();
225        for script_list in [
226            self.gsub.as_ref().and_then(|g| g.script_list().ok()),
227            self.gpos.as_ref().and_then(|g| g.script_list().ok()),
228        ]
229        .into_iter()
230        .flatten()
231        {
232            for script_record in script_list.script_records() {
233                let script_tag = script_record.script_tag();
234                let languages: &mut IndexSet<Tag> = systems.entry(script_tag).or_default();
235                let script = script_record.script(script_list.offset_data())?;
236                if script.default_lang_sys().is_some() {
237                    languages.insert(Tag::new(b"dflt"));
238                }
239                for lang_sys in script.lang_sys_records() {
240                    let lang_sys_tag = lang_sys.lang_sys_tag();
241                    languages.insert(lang_sys_tag);
242                }
243            }
244        }
245        self.language_systems = systems;
246        Ok(())
247    }
248
249    fn dump_language_systems(&self) -> Vec<ToplevelItem> {
250        let mut items = vec![];
251        for (script_tag, lang_sys_tags) in &self.language_systems {
252            for lang_sys_tag in lang_sys_tags {
253                let lss =
254                    LanguageSystemStatement::new(script_tag.to_string(), lang_sys_tag.to_string());
255                items.push(ToplevelItem::LanguageSystem(lss));
256            }
257        }
258        items
259    }
260
261    fn resolve_coverage(&self, coverage: &CoverageTable) -> Vec<GlyphContainer> {
262        coverage
263            .iter()
264            .map(|g| GlyphContainer::GlyphName(self.get_name(g)))
265            .collect()
266    }
267    fn resolve_coverage_to_class(&mut self, coverage: &CoverageTable) -> GlyphContainer {
268        let glyphs = self.resolve_coverage(coverage);
269        if glyphs.len() == 1 {
270            return glyphs.into_iter().next().unwrap();
271        }
272        let glyphclass = GlyphClass::new(glyphs.clone(), 0..0);
273
274        if glyphs.len() >= PROMOTE_TO_NAMED_CLASS_THRESHOLD {
275            // Have we seen this exact set of glyphs before (in order)? If so, reuse the same class. Otherwise, make a new one.
276            if let Some(_class_name) = self
277                .named_classes
278                .iter()
279                .find(|(_, class)| class.glyphs == glyphclass.glyphs)
280                .map(|(name, _)| name)
281            {
282                GlyphContainer::GlyphClass(GlyphClass::new(glyphclass.glyphs.clone(), 0..0))
283            } else {
284                let class_name = format!("class_{}", self.named_classes.len());
285                self.named_classes
286                    .insert(class_name.clone().into(), glyphclass.clone());
287                GlyphContainer::GlyphClass(GlyphClass::new(glyphclass.glyphs.clone(), 0..0))
288            }
289        } else {
290            GlyphContainer::GlyphClass(glyphclass)
291        }
292    }
293
294    fn resolve_classes(&self, class_def: &ClassDef) -> HashMap<u16, Vec<GlyphContainer>> {
295        let mut classes: HashMap<u16, Vec<GlyphContainer>> = HashMap::new();
296        let mut used_glyphs = HashSet::new();
297        for (glyph_id, class_id) in class_def.iter() {
298            used_glyphs.insert(glyph_id.to_u16());
299            classes
300                .entry(class_id)
301                .or_default()
302                .push(GlyphContainer::GlyphName(self.get_name(glyph_id)));
303        }
304
305        // Class 0 is all glyphs that are not explicitly assigned by the ClassDef.
306        for gid in 0..self.num_glyphs {
307            if !used_glyphs.contains(&gid) {
308                classes
309                    .entry(0)
310                    .or_default()
311                    .push(GlyphContainer::GlyphName(
312                        self.get_name(GlyphId16::new(gid)),
313                    ));
314            }
315        }
316        classes
317    }
318
319    pub(crate) fn gensym(&mut self, prefix: &str) -> SmolStr {
320        let symbol_index = self.symbols.entry(prefix.into()).or_insert(1);
321        let name = SmolStr::new(format!("{}_{}", prefix, symbol_index));
322        *symbol_index += 1;
323        name
324    }
325
326    fn create_next_lookup_block<T: SubOrPos>(
327        &mut self,
328        prefix: &str,
329        index: u16,
330        phase: T,
331    ) -> LookupBlock {
332        let name = self.gensym(prefix);
333        self.lookup_map
334            .insert((phase.to_string(), index), name.clone());
335        LookupBlock::new(name.clone(), vec![], false, 0..0)
336    }
337
338    fn get_lookup_name<T: SubOrPos>(&self, lookup_list_index: u16, phase: T) -> SmolStr {
339        self.lookup_map
340            .get(&(phase.to_string(), lookup_list_index))
341            .cloned()
342            .unwrap_or_else(|| format!("{}_lookup_{}", phase, lookup_list_index).into())
343    }
344
345    fn uncompile_gdef(&mut self) -> Result<Vec<ToplevelItem>, ReadError> {
346        let mut items = vec![];
347        let mut base_glyphs = vec![];
348        let mut mark_glyphs = vec![];
349        let mut ligature_glyphs = vec![];
350        let mut component_glyphs = vec![];
351        let make_class =
352            |v: Vec<GlyphContainer>| Some(GlyphContainer::GlyphClass(GlyphClass::new(v, 0..0)));
353        if let Some(gdef) = &self.gdef {
354            // Uncompile glyph categories
355            if let Some(Ok(glyph_class_def)) = gdef.glyph_class_def() {
356                for (gid, class) in glyph_class_def.iter() {
357                    let name = self.get_name(gid);
358                    match class {
359                        1 => base_glyphs.push(GlyphContainer::GlyphName(name)),
360                        2 => ligature_glyphs.push(GlyphContainer::GlyphName(name)),
361                        3 => mark_glyphs.push(GlyphContainer::GlyphName(name)),
362                        4 => component_glyphs.push(GlyphContainer::GlyphName(name)),
363                        _ => {}
364                    }
365                }
366                items.push(ToplevelItem::GdefClassDef(GlyphClassDefStatement::new(
367                    make_class(base_glyphs),
368                    make_class(ligature_glyphs),
369                    make_class(mark_glyphs),
370                    make_class(component_glyphs),
371                    0..0,
372                )));
373            }
374        }
375        Ok(items)
376    }
377
378    fn uncompile_feature_table(&mut self) -> Result<(), ReadError> {
379        if let Some(gsub) = &self.gsub {
380            for feature_record in gsub.feature_list()?.feature_records() {
381                let feature_tag = feature_record.feature_tag();
382                let feature = feature_record.feature(gsub.feature_list()?.offset_data())?;
383                let lookup_indices = feature.lookup_list_indices();
384                self.features.insert(
385                    feature_tag.to_string().into(),
386                    lookup_indices
387                        .iter()
388                        .map(|i| {
389                            LookupReferenceStatement::new(
390                                self.get_lookup_name(i.get(), Subst).into(),
391                                0..0,
392                            )
393                        })
394                        .collect(),
395                );
396            }
397        }
398
399        if let Some(gpos) = &self.gpos {
400            for feature_record in gpos.feature_list()?.feature_records() {
401                let feature_tag = feature_record.feature_tag();
402                let feature = feature_record.feature(gpos.feature_list()?.offset_data())?;
403                let lookup_indices = feature.lookup_list_indices();
404                self.features.insert(
405                    feature_tag.to_string().into(),
406                    lookup_indices
407                        .iter()
408                        .map(|i| {
409                            LookupReferenceStatement::new(
410                                self.get_lookup_name(i.get(), Pos).into(),
411                                0..0,
412                            )
413                        })
414                        .collect(),
415                );
416            }
417        }
418
419        Ok(())
420    }
421
422    fn add_lookup_flags(
423        &mut self,
424        lookupblock: &mut LookupBlock,
425        flags: LookupFlag,
426        mark_filtering_set: Option<u16>,
427    ) {
428        if flags == LookupFlag::empty() {
429            return;
430        }
431        let mark_glyph_sets = self.gdef.as_ref().and_then(|x| {
432            let mark_glyph_sets = x.mark_glyph_sets_def();
433            if let Some(Ok(mark_glyph_sets)) = mark_glyph_sets {
434                Some(mark_glyph_sets)
435            } else {
436                None
437            }
438        });
439        let mark_attachment_classes = self.gdef.as_ref().and_then(|x| {
440            let mark_attachment_classes = x.mark_attach_class_def();
441            if let Some(Ok(mark_attachment_classes)) = mark_attachment_classes {
442                Some(mark_attachment_classes)
443            } else {
444                None
445            }
446        });
447        let set = mark_filtering_set.and_then(|set| {
448            mark_glyph_sets
449                .and_then(|mgss| mgss.coverages().get(set as usize).ok())
450                .map(|coverage| self.resolve_coverage_to_class(&coverage))
451        });
452        let mark_attachment_class = flags.mark_attachment_class().and_then(|class| {
453            mark_attachment_classes
454                .and_then(|mac| self.resolve_classes(&mac).get(&class).cloned())
455                .map(|classes| GlyphContainer::GlyphClass(GlyphClass::new(classes, 0..0)))
456        });
457
458        lookupblock.statements.insert(
459            0,
460            Statement::LookupFlag(LookupFlagStatement::new(
461                flags.to_bits(),
462                mark_attachment_class,
463                set,
464                0..0,
465            )),
466        );
467    }
468}
469
470/// Uncompile a TTF font into a fea file.
471///
472/// If do_gdef is true, also uncompile the GDEF table and include it in the output.
473/// Returns a [fea_rs_ast::FeatureFile] representing the uncompiled font, or a ReadError if something went wrong during reading.
474pub fn uncompile(
475    font: &skrifa::FontRef,
476    do_gdef: bool,
477) -> Result<fea_rs_ast::FeatureFile, ReadError> {
478    let mut context = UncompileContext::new(font)?;
479
480    let mut ff = fea_rs_ast::FeatureFile::new(vec![]);
481    ff.statements.extend(context.dump_language_systems());
482
483    if do_gdef {
484        ff.statements.extend(context.uncompile_gdef()?);
485    }
486
487    // Add mark classes to the feature file
488    for definitions in context.mark_classes.values() {
489        for definition in definitions {
490            ff.statements
491                .push(ToplevelItem::MarkClassDefinition(definition.clone()));
492        }
493    }
494    // Add named class definitions
495    for (name, contents) in context.named_classes.iter() {
496        ff.statements.push(ToplevelItem::GlyphClassDefinition(
497            GlyphClassDefinition::new(name.to_string(), contents.clone(), 0..0),
498        ));
499    }
500    // Add all lookups to the feature file
501    for lookup in context.lookups.values() {
502        ff.statements.push(ToplevelItem::Lookup(lookup.clone()));
503    }
504    // Add all feature references to the feature file
505    for (feature_name, lookup_refs) in context.features.iter() {
506        ff.statements
507            .push(ToplevelItem::Feature(fea_rs_ast::FeatureBlock::new(
508                feature_name.clone(),
509                lookup_refs
510                    .iter()
511                    .map(|lr| Statement::LookupReference(lr.clone()))
512                    .collect(),
513                false,
514                0..0,
515            )));
516    }
517
518    Ok(ff)
519}
520
521/// Uncompile a TTF font from a byte slice into a fea file. See uncompile() for details.
522pub fn uncompile_bytes(
523    font_data: &[u8],
524    do_gdef: bool,
525) -> Result<fea_rs_ast::FeatureFile, ReadError> {
526    let fontref = skrifa::FontRef::new(font_data)?;
527    uncompile(&fontref, do_gdef)
528}
529
530/// Uncompile a TTF font to a context object.
531///
532/// This partially decompiles the font, giving you the component parts so that you can
533/// put them where you want them. Useful for font editors and other tools that want the
534/// data but don't want to go all the way to a fea file.
535pub fn uncompile_context<'a>(font: &'a skrifa::FontRef) -> Result<UncompileContext<'a>, ReadError> {
536    UncompileContext::new(font)
537}
538
539#[cfg(test)]
540mod tests {
541    use fea_rs_ast::AsFea;
542
543    use super::*;
544    #[test]
545    fn test_uncompile_static() {
546        let data = std::fs::read("resources/test.ttf").unwrap();
547        let fontref = skrifa::FontRef::new(&data).unwrap();
548        let ff = uncompile(&fontref, true).unwrap();
549        assert_eq!(
550            ff.as_fea(""),
551            "GlyphClassDef [A], [], [grave acute dotbelowcomb], [];\nmarkClass grave <anchor 200 150> @bottomleft;\nmarkClass acute <anchor 350 0> @bottomleft;\nmarkClass dotbelowcomb <anchor 200 -200> @mark_class_1_1;\nlookup gsub_single_1 {\n    sub a by b;\n} gsub_single_1;\nlookup gsub_multiple_1 {\n    sub a by b c;\n} gsub_multiple_1;\nlookup gsub_alternate_1 {\n    sub a from [b c d e f];\n} gsub_alternate_1;\nlookup gsub_ligature_1 {\n    sub b c by a;\n} gsub_ligature_1;\nlookup gsub_contextual_1 {\n    sub [one a]' lookup gsub_single_1 b' [two c]' lookup gsub_multiple_1;\n} gsub_contextual_1;\nlookup gsub_chain_contextual_1 {\n    sub one two three a' lookup gsub_single_1 b' c' lookup gsub_multiple_1 x y z;\n} gsub_chain_contextual_1;\nlookup gpos_mark_to_base_1 {\n    pos base A\n        <anchor 150 100> mark @bottomleft\n        <anchor -200 -200> mark @mark_class_1_1;\n} gpos_mark_to_base_1;\n"
552        );
553    }
554}