1use std::collections::{HashMap, HashSet};
30
31pub 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};
41pub 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>; const PROMOTE_TO_NAMED_CLASS_THRESHOLD: usize = 5;
69
70#[cfg_attr(feature = "cli", derive(serde::Serialize))]
72pub struct UncompileContext<'a> {
73 pub lookups: IndexMap<SmolStr, LookupBlock>,
75 #[cfg_attr(
77 feature = "cli",
78 serde(serialize_with = "crate::serialize::serialize_lookup_map")
79 )]
80 pub lookup_map: HashMap<(String, u16), SmolStr>,
81 #[cfg_attr(
83 feature = "cli",
84 serde(serialize_with = "crate::serialize::serialize_language_systems")
85 )]
86 pub language_systems: IndexMap<Tag, IndexSet<Tag>>,
87 pub unnamed_anchors: IndexMap<SmolStr, Vec<Anchor>>,
90 pub anchors: IndexMap<SmolStr, IndexMap<SmolStr, Anchor>>,
92 pub mark_classes: IndexMap<SmolStr, Vec<MarkClassDefinition>>,
94 #[cfg_attr(
96 feature = "cli",
97 serde(serialize_with = "crate::serialize::serialize_named_classes")
98 )]
99 pub named_classes: IndexMap<SmolStr, GlyphClass>,
100 #[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 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 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 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
470pub 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 for definitions in context.mark_classes.values() {
489 for definition in definitions {
490 ff.statements
491 .push(ToplevelItem::MarkClassDefinition(definition.clone()));
492 }
493 }
494 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 for lookup in context.lookups.values() {
502 ff.statements.push(ToplevelItem::Lookup(lookup.clone()));
503 }
504 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
521pub 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
530pub 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}