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