fea_rs/compile/
output.rs

1//! The result of a compilation
2
3use std::collections::HashMap;
4
5use write_fonts::{
6    tables::{
7        self as wtables, gdef::GlyphClassDef, layout::FeatureParams, maxp::Maxp, stat::AxisValue,
8    },
9    types::{GlyphId16, NameId},
10    BuilderError, FontBuilder,
11};
12
13use super::Opts;
14
15use crate::GlyphMap;
16
17/// The tables generated by this compilation.
18///
19/// All tables are optional, and the set of tables that are present depends
20/// on the input file.
21///
22/// Each table is a type defined in the [`write-fonts`][] crate. The caller
23/// may either interact with these directly, or else they may use the [`to_binary`]
24/// method to generate a binary font.
25///
26/// [`to_binary`]: Compilation::to_binary
27pub struct Compilation {
28    /// The options passed in for this compilation.
29    pub(crate) opts: Opts,
30    /// The `head` table, if one was generated
31    pub head: Option<wtables::head::Head>,
32    /// The `hhea` table, if one was generated
33    pub hhea: Option<wtables::hhea::Hhea>,
34    /// The `vhea` table, if one was generated
35    pub vhea: Option<wtables::vhea::Vhea>,
36    /// The `OS/2` table, if one was generated
37    pub os2: Option<wtables::os2::Os2>,
38    /// The `GDEF` table, if one was generated
39    pub gdef: Option<wtables::gdef::Gdef>,
40    /// The `BASE` table, if one was generated
41    pub base: Option<wtables::base::Base>,
42    /// The `name` table, if one was generated
43    pub name: Option<wtables::name::Name>,
44    /// The `STAT` table, if one was generated
45    pub stat: Option<wtables::stat::Stat>,
46    /// The `GSUB` table, if one was generated
47    pub gsub: Option<wtables::gsub::Gsub>,
48    /// The `GPOS` table, if one was generated
49    pub gpos: Option<wtables::gpos::Gpos>,
50    /// Any *explicit* gdef classes declared in the FEA.
51    ///
52    /// This is provided so that the user can reference them if they are going
53    /// to manually generate kerning or markpos lookups.
54    pub gdef_classes: Option<HashMap<GlyphId16, GlyphClassDef>>,
55}
56
57impl Compilation {
58    /// Returns `true` if the FEA generated tables other than GSUB, GPOS & GDEF.
59    pub fn has_non_layout_tables(&self) -> bool {
60        self.head.is_some()
61            || self.hhea.is_some()
62            || self.vhea.is_some()
63            || self.os2.is_some()
64            || self.base.is_some()
65            || self.name.is_some()
66            || self.stat.is_some()
67    }
68
69    /// Remap any `NameId`s in the name table and anywhere they are referenced.
70    ///
71    /// This is used for merging the results of our compilation with other
72    /// compilation operations which may have occured elsewhere, and which may
73    /// have used the same `NameId`s as us for different strings.
74    ///
75    /// We will take all the name ids we have declared that are >= 256 and offset
76    /// them to start at `first_avail_id`.
77    pub fn remap_name_ids(&mut self, first_avail_id: u16) {
78        let id_offset = first_avail_id.saturating_sub(NameId::LAST_RESERVED_NAME_ID.to_u16() + 1);
79        log::info!("remapping FEA name ideas with delta {id_offset}");
80        if id_offset == 0 {
81            return;
82        }
83
84        let adjust_id = |id: NameId| {
85            if !id.is_reserved() {
86                // in the case of a degenerate name table we'll just reuse the last id?
87                // entries will be pruned later.
88                id.checked_add(id_offset)
89                    .unwrap_or(NameId::LAST_ALLOWED_NAME_ID)
90            } else {
91                id
92            }
93        };
94
95        if let Some(name) = self.name.as_mut() {
96            let records = std::mem::take(&mut name.name_record);
97            name.name_record = records
98                .into_iter()
99                .map(|mut rec| {
100                    rec.name_id = adjust_id(rec.name_id);
101                    rec
102                })
103                .collect();
104        }
105
106        if let Some(gsub) = self.gsub.as_mut() {
107            gsub.feature_list
108                .as_mut()
109                .feature_records
110                .iter_mut()
111                .for_each(|rec| match rec.feature.as_mut().feature_params.as_mut() {
112                    Some(FeatureParams::StylisticSet(params)) => {
113                        params.ui_name_id = adjust_id(params.ui_name_id);
114                    }
115                    Some(FeatureParams::CharacterVariant(params)) => {
116                        params.feat_ui_label_name_id = adjust_id(params.feat_ui_label_name_id);
117                        params.feat_ui_tooltip_text_name_id =
118                            adjust_id(params.feat_ui_tooltip_text_name_id);
119                        params.sample_text_name_id = adjust_id(params.sample_text_name_id);
120                        params.first_param_ui_label_name_id =
121                            adjust_id(params.first_param_ui_label_name_id);
122                    }
123                    _ => (),
124                });
125        }
126        if let Some(stat) = self.stat.as_mut() {
127            stat.elided_fallback_name_id = stat
128                .elided_fallback_name_id
129                .map(|id| id.to_u16().saturating_add(id_offset).into());
130            stat.design_axes.iter_mut().for_each(|axe| {
131                axe.axis_name_id = adjust_id(axe.axis_name_id);
132            });
133            if let Some(blah) = stat.offset_to_axis_values.as_mut() {
134                blah.iter_mut().for_each(|val| match val.as_mut() {
135                    AxisValue::Format1(val) => val.value_name_id = adjust_id(val.value_name_id),
136                    AxisValue::Format2(val) => val.value_name_id = adjust_id(val.value_name_id),
137                    AxisValue::Format3(val) => val.value_name_id = adjust_id(val.value_name_id),
138                    AxisValue::Format4(val) => val.value_name_id = adjust_id(val.value_name_id),
139                });
140            }
141        }
142    }
143
144    /// Assemble the output tables into a `FontBuilder`.
145    ///
146    /// This is a convenience method. To compile a binary font you can use
147    /// [`to_binary`] instead, and for more fine-grained control you can inspect
148    /// and manipulate the raw tables directly.
149    ///
150    /// [`to_binary`]: Compilation::to_binary
151    pub fn to_font_builder(&self) -> Result<FontBuilder, BuilderError> {
152        let mut builder = FontBuilder::default();
153        macro_rules! add_if_some {
154            ($table:expr) => {
155                if let Some(table) = $table.as_ref() {
156                    builder.add_table(table)?;
157                }
158            };
159        }
160        add_if_some!(self.head);
161        add_if_some!(self.hhea);
162        add_if_some!(self.vhea);
163        add_if_some!(self.os2);
164        add_if_some!(self.gdef);
165        add_if_some!(self.base);
166        add_if_some!(self.name);
167        add_if_some!(self.stat);
168        add_if_some!(self.gsub);
169        add_if_some!(self.gpos);
170        Ok(builder)
171    }
172
173    /// Compile the output tables into a font.
174    ///
175    /// This is a convenience method used for things like testing; if you are
176    /// building a font compiler you will probably prefer to manipulate the
177    /// generated tables directly.
178    pub fn to_binary(&self, glyph_map: &GlyphMap) -> Result<Vec<u8>, BuilderError> {
179        // because we often inspect our output with ttx, and ttx fails if maxp is
180        // missing, we create a maxp table.
181        let mut builder = self.to_font_builder()?;
182        let maxp = Maxp::new(glyph_map.len().try_into().unwrap());
183        builder.add_table(&maxp)?;
184        if self.opts.make_post_table {
185            let post = glyph_map.make_post_table();
186            builder.add_table(&post)?;
187        }
188
189        Ok(builder.build())
190    }
191}