use std::collections::{HashMap, HashSet};
pub use fea_rs_ast;
use fea_rs_ast::ordered_float::OrderedFloat;
use fea_rs_ast::{
Anchor, AsFea, AttachStatement, CharacterStatement, Comment, ConditionSet, GdefStatement,
GlyphClass, GlyphClassDefStatement, GlyphClassDefinition, GlyphContainer, GlyphName,
LanguageStatement, LanguageSystemStatement, LigatureCaretByIndexStatement,
LigatureCaretByPosStatement, LookupBlock, LookupFlagStatement, LookupReferenceStatement,
MarkClass, MarkClassDefinition, Metric, NameRecord, NameRecordKind, NestedBlock, Pos,
ScriptStatement, SizeParameters, Statement, SubOrPos, Subst, SubtableStatement, Table,
ToplevelItem, VariationBlock,
};
use indexmap::{IndexMap, IndexSet};
pub use skrifa;
use skrifa::{
GlyphId, GlyphId16, GlyphNames, Tag,
metrics::GlyphMetrics,
prelude::{LocationRef, Size},
raw::{
ReadError, TableProvider,
tables::{
gdef::{CaretValue, Gdef},
gpos::Gpos,
gsub::{ClassDef, Gsub},
layout::{
CharacterVariantParams, Condition, CoverageTable, FeatureList, FeatureParams,
FeatureVariations, LookupFlag, ScriptList, SizeParams,
},
name::{Name, NameId},
},
},
};
use smol_str::SmolStr;
mod contextual;
mod gpos;
mod gsub;
#[cfg(feature = "cli")]
mod serialize;
mod variations;
pub(crate) type SimpleUserLocation = IndexMap<SmolStr, OrderedFloat<f64>>; type LanguageSystem = (Tag, Tag); type FeatureLanguageSystems = Vec<(LanguageSystem, bool)>;
const PROMOTE_TO_NAMED_CLASS_THRESHOLD: usize = 5;
#[cfg_attr(feature = "cli", derive(serde::Serialize))]
pub struct UncompileContext<'a> {
pub lookups: IndexMap<SmolStr, LookupBlock>,
#[cfg_attr(
feature = "cli",
serde(serialize_with = "crate::serialize::serialize_lookup_map")
)]
pub lookup_map: HashMap<(String, u16), SmolStr>,
#[cfg_attr(
feature = "cli",
serde(serialize_with = "crate::serialize::serialize_language_systems")
)]
pub language_systems: IndexMap<Tag, IndexSet<Tag>>,
pub unnamed_anchors: IndexMap<SmolStr, Vec<Anchor>>,
pub anchors: IndexMap<SmolStr, IndexMap<SmolStr, Anchor>>,
pub mark_classes: IndexMap<SmolStr, Vec<MarkClassDefinition>>,
#[cfg_attr(
feature = "cli",
serde(serialize_with = "crate::serialize::serialize_named_classes")
)]
pub named_classes: IndexMap<SmolStr, GlyphClass>,
#[cfg_attr(
feature = "cli",
serde(serialize_with = "crate::serialize::serialize_features")
)]
pub features: IndexMap<SmolStr, Vec<Statement>>,
pub feature_variations: Vec<(ConditionSet, Vec<VariationBlock>)>,
#[cfg_attr(feature = "cli", serde(skip))]
symbols: IndexMap<SmolStr, usize>,
#[cfg_attr(feature = "cli", serde(skip))]
gpos: Option<Gpos<'a>>,
#[cfg_attr(feature = "cli", serde(skip))]
gsub: Option<Gsub<'a>>,
#[cfg_attr(feature = "cli", serde(skip))]
gdef: Option<Gdef<'a>>,
#[cfg_attr(feature = "cli", serde(skip))]
name: Option<Name<'a>>,
#[cfg_attr(feature = "cli", serde(skip))]
glyph_metrics: GlyphMetrics<'a>,
#[cfg_attr(feature = "cli", serde(skip))]
glyph_id_to_name: HashMap<GlyphId, SmolStr>,
#[cfg_attr(feature = "cli", serde(skip))]
glyph_name_to_id: HashMap<SmolStr, GlyphId>,
#[cfg_attr(feature = "cli", serde(skip))]
axis_tags: Vec<Tag>,
#[cfg_attr(feature = "cli", serde(skip))]
axes: Option<fontdrasil::types::Axes>,
num_glyphs: u16,
aalt_lookups: IndexSet<SmolStr>,
}
impl<'a> UncompileContext<'a> {
fn new(font: &'a skrifa::FontRef) -> Result<Self, ReadError> {
let glyph_names = GlyphNames::new(font);
let default = LocationRef::default();
let glyph_metrics = GlyphMetrics::new(font, Size::unscaled(), default);
let glyph_id_to_name: HashMap<GlyphId, SmolStr> = unique_glyph_names(font)
.into_iter()
.enumerate()
.map(|(gid, name)| (GlyphId::new(gid as u32), name))
.collect();
let glyph_name_to_id = glyph_id_to_name
.iter()
.map(|(gid, name): (&GlyphId, &SmolStr)| (name.clone(), *gid))
.collect();
let mut slf = Self {
lookups: IndexMap::new(),
lookup_map: HashMap::new(),
symbols: IndexMap::new(),
gpos: font.gpos().ok(),
gsub: font.gsub().ok(),
gdef: font.gdef().ok(),
name: font.name().ok(),
language_systems: IndexMap::new(),
unnamed_anchors: IndexMap::new(),
anchors: IndexMap::new(),
mark_classes: IndexMap::new(),
named_classes: IndexMap::new(),
features: IndexMap::new(),
feature_variations: vec![],
glyph_metrics,
glyph_id_to_name,
glyph_name_to_id,
axis_tags: font
.fvar()
.ok()
.and_then(|fvar| fvar.axes().ok())
.map(|axes| axes.iter().map(|axis| axis.axis_tag()).collect())
.unwrap_or_default(),
axes: variations::fontdrasil_axes(font)?,
num_glyphs: glyph_names.num_glyphs() as u16,
aalt_lookups: IndexSet::new(),
};
slf.gather_language_systems()?;
slf.uncompile_gsub_lookups()?;
slf.uncompile_gpos_lookups()?;
slf.uncompile_feature_table()?;
slf.uncompile_feature_variations()?;
slf.order_lookups();
Ok(slf)
}
fn register_anchor(&mut self, glyphname: &SmolStr, anchor: &Anchor, anchor_name: Option<&str>) {
if let Some(anchor_name) = anchor_name {
self.anchors
.entry(anchor_name.to_string().into())
.or_default()
.insert(glyphname.clone(), anchor.clone());
} else {
self.unnamed_anchors
.entry(glyphname.clone())
.or_default()
.push(anchor.clone());
}
}
fn register_mark_class(
&mut self,
anchors: Vec<(GlyphContainer, Anchor)>,
class_name: &SmolStr,
) {
let definitions = anchors
.iter()
.map(|(members, anchor)| {
MarkClassDefinition::new(
MarkClass::new(class_name),
anchor.clone(),
members.clone(),
)
})
.collect();
self.mark_classes.insert(class_name.clone(), definitions);
}
fn get_name(&self, id: GlyphId16) -> GlyphName {
let str: SmolStr = self
.glyph_id_to_name
.get(&GlyphId::new(id.to_u32()))
.cloned()
.unwrap_or_else(|| format!("gid{:04}", id.to_u32()).into());
if str == "NULL" {
return GlyphName::new("\\NULL");
}
GlyphName::new(&str)
}
fn gather_language_systems(&mut self) -> Result<(), ReadError> {
let mut systems = IndexMap::new();
for script_list in [
self.gsub.as_ref().and_then(|g| g.script_list().ok()),
self.gpos.as_ref().and_then(|g| g.script_list().ok()),
]
.into_iter()
.flatten()
{
for script_record in script_list.script_records() {
let script_tag = script_record.script_tag();
let languages: &mut IndexSet<Tag> = systems.entry(script_tag).or_default();
let script = script_record.script(script_list.offset_data())?;
if script.default_lang_sys().is_some() {
languages.insert(Tag::new(b"dflt"));
}
for lang_sys in script.lang_sys_records() {
let lang_sys_tag = lang_sys.lang_sys_tag();
languages.insert(lang_sys_tag);
}
}
}
if let Some(index) = systems.get_index_of(&Tag::new(b"DFLT")) {
systems.move_index(index, 0);
let languages = &mut systems[0];
if let Some(index) = languages.get_index_of(&Tag::new(b"dflt")) {
languages.move_index(index, 0);
}
}
self.language_systems = systems;
Ok(())
}
fn dump_language_systems(&self) -> Vec<ToplevelItem> {
let mut items = vec![];
for (script_tag, lang_sys_tags) in &self.language_systems {
for lang_sys_tag in lang_sys_tags {
let lss =
LanguageSystemStatement::new(script_tag.to_string(), lang_sys_tag.to_string());
items.push(ToplevelItem::LanguageSystem(lss));
}
}
items
}
fn resolve_coverage(&self, coverage: &CoverageTable) -> Vec<GlyphContainer> {
coverage
.iter()
.map(|g| GlyphContainer::GlyphName(self.get_name(g)))
.collect()
}
fn resolve_coverage_to_class(
&mut self,
coverage: &CoverageTable,
collapse_single: bool,
) -> GlyphContainer {
let glyphs = self.resolve_coverage(coverage);
if collapse_single && glyphs.len() == 1 {
return glyphs.into_iter().next().unwrap();
}
let glyphclass = GlyphClass::new(glyphs.clone(), 0..0);
if glyphs.len() >= PROMOTE_TO_NAMED_CLASS_THRESHOLD {
if let Some(_class_name) = self
.named_classes
.iter()
.find(|(_, class)| class.glyphs == glyphclass.glyphs)
.map(|(name, _)| name)
{
GlyphContainer::GlyphClass(GlyphClass::new(glyphclass.glyphs.clone(), 0..0))
} else {
let class_name = format!("class_{}", self.named_classes.len());
self.named_classes
.insert(class_name.clone().into(), glyphclass.clone());
GlyphContainer::GlyphClass(GlyphClass::new(glyphclass.glyphs.clone(), 0..0))
}
} else {
GlyphContainer::GlyphClass(glyphclass)
}
}
fn resolve_classes(&self, class_def: Option<&ClassDef>) -> HashMap<u16, Vec<GlyphContainer>> {
let mut classes: HashMap<u16, Vec<GlyphContainer>> = HashMap::new();
let mut used_glyphs = HashSet::new();
for (glyph_id, class_id) in class_def.into_iter().flat_map(|class_def| class_def.iter()) {
used_glyphs.insert(glyph_id.to_u16());
classes
.entry(class_id)
.or_default()
.push(GlyphContainer::GlyphName(self.get_name(glyph_id)));
}
for gid in 0..self.num_glyphs {
if !used_glyphs.contains(&gid) {
classes
.entry(0)
.or_default()
.push(GlyphContainer::GlyphName(
self.get_name(GlyphId16::new(gid)),
));
}
}
classes
}
pub(crate) fn gensym(&mut self, prefix: &str) -> SmolStr {
let symbol_index = self.symbols.entry(prefix.into()).or_insert(1);
let name = SmolStr::new(format!("{}_{}", prefix, symbol_index));
*symbol_index += 1;
name
}
pub(crate) fn mark_class_symbol(&mut self, class_number: u16) -> SmolStr {
let prefix = SmolStr::new(format!("mark_class_{}x", class_number));
let symbol_index = self.symbols.entry(prefix.clone()).or_insert(1);
let name = SmolStr::new(format!("{}{}", prefix, symbol_index));
*symbol_index += 1;
name
}
fn assign_lookup_name<T: SubOrPos>(&mut self, prefix: &str, index: u16, phase: T) {
let name = self.gensym(prefix);
self.lookup_map.insert((phase.to_string(), index), name);
}
fn create_lookup_block<T: SubOrPos>(&self, index: u16, phase: T) -> LookupBlock {
LookupBlock::new(self.get_lookup_name(index, phase), vec![], false, 0..0)
}
fn get_lookup_name<T: SubOrPos>(&self, lookup_list_index: u16, phase: T) -> SmolStr {
self.lookup_map
.get(&(phase.to_string(), lookup_list_index))
.cloned()
.unwrap_or_else(|| format!("{}_lookup_{}", phase, lookup_list_index).into())
}
fn order_lookups(&mut self) {
let used_by_features: HashSet<SmolStr> = self
.features
.values()
.flatten()
.filter_map(|statement| match statement {
Statement::LookupReference(reference) => Some(SmolStr::new(&reference.lookup_name)),
_ => None,
})
.collect();
let calls: HashMap<SmolStr, Vec<SmolStr>> = self
.lookups
.iter()
.map(|(name, lookup)| (name.clone(), called_lookups(lookup)))
.collect();
let contextual_only: HashSet<SmolStr> = calls
.values()
.flatten()
.filter(|name| !used_by_features.contains(*name))
.cloned()
.collect();
let feature_dependencies: HashMap<SmolStr, Vec<SmolStr>> = calls
.keys()
.map(|name| {
(
name.clone(),
feature_dependencies(name, &calls, &contextual_only),
)
})
.collect();
let count_pending_dependencies =
|name: &SmolStr, pending: &IndexMap<SmolStr, LookupBlock>| {
feature_dependencies[name]
.iter()
.filter(|dependency| pending.contains_key(*dependency))
.count()
};
let mut pending = std::mem::take(&mut self.lookups);
let mut deferred: Vec<SmolStr> = vec![];
let names: Vec<SmolStr> = pending.keys().cloned().collect();
for name in names {
if !pending.contains_key(&name) {
continue;
}
let pending_dependencies = count_pending_dependencies(&name, &pending);
if contextual_only.contains(&name) {
if pending_dependencies == 0 {
self.place_lookup(&name, &mut pending, &calls);
}
continue;
}
if pending_dependencies > 1 {
deferred.push(name);
continue;
}
self.place_lookup(&name, &mut pending, &calls);
while let Some(index) = deferred
.iter()
.position(|name| count_pending_dependencies(name, &pending) == 0)
{
let name = deferred.remove(index);
self.place_lookup(&name, &mut pending, &calls);
}
}
let remaining: Vec<SmolStr> = pending.keys().cloned().collect();
for name in deferred.into_iter().chain(remaining) {
self.place_lookup(&name, &mut pending, &calls);
}
}
fn place_lookup(
&mut self,
name: &SmolStr,
pending: &mut IndexMap<SmolStr, LookupBlock>,
calls: &HashMap<SmolStr, Vec<SmolStr>>,
) {
let Some(lookup) = pending.shift_remove(name) else {
return;
};
for dependency in calls.get(name).into_iter().flatten() {
self.place_lookup(dependency, pending, calls);
}
self.lookups.insert(name.clone(), lookup);
}
fn uncompile_gdef(&mut self) -> Result<Vec<ToplevelItem>, ReadError> {
let mut statements = vec![];
let mut base_glyphs = vec![];
let mut mark_glyphs = vec![];
let mut ligature_glyphs = vec![];
let mut component_glyphs = vec![];
let make_class =
|v: Vec<GlyphContainer>| Some(GlyphContainer::GlyphClass(GlyphClass::new(v, 0..0)));
if let Some(gdef) = &self.gdef {
if let Some(Ok(glyph_class_def)) = gdef.glyph_class_def() {
for (gid, class) in glyph_class_def.iter() {
let name = self.get_name(gid);
match class {
1 => base_glyphs.push(GlyphContainer::GlyphName(name)),
2 => ligature_glyphs.push(GlyphContainer::GlyphName(name)),
3 => mark_glyphs.push(GlyphContainer::GlyphName(name)),
4 => component_glyphs.push(GlyphContainer::GlyphName(name)),
_ => {}
}
}
statements.push(GdefStatement::GlyphClassDef(GlyphClassDefStatement::new(
make_class(base_glyphs),
make_class(ligature_glyphs),
make_class(mark_glyphs),
make_class(component_glyphs),
0..0,
)));
}
if let Some(Ok(attach_list)) = gdef.attach_list() {
let coverage = attach_list.coverage()?;
for (gid, attach_point) in coverage.iter().zip(attach_list.attach_points().iter()) {
let point_indices: Vec<usize> = attach_point?
.point_indices()
.iter()
.map(|index| index.get() as usize)
.collect();
if !point_indices.is_empty() {
statements.push(GdefStatement::Attach(AttachStatement::new(
GlyphContainer::GlyphName(self.get_name(gid)),
point_indices,
0..0,
)));
}
}
}
if let Some(Ok(lig_caret_list)) = gdef.lig_caret_list() {
let coverage = lig_caret_list.coverage()?;
for (gid, lig_glyph) in coverage.iter().zip(lig_caret_list.lig_glyphs().iter()) {
let mut positions = vec![];
let mut point_indices = vec![];
for caret in lig_glyph?.caret_values().iter() {
match caret? {
CaretValue::Format1(caret) => {
positions.push(Metric::Scalar(caret.coordinate()))
}
CaretValue::Format2(caret) => {
point_indices.push(caret.caret_value_point_index() as usize)
}
CaretValue::Format3(caret) => {
positions.push(self.resolve_pos_with_variations(
caret.coordinate(),
Some(caret.device()),
)?)
}
}
}
let glyph = GlyphContainer::GlyphName(self.get_name(gid));
if !positions.is_empty() {
statements.push(GdefStatement::LigatureCaretByPos(
LigatureCaretByPosStatement::new(glyph.clone(), positions, 0..0),
));
}
if !point_indices.is_empty() {
statements.push(GdefStatement::LigatureCaretByIndex(
LigatureCaretByIndexStatement::new(glyph, point_indices, 0..0),
));
}
}
}
}
if statements.is_empty() {
return Ok(vec![]);
}
Ok(vec![ToplevelItem::Gdef(Table { statements })])
}
fn uncompile_feature_table(&mut self) -> Result<(), ReadError> {
let mut registered_features = vec![];
let mut unregistered_features = vec![];
let mut feature_name_ids = IndexMap::new();
let mut size_params = None;
let mut cv_params = IndexMap::new();
if let Some(feature_list) = self.gsub.as_ref().and_then(|gsub| gsub.feature_list().ok()) {
let systems =
feature_language_systems(self.gsub.as_ref().and_then(|g| g.script_list().ok()))?;
for (index, feature_record) in feature_list.feature_records().iter().enumerate() {
let feature_tag = feature_record.feature_tag();
let feature = feature_record.feature(feature_list.offset_data())?;
if let Some(Ok(FeatureParams::StylisticSet(params))) = feature.feature_params() {
feature_name_ids
.entry(feature_tag)
.or_insert(params.ui_name_id());
}
if let Some(Ok(FeatureParams::CharacterVariant(params))) = feature.feature_params()
{
cv_params.entry(feature_tag).or_insert(params);
}
let lookup_indices = feature.lookup_list_indices();
let lookups = lookup_indices
.iter()
.map(|i| lookup_reference(&self.get_lookup_name(i.get(), Subst)))
.collect();
if let Some(language_systems) = systems.get(&(index as u16)) {
registered_features.push((feature_tag, language_systems.clone(), lookups));
} else {
unregistered_features.push((feature_tag, lookups));
}
}
}
if let Some(feature_list) = self.gpos.as_ref().and_then(|gpos| gpos.feature_list().ok()) {
let systems =
feature_language_systems(self.gpos.as_ref().and_then(|g| g.script_list().ok()))?;
for (index, feature_record) in feature_list.feature_records().iter().enumerate() {
let feature_tag = feature_record.feature_tag();
let feature = feature_record.feature(feature_list.offset_data())?;
if let Some(Ok(FeatureParams::StylisticSet(params))) = feature.feature_params() {
feature_name_ids
.entry(feature_tag)
.or_insert(params.ui_name_id());
}
if let Some(Ok(FeatureParams::Size(params))) = feature.feature_params() {
size_params.get_or_insert(params);
}
let lookup_indices = feature.lookup_list_indices();
let lookups = lookup_indices
.iter()
.map(|i| lookup_reference(&self.get_lookup_name(i.get(), Pos)))
.collect();
if let Some(language_systems) = systems.get(&(index as u16)) {
registered_features.push((feature_tag, language_systems.clone(), lookups));
} else {
unregistered_features.push((feature_tag, lookups));
}
}
}
self.add_registered_features(registered_features);
for (feature_tag, name_id) in feature_name_ids {
let names = self.uncompile_feature_names("featureNames", name_id);
if let Some(statements) = self.features.get_mut(feature_tag.to_string().as_str()) {
statements.insert(0, names);
}
}
for (feature_tag, params) in cv_params {
let parameters = self.uncompile_cv_parameters(¶ms);
if let Some(statements) = self.features.get_mut(feature_tag.to_string().as_str()) {
statements.insert(0, parameters);
}
}
if let Some(params) = size_params {
let parameters = self.uncompile_size_parameters(¶ms);
if let Some(statements) = self.features.get_mut("size") {
statements.splice(0..0, parameters);
}
}
let referenced_lookups: Vec<Statement> = self
.features
.values()
.flatten()
.cloned()
.chain(
self.lookups
.values()
.flat_map(called_lookups)
.map(|name| lookup_reference(&name)),
)
.collect();
for (feature_tag, lookups) in unregistered_features {
let lookups = lookups
.into_iter()
.filter(|lookup| !referenced_lookups.contains(lookup))
.map(comment_out)
.collect();
self.add_feature_lookups(feature_tag, lookups);
}
Ok(())
}
fn uncompile_feature_variations(&mut self) -> Result<(), ReadError> {
if let Some((feature_list, feature_variations, script_list)) =
self.gsub.as_ref().and_then(|gsub| {
Some((
gsub.feature_list().ok()?,
gsub.feature_variations()?,
gsub.script_list().ok(),
))
})
{
self.add_feature_variations(feature_list, feature_variations?, script_list, Subst)?;
}
if let Some((feature_list, feature_variations, script_list)) =
self.gpos.as_ref().and_then(|gpos| {
Some((
gpos.feature_list().ok()?,
gpos.feature_variations()?,
gpos.script_list().ok(),
))
})
{
self.add_feature_variations(feature_list, feature_variations?, script_list, Pos)?;
}
Ok(())
}
fn add_feature_variations<T: SubOrPos + Copy>(
&mut self,
feature_list: FeatureList,
feature_variations: FeatureVariations,
script_list: Option<ScriptList>,
phase: T,
) -> Result<(), ReadError> {
let systems = feature_language_systems(script_list)?;
let data = feature_variations.offset_data();
for record in feature_variations.feature_variation_records() {
let (Some(condition_set), Some(substitution)) = (
record.condition_set(data),
record.feature_table_substitution(data),
) else {
continue;
};
let Some(conditions) = condition_set?
.conditions()
.iter()
.map(|condition| match condition {
Ok(Condition::Format1AxisRange(condition)) => {
self.resolve_condition(&condition)
}
_ => None,
})
.collect::<Option<Vec<_>>>()
else {
continue;
};
let name = self.gensym("condition").to_string();
let substitution = substitution?;
let mut registrations: IndexMap<Tag, IndexMap<LanguageSystem, (bool, Vec<Statement>)>> =
IndexMap::new();
for substitution_record in substitution.substitutions() {
let feature_index = substitution_record.feature_index();
let Some(feature_record) =
feature_list.feature_records().get(feature_index as usize)
else {
continue;
};
let feature = feature_record.feature(feature_list.offset_data())?;
let alternate =
substitution_record.alternate_feature(substitution.offset_data())?;
let Some(additional_lookups) = alternate
.lookup_list_indices()
.strip_prefix(feature.lookup_list_indices())
else {
continue;
};
let lookups: Vec<Statement> = additional_lookups
.iter()
.map(|index| lookup_reference(&self.get_lookup_name(index.get(), phase)))
.collect();
for (language_system, _) in systems.get(&feature_index).into_iter().flatten() {
let (_, registered_lookups) = registrations
.entry(feature_record.feature_tag())
.or_default()
.entry(*language_system)
.or_default();
for lookup in &lookups {
if !registered_lookups.contains(lookup) {
registered_lookups.push(lookup.clone());
}
}
}
}
let variations = registrations
.into_iter()
.map(|(feature_tag, systems)| {
let statements = self
.registration_statements(&systems)
.unwrap_or_else(|| systems.into_values().next().unwrap_or_default().1);
VariationBlock::new(
feature_tag.to_string().into(),
name.clone(),
statements,
false,
0..0,
)
})
.collect();
self.feature_variations
.push((ConditionSet::new(name, conditions, 0..0), variations));
}
Ok(())
}
fn add_registered_features(
&mut self,
features: Vec<(Tag, FeatureLanguageSystems, Vec<Statement>)>,
) {
let mut registrations: IndexMap<Tag, IndexMap<LanguageSystem, (bool, Vec<Statement>)>> =
IndexMap::new();
for (feature_tag, language_systems, lookups) in features {
for (language_system, required) in language_systems {
let (is_required, registered) = registrations
.entry(feature_tag)
.or_default()
.entry(language_system)
.or_default();
*is_required |= required;
for lookup in &lookups {
if !registered.contains(lookup) {
registered.push(lookup.clone());
}
}
}
}
for (feature_tag, systems) in registrations {
let statements = if [Tag::new(b"aalt"), Tag::new(b"size")].contains(&feature_tag) {
None
} else {
self.registration_statements(&systems)
};
if let Some(statements) = statements {
self.features
.insert(feature_tag.to_string().into(), statements);
} else {
for (_, lookups) in systems.into_values() {
self.add_feature_lookups(feature_tag, lookups);
}
}
}
let used: HashSet<SmolStr> = self
.features
.values()
.flatten()
.filter_map(|statement| match statement {
Statement::LookupReference(reference) => Some(SmolStr::new(&reference.lookup_name)),
_ => None,
})
.chain(self.lookups.values().flat_map(called_lookups))
.collect();
for name in std::mem::take(&mut self.aalt_lookups) {
if !used.contains(&name) {
self.lookups.shift_remove(&name);
}
}
}
fn registration_statements(
&self,
systems: &IndexMap<LanguageSystem, (bool, Vec<Statement>)>,
) -> Option<Vec<Statement>> {
let dflt = Tag::new(b"dflt");
let system_count: usize = self.language_systems.values().map(|l| l.len()).sum();
let (_, first) = &systems[0];
let everywhere = systems.len() == system_count
&& systems.values().all(|(required, lookups)| {
!required
&& lookups.len() == first.len()
&& lookups.iter().all(|l| first.contains(l))
});
if everywhere {
return None;
}
let mut statements = vec![];
for (script_tag, languages) in &self.language_systems {
if !languages
.iter()
.any(|language| systems.contains_key(&(*script_tag, *language)))
{
continue;
}
statements.push(Statement::Script(ScriptStatement::new(
script_tag.to_string().trim_end().into(),
)));
let (default_required, default_lookups) = systems
.get(&(*script_tag, dflt))
.cloned()
.unwrap_or_default();
if default_required {
statements.push(Statement::Language(LanguageStatement::new(
dflt.to_string(),
true,
true,
)));
}
statements.extend(default_lookups.iter().cloned());
for language in languages.iter().filter(|language| **language != dflt) {
let Some((required, lookups)) = systems.get(&(*script_tag, *language)) else {
continue;
};
let include_dflt = default_lookups.iter().all(|l| lookups.contains(l));
statements.push(Statement::Language(LanguageStatement::new(
language.to_string().trim_end().into(),
include_dflt,
*required,
)));
statements.extend(
lookups
.iter()
.filter(|l| !include_dflt || !default_lookups.contains(l))
.cloned(),
);
}
}
Some(statements)
}
fn uncompile_feature_names(&self, tag: &str, name_id: NameId) -> Statement {
let statements = self
.name
.iter()
.flat_map(|name| {
name.name_record()
.iter()
.filter(move |record| record.name_id() == name_id)
.filter_map(move |record| {
let string = record.string(name.string_data()).ok()?;
Some(Statement::FeatureNameStatement(NameRecord::new(
record.platform_id(),
record.encoding_id(),
record.language_id(),
string.to_string(),
NameRecordKind::FeatureName,
0..0,
)))
})
})
.collect();
Statement::NestedBlock(NestedBlock {
tag: tag.into(),
statements,
pos: 0..0,
})
}
fn uncompile_cv_parameters(&self, params: &CharacterVariantParams) -> Statement {
let mut statements = vec![];
for (tag, name_id) in [
("FeatUILabelNameID", params.feat_ui_label_name_id()),
(
"FeatUITooltipTextNameID",
params.feat_ui_tooltip_text_name_id(),
),
("SampleTextNameID", params.sample_text_name_id()),
] {
statements.push(self.uncompile_feature_names(tag, name_id));
}
let first = params.first_param_ui_label_name_id().to_u16();
for index in 0..params.num_named_parameters() {
statements.push(
self.uncompile_feature_names("ParamUILabelNameID", NameId::new(first + index)),
);
}
statements.extend(params.character().iter().map(|character| {
Statement::Character(CharacterStatement::new(character.get().to_u32(), 0..0))
}));
Statement::NestedBlock(NestedBlock {
tag: "cvParameters".into(),
statements,
pos: 0..0,
})
}
fn uncompile_size_parameters(&self, params: &SizeParams) -> Vec<Statement> {
let mut statements = vec![Statement::SizeParameters(SizeParameters::new(
params.design_size() as f64 / 10.0,
params.identifier(),
params.range_start() as f64 / 10.0,
params.range_end() as f64 / 10.0,
0..0,
))];
let name_id = NameId::new(params.name_entry());
if name_id != NameId::new(0) {
statements.extend(self.name.iter().flat_map(|name| {
name.name_record()
.iter()
.filter(move |record| record.name_id() == name_id)
.filter_map(move |record| {
let string = record.string(name.string_data()).ok()?;
Some(Statement::SizeMenuName(NameRecord::new(
record.platform_id(),
record.encoding_id(),
record.language_id(),
string.to_string(),
NameRecordKind::FeatureSizeMenuName,
0..0,
)))
})
}));
}
statements
}
fn add_feature_lookups(&mut self, feature_tag: Tag, lookups: Vec<Statement>) {
let lookups = if feature_tag == Tag::new(b"aalt") {
lookups
.into_iter()
.flat_map(|statement| match &statement {
Statement::LookupReference(reference) => self
.lookups
.get(reference.lookup_name.as_str())
.map(|block| {
self.aalt_lookups.insert(block.name.clone());
block
.statements
.iter()
.filter(|s| {
matches!(
s,
Statement::SingleSubst(_) | Statement::AlternateSubst(_)
)
})
.cloned()
.collect()
})
.unwrap_or_else(|| vec![statement]),
_ => vec![statement],
})
.collect()
} else {
lookups
};
let statements = self
.features
.entry(feature_tag.to_string().into())
.or_default();
for lookup in lookups {
if !statements.contains(&lookup) {
statements.push(lookup);
}
}
}
fn add_lookup_flags(
&mut self,
lookupblock: &mut LookupBlock,
flags: LookupFlag,
mark_filtering_set: Option<u16>,
) {
if flags == LookupFlag::empty() {
return;
}
let mark_glyph_sets = self.gdef.as_ref().and_then(|x| {
let mark_glyph_sets = x.mark_glyph_sets_def();
if let Some(Ok(mark_glyph_sets)) = mark_glyph_sets {
Some(mark_glyph_sets)
} else {
None
}
});
let mark_attachment_classes = self.gdef.as_ref().and_then(|x| {
let mark_attachment_classes = x.mark_attach_class_def();
if let Some(Ok(mark_attachment_classes)) = mark_attachment_classes {
Some(mark_attachment_classes)
} else {
None
}
});
let set = mark_filtering_set.and_then(|set| {
mark_glyph_sets
.and_then(|mgss| mgss.coverages().get(set as usize).ok())
.map(|coverage| self.resolve_coverage_to_class(&coverage, false))
});
let mark_attachment_class = flags.mark_attachment_class().map(|class| {
let classes = mark_attachment_classes
.and_then(|mac| self.resolve_classes(Some(&mac)).get(&class).cloned())
.unwrap_or_default();
GlyphContainer::GlyphClass(GlyphClass::new(classes, 0..0))
});
lookupblock.statements.insert(
0,
Statement::LookupFlag(LookupFlagStatement::new(
flags.to_bits(),
mark_attachment_class,
set,
0..0,
)),
);
}
}
fn lookup_reference(name: &SmolStr) -> Statement {
Statement::LookupReference(LookupReferenceStatement::new(name.to_string(), 0..0))
}
pub(crate) fn add_subtable_break(lookupblock: &mut LookupBlock) {
if lookupblock
.statements
.last()
.is_some_and(|statement| !matches!(statement, Statement::Subtable(_)))
{
lookupblock
.statements
.push(Statement::Subtable(SubtableStatement::new()));
}
}
fn comment_out(statement: Statement) -> Statement {
Statement::Comment(Comment::new(format!("# {}", statement.as_fea(""))))
}
fn feature_language_systems(
script_list: Option<ScriptList>,
) -> Result<HashMap<u16, FeatureLanguageSystems>, ReadError> {
let mut systems: HashMap<u16, FeatureLanguageSystems> = HashMap::new();
let Some(script_list) = script_list else {
return Ok(systems);
};
for script_record in script_list.script_records() {
let script = script_record.script(script_list.offset_data())?;
let mut language_systems = vec![];
if let Some(lang_sys) = script.default_lang_sys() {
language_systems.push((Tag::new(b"dflt"), lang_sys?));
}
for lang_sys_record in script.lang_sys_records() {
language_systems.push((
lang_sys_record.lang_sys_tag(),
lang_sys_record.lang_sys(script.offset_data())?,
));
}
for (lang_sys_tag, lang_sys) in language_systems {
let required_feature_index = lang_sys.required_feature_index();
let indices = lang_sys.feature_indices().iter().map(|index| index.get());
for index in
indices.chain((required_feature_index != 0xFFFF).then_some(required_feature_index))
{
systems.entry(index).or_default().push((
(script_record.script_tag(), lang_sys_tag),
index == required_feature_index,
));
}
}
}
Ok(systems)
}
fn called_lookups(lookup: &LookupBlock) -> Vec<SmolStr> {
lookup
.statements
.iter()
.flat_map(|statement| match statement {
Statement::ChainedContextSubst(statement) => statement.lookups.concat(),
Statement::ChainedContextPos(statement) => statement.lookups.concat(),
_ => vec![],
})
.collect()
}
fn feature_dependencies(
name: &SmolStr,
calls: &HashMap<SmolStr, Vec<SmolStr>>,
contextual_only: &HashSet<SmolStr>,
) -> Vec<SmolStr> {
let mut dependencies = vec![];
let mut seen = HashSet::new();
let mut stack: Vec<&SmolStr> = calls.get(name).into_iter().flatten().collect();
while let Some(dependency) = stack.pop() {
if !seen.insert(dependency) {
continue;
}
if contextual_only.contains(dependency) {
stack.extend(calls.get(dependency).into_iter().flatten());
} else {
dependencies.push(dependency.clone());
}
}
dependencies
}
pub fn uncompile(
font: &skrifa::FontRef,
do_gdef: bool,
) -> Result<fea_rs_ast::FeatureFile, ReadError> {
let mut context = UncompileContext::new(font)?;
let mut ff = fea_rs_ast::FeatureFile::new(vec![]);
ff.statements.extend(context.dump_language_systems());
if do_gdef {
ff.statements.extend(context.uncompile_gdef()?);
}
for definitions in context.mark_classes.values() {
for definition in definitions {
ff.statements
.push(ToplevelItem::MarkClassDefinition(definition.clone()));
}
}
for (name, contents) in context.named_classes.iter() {
ff.statements.push(ToplevelItem::GlyphClassDefinition(
GlyphClassDefinition::new(name.to_string(), contents.clone(), 0..0),
));
}
for lookup in context.lookups.values() {
ff.statements.push(ToplevelItem::Lookup(lookup.clone()));
}
for (feature_name, lookup_refs) in context.features.iter() {
ff.statements
.push(ToplevelItem::Feature(fea_rs_ast::FeatureBlock::new(
feature_name.clone(),
lookup_refs.clone(),
false,
0..0,
)));
}
for (condition_set, variations) in context.feature_variations.iter() {
ff.statements
.push(ToplevelItem::ConditionSet(condition_set.clone()));
for variation in variations {
ff.statements
.push(ToplevelItem::VariationBlock(variation.clone()));
}
}
Ok(ff)
}
pub fn uncompile_bytes(
font_data: &[u8],
do_gdef: bool,
) -> Result<fea_rs_ast::FeatureFile, ReadError> {
let fontref = skrifa::FontRef::new(font_data)?;
uncompile(&fontref, do_gdef)
}
pub fn uncompile_context<'a>(font: &'a skrifa::FontRef) -> Result<UncompileContext<'a>, ReadError> {
UncompileContext::new(font)
}
pub fn unique_glyph_names(font: &skrifa::FontRef) -> Vec<SmolStr> {
let glyph_names = GlyphNames::new(font);
unique_names(
(0..glyph_names.num_glyphs())
.map(GlyphId::new)
.map(|gid| fea_glyph_name(&glyph_name(&glyph_names, gid)))
.collect(),
)
}
fn glyph_name(glyph_names: &GlyphNames, gid: GlyphId) -> String {
match glyph_names.get(gid) {
Some(name) if !name.is_synthesized() => name.to_string(),
_ if gid.to_u32() == 0 => ".notdef".to_string(),
_ => format!("glyph{:05}", gid.to_u32()),
}
}
fn fea_glyph_name(name: &str) -> SmolStr {
let mut out: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
c
} else {
'_'
}
})
.collect();
if out.is_empty() || out.starts_with(|c: char| c.is_ascii_digit() || c == '-') {
out.insert(0, '_');
}
SmolStr::new(out)
}
fn unique_names(names: Vec<SmolStr>) -> Vec<SmolStr> {
let all: HashSet<SmolStr> = names.iter().cloned().collect();
let mut taken: HashSet<SmolStr> = HashSet::new();
names
.into_iter()
.map(|name| {
if taken.insert(name.clone()) {
return name;
}
let base = match name.strip_prefix('.') {
Some(rest) => format!("_{rest}"),
None => name.to_string(),
};
(1..)
.map(|n| SmolStr::new(format!("{base}.{n}")))
.find(|candidate| !all.contains(candidate) && taken.insert(candidate.clone()))
.unwrap_or(name)
})
.collect()
}
#[cfg(test)]
mod tests {
use fea_rs_ast::AsFea;
use super::*;
#[test]
fn test_uncompile_static() {
let data = std::fs::read("resources/test.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
table GDEF {
GlyphClassDef [A], [], [grave acute dotbelowcomb], [];
} GDEF;
markClass grave <anchor 200 150> @bottomleft;
markClass acute <anchor 350 0> @bottomleft;
markClass dotbelowcomb <anchor 200 -200> @mark_class_1x1;
lookup gsub_single_1 {
sub a by b;
} gsub_single_1;
lookup gsub_multiple_1 {
sub a by b c;
} gsub_multiple_1;
lookup gsub_alternate_1 {
sub a from [b c d e f];
} gsub_alternate_1;
lookup gsub_ligature_1 {
sub b c by a;
} gsub_ligature_1;
lookup gsub_contextual_1 {
sub [one a]' lookup gsub_single_1 b' [two c]' lookup gsub_multiple_1;
} gsub_contextual_1;
lookup gsub_chain_contextual_1 {
sub one two three a' lookup gsub_single_1 b' c' lookup gsub_multiple_1 x y z;
} gsub_chain_contextual_1;
lookup gsub_single_2 {
sub [a b] by [b c];
} gsub_single_2;
lookup gsub_single_3 {
sub [a b] by [d c];
} gsub_single_3;
lookup gsub_single_4 {
sub a by c;
} gsub_single_4;
lookup gsub_chain_contextual_2 {
sub one a' lookup gsub_single_4;
} gsub_chain_contextual_2;
lookup gsub_multiple_2 {
sub e by NULL;
} gsub_multiple_2;
lookup gsub_chain_contextual_3 {
ignore sub one a' b;
sub a' lookup gsub_single_1;
} gsub_chain_contextual_3;
lookup gsub_contextual_2 {
ignore sub a' b';
sub a' lookup gsub_single_1 c';
} gsub_contextual_2;
lookup gsub_single_5 {
lookupflag UseMarkFilteringSet [acute];
sub b by c;
} gsub_single_5;
lookup gsub_reverse_1 {
rsub one two [a b]' c by [d e];
} gsub_reverse_1;
lookup gsub_single_6 {
sub one by two;
} gsub_single_6;
lookup gpos_mark_to_base_1 {
pos base A
<anchor 150 100> mark @bottomleft
<anchor -200 -200> mark @mark_class_1x1;
} gpos_mark_to_base_1;
lookup gpos_single_1 {
pos A 10;
} gpos_single_1;
lookup gpos_chain_contextual_1 {
pos one A' lookup gpos_single_1;
} gpos_chain_contextual_1;
lookup gpos_single_2 {
pos A 20;
} gpos_single_2;
lookup gpos_chain_contextual_2 {
ignore pos one A';
pos A' lookup gpos_single_2;
} gpos_chain_contextual_2;
lookup gpos_single_3 {
pos [a b] -50;
} gpos_single_3;
lookup gpos_single_4 {
pos a -50;
pos b -60;
} gpos_single_4;
lookup gpos_single_5 {
pos one 30;
} gpos_single_5;
feature ss01 {
lookup gsub_single_6;
lookup gpos_single_5;
} ss01;
"
);
}
#[test]
fn test_uncompile_language_systems() {
let data = std::fs::read("resources/languages.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
languagesystem latn dflt;
languagesystem latn TRK;
lookup gsub_single_1 {
sub b by c;
} gsub_single_1;
lookup gsub_single_2 {
sub a by d;
} gsub_single_2;
lookup gsub_single_3 {
sub a by e;
} gsub_single_3;
feature calt {
script latn;
lookup gsub_single_2;
language TRK;
lookup gsub_single_3;
} calt;
feature locl {
script latn;
lookup gsub_single_2;
language TRK exclude_dflt;
lookup gsub_single_3;
} locl;
feature ss01 {
lookup gsub_single_1;
} ss01;
"
);
}
#[test]
fn test_uncompile_unregistered_features() {
let data = std::fs::read("resources/unregistered.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 {
sub b by c;
} gsub_single_1;
lookup gsub_single_2 {
sub b by d;
} gsub_single_2;
feature ss01 {
lookup gsub_single_1;
} ss01;
feature ss02 {
# lookup gsub_single_2;
} ss02;
feature ss03 {
} ss03;
"
);
}
#[test]
fn test_uncompile_required_features() {
let data = std::fs::read("resources/required.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 {
sub a by b;
} gsub_single_1;
feature rlig {
script DFLT;
language dflt required;
lookup gsub_single_1;
} rlig;
"
);
}
#[test]
fn test_uncompile_ligature_carets() {
let data = std::fs::read("resources/carets.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
table GDEF {
LigatureCaretByPos a 100 200;
LigatureCaretByIndex b 3;
} GDEF;
"
);
}
#[test]
fn test_uncompile_empty_mark_attachment_class() {
let data = std::fs::read("resources/markattach.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 {
lookupflag MarkAttachmentType [];
sub a by b;
} gsub_single_1;
feature ss01 {
lookup gsub_single_1;
} ss01;
"
);
}
#[test]
fn test_uncompile_attachment_points() {
let data = std::fs::read("resources/attach.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
table GDEF {
Attach a 1 2;
Attach b 3;
} GDEF;
"
);
}
#[test]
fn test_uncompile_variable_advances() {
let data = std::fs::read("resources/advances.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gpos_single_1 {
pos a <0 0 (wght=100:-10 wght=400:0 wght=900:20) (wght=100:30 wght=400:0 wght=900:-40)>;
} gpos_single_1;
feature kern {
lookup gpos_single_1;
} kern;
"
);
}
#[test]
fn test_uncompile_axis_order() {
let data = std::fs::read("resources/axisorder.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gpos_single_1 {
pos a (wdth=100,wght=100:-10 wdth=100,wght=400:0 wdth=100,wght=900:20);
} gpos_single_1;
feature kern {
lookup gpos_single_1;
} kern;
"
);
}
#[test]
fn test_uncompile_class_kerning() {
let data = std::fs::read("resources/kerning.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gpos_pair_1 {
pos [a b] [c d] -10;
} gpos_pair_1;
feature kern {
lookup gpos_pair_1;
} kern;
"
);
}
#[test]
fn test_uncompile_feature_names() {
let data = std::fs::read("resources/featurenames.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 {
sub a by b;
} gsub_single_1;
feature ss01 {
featureNames {
name 1 \"Alternate a\";
name \"Alternate a\";
};
lookup gsub_single_1;
} ss01;
"
);
}
#[test]
fn test_uncompile_feature_variations() {
let data = std::fs::read("resources/variations.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 {
sub a by b;
} gsub_single_1;
lookup gsub_single_2 {
sub b by c;
} gsub_single_2;
feature rclt {
lookup gsub_single_1;
} rclt;
conditionset condition_1 {
\twght 700 900;
} condition_1;
variation rclt condition_1 {
lookup gsub_single_2;
} rclt;
"
);
}
#[test]
fn test_uncompile_variation_regions() {
let data = std::fs::read("resources/regions.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gpos_single_1 {
pos a (wght=100:-10 wght=400:0 wght=650:15 wght=900:20);
pos b (wght=100:-10 wght=400:0 wght=900:20);
pos c (wght=400:0 wght=650:0 wght=900:20);
} gpos_single_1;
feature kern {
lookup gpos_single_1;
} kern;
"
);
}
#[test]
fn test_uncompile_null_glyph() {
let data = std::fs::read("resources/nullglyph.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 {
sub a by \\NULL;
} gsub_single_1;
feature ss01 {
lookup gsub_single_1;
} ss01;
"
);
}
#[test]
fn test_uncompile_single_subst_modulo() {
let data = std::fs::read("resources/gsub1_1_modulo_f1.otf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem latn dflt;
lookup gsub_single_1 {
sub [g21 g22] by [gid32787 gid32788];
subtable;
sub [g19 g20] by [gid32789 gid32790];
} gsub_single_1;
lookup gsub_single_2 {
sub gid32787 by g23;
subtable;
sub gid32788 by g18;
subtable;
sub gid32789 by g17;
subtable;
sub gid32790 by g24;
} gsub_single_2;
feature test {
lookup gsub_single_1;
lookup gsub_single_2;
} test;
"
);
}
#[test]
fn test_uncompile_no_variation_index() {
let data = std::fs::read("resources/novariation.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gpos_single_1 {
pos a (wght=100:-10 wght=400:0);
pos b 10;
} gpos_single_1;
feature kern {
lookup gpos_single_1;
} kern;
"
);
}
#[test]
fn test_uncompile_subtable_breaks() {
let data = std::fs::read("resources/subtables.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 {
sub a by b;
subtable;
sub c by d;
} gsub_single_1;
lookup gpos_pair_1 {
pos [a b] [c d] -40;
subtable;
pos [a b] [c d] 0;
} gpos_pair_1;
feature salt {
lookup gsub_single_1;
} salt;
feature kern {
lookup gpos_pair_1;
} kern;
"
);
}
#[test]
fn test_fea_glyph_name() {
assert_eq!(fea_glyph_name("Numero sign"), "Numero_sign");
assert_eq!(fea_glyph_name("uni0300"), "uni0300");
assert_eq!(fea_glyph_name("1st"), "_1st");
assert_eq!(fea_glyph_name("-x"), "_-x");
assert_eq!(fea_glyph_name(""), "_");
assert_eq!(fea_glyph_name(".notdef"), ".notdef");
}
#[test]
fn test_unique_names() {
let names = [".notdef", ".notdef", "a", "a", "a.1"].map(SmolStr::new);
assert_eq!(
unique_names(names.to_vec()),
[".notdef", "_notdef.1", "a", "a.2", "a.1"]
);
}
#[test]
fn test_unique_glyph_names() {
let data = std::fs::read("resources/dupnames.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
assert_eq!(
unique_glyph_names(&fontref),
[".notdef", "a", "dupe", "dupe.1"]
);
let fea = uncompile(&fontref, true).unwrap().as_fea("");
assert!(fea.contains("sub a by dupe.1;"), "{fea}");
}
#[test]
fn test_unnamed_glyph_names() {
let data = std::fs::read("resources/noname.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
assert_eq!(
unique_glyph_names(&fontref),
[".notdef", "glyph00001", "glyph00002"]
);
}
#[test]
fn test_uncompile_aalt_rules() {
let data = std::fs::read("resources/aalt.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let fea = uncompile(&fontref, true).unwrap().as_fea("");
let aalt = &fea[fea.find("feature aalt").unwrap()..fea.find("} aalt;").unwrap()];
assert!(!aalt.contains("lookup "), "{fea}");
assert!(aalt.contains("sub a by a.alt1;"), "{fea}");
assert!(aalt.contains("sub b from [b.alt1 b.alt2];"), "{fea}");
assert!(fea.contains("lookup gsub_single_1 {"), "{fea}");
assert!(!fea.contains("lookup gsub_alternate_1 {"), "{fea}");
}
#[test]
fn test_uncompile_size_feature() {
let data = std::fs::read("resources/size.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
feature size {
parameters 10.0 3 80 139;
sizemenuname \"Text\";
} size;
"
);
}
#[test]
fn test_uncompile_variable_ligature_carets() {
let data = std::fs::read("resources/varcarets.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
table GDEF {
LigatureCaretByPos a (wght=100:100 wght=400:150 wght=900:200) 300;
} GDEF;
"
);
}
#[test]
fn test_uncompile_fractional_locations() {
let data = std::fs::read("resources/fraclocations.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gpos_single_1 {
pos a (wdth=75:-10 wdth=85:-8 wdth=87.5:-6 wdth=112.5:0);
} gpos_single_1;
feature kern {
lookup gpos_single_1;
} kern;
"
);
}
#[test]
fn test_uncompile_cv_parameters() {
let data = std::fs::read("resources/cvparams.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_alternate_1 {
sub a from [b c];
} gsub_alternate_1;
feature cv01 {
cvParameters {
FeatUILabelNameID {
name \"Alternate a\";
};
FeatUITooltipTextNameID {
name \"Alternate forms of a\";
};
SampleTextNameID {
name \"aaa\";
};
ParamUILabelNameID {
name \"Round\";
};
ParamUILabelNameID {
name \"Square\";
};
Character 0x61;
};
lookup gsub_alternate_1;
} cv01;
"
);
}
#[test]
fn test_uncompile_empty_avar_map() {
let data = std::fs::read("resources/emptyavar.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gpos_single_1 {
pos a (wght=100:-10 wght=400:0 wght=900:20);
} gpos_single_1;
feature kern {
lookup gpos_single_1;
} kern;
"
);
}
#[test]
fn test_uncompile_null_anchors() {
let data = std::fs::read("resources/nullanchors.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
table GDEF {
GlyphClassDef [b], [a], [c d], [];
} GDEF;
markClass c <anchor 0 500> @left;
markClass c <anchor 0 500> @mark_class_0x1;
markClass d <anchor 0 0> @mark_class_1x1;
markClass c <anchor 0 500> @topleft;
lookup gpos_mark_to_base_1 {
pos base a
<anchor 250 500> mark @left;
pos base b
<anchor NULL> mark @left;
} gpos_mark_to_base_1;
lookup gpos_mark_to_ligature_1 {
pos ligature a
<anchor 100 500> mark @mark_class_0x1
<anchor NULL> mark @mark_class_1x1
ligComponent
<anchor 400 500> mark @mark_class_0x1
<anchor NULL> mark @mark_class_1x1;
} gpos_mark_to_ligature_1;
lookup gpos_mark_to_mark_1 {
pos mark c
<anchor 0 800> mark @topleft;
pos mark d
<anchor NULL> mark @topleft;
} gpos_mark_to_mark_1;
feature mark {
lookup gpos_mark_to_base_1;
lookup gpos_mark_to_ligature_1;
} mark;
feature mkmk {
lookup gpos_mark_to_mark_1;
} mkmk;
"
);
}
#[test]
fn test_uncompile_extension_lookups() {
let data = std::fs::read("resources/extension.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
lookup gsub_single_1 useExtension {
sub a by b;
} gsub_single_1;
lookup gsub_single_2 {
sub c by d;
} gsub_single_2;
lookup gpos_pair_1 useExtension {
pos a b -10;
} gpos_pair_1;
feature ss01 {
lookup gsub_single_1;
lookup gsub_single_2;
} ss01;
feature kern {
lookup gpos_pair_1;
} kern;
"
);
}
#[test]
fn test_uncompile_variation_scripts() {
let data = std::fs::read("resources/variationscripts.ttf").unwrap();
let fontref = skrifa::FontRef::new(&data).unwrap();
let ff = uncompile(&fontref, true).unwrap();
assert_eq!(
ff.as_fea(""),
"\
languagesystem DFLT dflt;
languagesystem latn dflt;
languagesystem grek dflt;
lookup gsub_single_1 {
sub space by exclam;
} gsub_single_1;
lookup gpos_pair_1 {
pos space exclam -10;
} gpos_pair_1;
feature rvrn {
script DFLT;
script latn;
} rvrn;
feature kern {
lookup gpos_pair_1;
} kern;
conditionset condition_1 {
\twght 600 900;
} condition_1;
variation rvrn condition_1 {
script DFLT;
lookup gsub_single_1;
script latn;
lookup gsub_single_1;
} rvrn;
"
);
}
}