fea_rs/compile/
output.rs

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