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