use std::{
borrow::Cow,
collections::{BTreeMap, HashMap, HashSet},
fmt::Display,
fs,
path::{Path, PathBuf},
sync::Arc,
};
use feature_variations::FeatureVariationsProvider;
use log::{debug, error, trace, warn};
use ordered_float::OrderedFloat;
use fea_rs::{
DiagnosticSet, GlyphMap, Opts, ParseTree,
compile::{
Compilation, FeatureBuilder, FeatureProvider, NopFeatureProvider, PendingLookup,
VariationInfo, error::CompilerError,
},
parse::{FileSystemResolver, SourceLoadError, SourceResolver},
typed::{AstNode, LanguageSystem},
};
use fontir::{
ir::{FeaturesSource, GlyphOrder, StaticMetadata},
orchestration::{Flags, WorkId as FeWorkId},
};
use fontdrasil::{
coords::NormalizedLocation,
orchestration::{Access, AccessBuilder, Work},
types::Axis,
variations::{DeltaError, VariationModel},
};
use properties::UnicodeShortName;
use write_fonts::{
OtRound,
tables::{gdef::GlyphClassDef, layout::ClassDef, variations::VariationRegion},
types::{GlyphId16, NameId, Tag},
};
use crate::{
error::Error,
orchestration::{
AnyWorkId, BeWork, Context, ExtraFeaTables, FeaFirstPassOutput, FeaRsKerns, FeaRsMarks,
WorkId,
},
};
mod feature_variations;
mod kern;
mod marks;
mod ot_tags;
mod properties;
#[cfg(test)]
mod test_helpers;
pub use kern::{create_gather_ir_kerning_work, create_kern_segment_work, create_kerns_work};
pub use marks::create_mark_work;
const DFLT_SCRIPT: Tag = Tag::new(b"DFLT");
const DFLT_LANG: Tag = Tag::new(b"dflt");
#[derive(Debug)]
pub struct FeatureFirstPassWork {}
#[derive(Debug)]
pub struct FeatureCompilationWork {}
struct InMemoryResolver {
content_path: PathBuf,
content: Arc<str>,
include_dir: Option<PathBuf>,
}
impl InMemoryResolver {
fn empty() -> Self {
InMemoryResolver {
content_path: Default::default(),
content: "".into(),
include_dir: None,
}
}
}
impl SourceResolver for InMemoryResolver {
fn get_contents(&self, rel_path: &Path) -> Result<Arc<str>, SourceLoadError> {
if rel_path == &*self.content_path {
return Ok(self.content.clone());
}
let Some(include_dir) = &self.include_dir else {
return Err(SourceLoadError::new(
rel_path.to_path_buf(),
NoIncludePathError::new(),
));
};
let path = include_dir
.join(rel_path)
.canonicalize()
.map_err(|e| SourceLoadError::new(rel_path.to_path_buf(), e))?;
if !path.is_file() {
return Err(SourceLoadError::new(
rel_path.to_path_buf(),
Error::FileExpected(path),
));
}
trace!("Resolved {rel_path:?} to {path:?}");
let contents = fs::read_to_string(path)
.map_err(|e| SourceLoadError::new(rel_path.to_path_buf(), e))?;
Ok(Arc::from(contents.as_str()))
}
}
#[derive(Debug)]
struct NoIncludePathError {}
impl NoIncludePathError {
fn new() -> NoIncludePathError {
NoIncludePathError {}
}
}
impl std::error::Error for NoIncludePathError {}
impl Display for NoIncludePathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("No include path available")?;
Ok(())
}
}
pub(crate) struct FeaVariationInfo<'a> {
axes: HashMap<Tag, (usize, &'a Axis)>,
static_metadata: &'a StaticMetadata,
}
impl<'a> FeaVariationInfo<'a> {
pub(crate) fn new(static_metadata: &'a StaticMetadata) -> FeaVariationInfo<'a> {
FeaVariationInfo {
axes: static_metadata
.axes
.iter()
.enumerate()
.map(|(i, a)| (a.tag, (i, a)))
.collect(),
static_metadata,
}
}
}
pub(crate) fn get_gdef_classes(
meta: &StaticMetadata,
ast: &FeaFirstPassOutput,
glyph_order: &GlyphOrder,
) -> HashMap<GlyphId16, GlyphClassDef> {
ast.gdef_classes
.as_ref()
.filter(|_| meta.gdef_categories.prefer_gdef_categories_in_fea)
.cloned()
.unwrap_or_else(|| {
meta.gdef_categories
.categories
.iter()
.filter_map(|(name, category)| {
glyph_order.glyph_id(name).map(|gid| (gid, *category))
})
.collect()
})
}
pub(crate) fn resolve_variable_metric<'a>(
static_metadata: &StaticMetadata,
values: impl Iterator<Item = (&'a NormalizedLocation, &'a OrderedFloat<f64>)>,
) -> Result<(i16, Vec<(VariationRegion, i16)>), DeltaError> {
let point_seqs: HashMap<_, _> = values
.into_iter()
.map(|(pos, value)| {
let value: f64 = value.into_inner().ot_round();
(pos.to_owned(), vec![value])
})
.collect();
let locations: HashSet<_> = point_seqs.keys().collect();
let global_locations: HashSet<_> = static_metadata.variation_model.locations().collect();
let var_model: Cow<'_, VariationModel> = if locations == global_locations {
Cow::Borrowed(&static_metadata.variation_model)
} else {
Cow::Owned(VariationModel::new(
locations.into_iter().cloned().collect(),
static_metadata.axes.axis_order(),
))
};
let raw_deltas: Vec<_> = var_model
.deltas(&point_seqs)?
.into_iter()
.map(|(region, values)| {
assert!(values.len() == 1, "{} values?!", values.len());
(region, values[0])
})
.collect();
let default_value: i16 = raw_deltas
.iter()
.filter_map(|(region, value)| {
let scaler = region.scalar_at(&var_model.default).into_inner();
(scaler != 0.0).then_some(*value * scaler)
})
.sum::<f64>()
.ot_round();
let mut deltas = Vec::with_capacity(raw_deltas.len());
for (region, value) in raw_deltas.iter().filter(|(r, _)| !r.is_default()) {
let mut region_axes = Vec::with_capacity(static_metadata.axes.len());
for axis in static_metadata.axes.iter() {
let coords = region.get(&axis.tag).unwrap().to_region_axis_coords();
region_axes.push(coords);
}
deltas.push((
write_fonts::tables::variations::VariationRegion { region_axes },
value.ot_round(),
));
}
Ok((default_value, deltas))
}
struct FeatureWriter<'a> {
kerning: &'a FeaRsKerns,
marks: &'a FeaRsMarks,
feature_variations: Option<FeatureVariationsProvider>,
}
impl<'a> FeatureWriter<'a> {
fn new(
kerning: &'a FeaRsKerns,
marks: &'a FeaRsMarks,
feature_variations: Option<FeatureVariationsProvider>,
) -> Self {
FeatureWriter {
marks,
kerning,
feature_variations,
}
}
fn add_kerning_features(&self, builder: &mut FeatureBuilder) {
self.kerning.add_features(builder);
}
fn add_marks(&self, builder: &mut FeatureBuilder) {
self.marks.add_features(builder);
}
fn add_feature_variations(&self, builder: &mut FeatureBuilder) {
if let Some(variations) = self.feature_variations.as_ref() {
variations.add_features(builder);
}
}
}
impl FeatureProvider for FeatureWriter<'_> {
fn add_features(&self, builder: &mut FeatureBuilder) {
self.add_kerning_features(builder);
self.add_marks(builder);
self.add_feature_variations(builder);
}
}
impl VariationInfo for FeaVariationInfo<'_> {
type Error = Error;
fn axis(&self, axis_tag: Tag) -> Option<(usize, &Axis)> {
self.axes.get(&axis_tag).map(|(i, a)| (*i, *a))
}
fn resolve_variable_metric(
&self,
values: &HashMap<NormalizedLocation, i16>,
) -> Result<(i16, Vec<(VariationRegion, i16)>), Error> {
let point_seqs: HashMap<_, _> = values
.iter()
.map(|(pos, value)| (pos.clone(), vec![*value as f64]))
.collect();
let locations: HashSet<_> = point_seqs.keys().collect();
let global_locations: HashSet<_> =
self.static_metadata.variation_model.locations().collect();
let var_model: Cow<'_, VariationModel> = if locations == global_locations {
Cow::Borrowed(&self.static_metadata.variation_model)
} else {
Cow::Owned(VariationModel::new(
locations.into_iter().cloned().collect(),
self.static_metadata.axes.axis_order(),
))
};
let deltas: Vec<_> = var_model
.deltas(&point_seqs)
.map_err(Error::DeltaError)?
.into_iter()
.map(|(region, values)| {
assert!(values.len() == 1, "{} values?!", values.len());
(region, values[0])
})
.collect();
let default_value = deltas
.iter()
.filter_map(|(region, value)| {
let scaler = region.scalar_at(&var_model.default).into_inner();
(scaler != 0.0).then_some(*value * scaler)
})
.sum::<f64>()
.ot_round();
let mut fears_deltas = Vec::with_capacity(deltas.len());
for (region, value) in deltas.iter().filter(|(r, _)| !r.is_default()) {
fears_deltas.push((
region.to_write_fonts_variation_region(&self.static_metadata.axes),
value.ot_round(),
));
}
Ok((default_value, fears_deltas))
}
fn axis_count(&self) -> u16 {
self.axes.len().try_into().unwrap()
}
fn resolve_glyphs_number_value(
&self,
name: &str,
) -> Result<HashMap<NormalizedLocation, f64>, Error> {
Ok(self
.static_metadata
.number_values
.iter()
.map(|(loc, names)| (loc.clone(), names.get(name).copied().unwrap_or_default().0))
.collect())
}
}
impl FeatureCompilationWork {
pub fn create() -> Box<BeWork> {
Box::new(FeatureCompilationWork {})
}
fn compile(
&self,
static_metadata: &StaticMetadata,
glyph_order: &GlyphOrder,
ast: &FeaFirstPassOutput,
kerns: &FeaRsKerns,
marks: &FeaRsMarks,
) -> Result<Compilation, Error> {
let feature_variations = static_metadata
.variations
.as_ref()
.map(|ir_variations| {
feature_variations::make_gsub_feature_variations(
ir_variations,
static_metadata,
glyph_order,
)
})
.transpose()?;
let var_info = FeaVariationInfo::new(static_metadata);
let feature_writer = FeatureWriter::new(kerns, marks, feature_variations);
match fea_rs::compile::compile(
&ast.ast,
&marks.glyphmap,
Some(&var_info),
Some(&feature_writer),
Opts::new(),
) {
Ok((result, warnings)) => {
log_fea_warnings("compilation", &warnings);
Ok(result)
}
Err(errors) => Err(Error::FeaCompileError(CompilerError::CompilationFail(
errors,
))),
}
}
}
fn write_debug_glyph_order(context: &Context, glyphs: &GlyphOrder) {
let glyph_order_file = context.debug_dir().join("glyph_order.txt");
let glyph_order = glyphs.names().map(|g| g.as_str()).collect::<Vec<_>>();
let glyph_order = glyph_order.join("\n");
if let Err(e) = fs::write(glyph_order_file, glyph_order) {
log::error!("failed to write glyph order to debug/glyph_order.txt: '{e}'");
}
}
fn write_debug_fea(context: &Context, is_error: bool, why: &str, fea_content: &str) {
if !context.flags.contains(Flags::EMIT_DEBUG) {
if is_error {
warn!("Debug fea not written for '{why}' because --emit-debug is off");
}
return;
}
let debug_file = context.debug_dir().join("features.fea");
match fs::write(&debug_file, fea_content) {
Ok(_) if is_error => warn!("{why}; fea written to {debug_file:?}"),
Ok(_) => debug!("fea written to {debug_file:?}"),
Err(e) => error!("{why}; failed to write fea to {debug_file:?}: {e}"),
};
}
impl Work<Context, AnyWorkId, Error> for FeatureFirstPassWork {
fn id(&self) -> AnyWorkId {
WorkId::FeaturesAst.into()
}
fn read_access(&self) -> Access<AnyWorkId> {
AccessBuilder::new()
.variant(FeWorkId::GlyphOrder)
.variant(FeWorkId::StaticMetadata)
.variant(FeWorkId::Features)
.build()
}
fn exec(&self, context: &Context) -> Result<(), Error> {
let features = context.ir.features.get();
let glyph_order = context.ir.glyph_order.get();
let static_metadata = context.ir.static_metadata.get();
let glyph_map = glyph_order.names().cloned().collect();
let result = self.parse(&features, &glyph_map);
if context.flags.contains(Flags::EMIT_DEBUG) {
write_debug_glyph_order(context, &glyph_order);
}
if let FeaturesSource::Memory { fea_content, .. } = features.as_ref() {
write_debug_fea(context, result.is_err(), "compile failed", fea_content);
}
let ast = result?;
self.validate(&ast, &glyph_map, &static_metadata)?;
let var_info = FeaVariationInfo::new(&static_metadata);
let (compilation, _) = fea_rs::compile::compile::<_, NopFeatureProvider>(
&ast,
&glyph_map,
Some(&var_info),
None,
Opts::new().compile_gpos(false),
)
.map_err(|err| {
Error::FeaCompileError(fea_rs::compile::error::CompilerError::CompilationFail(err))
})?;
context
.fea_ast
.set(FeaFirstPassOutput::new(ast, compilation)?);
Ok(())
}
}
impl FeatureFirstPassWork {
pub fn create() -> Box<BeWork> {
Box::new(Self {})
}
fn parse(&self, features: &FeaturesSource, glyph_map: &GlyphMap) -> Result<ParseTree, Error> {
let (resolver, root_path) = get_resolver_and_root_path(features);
let (tree, diagnostics) = fea_rs::parse::parse_root(root_path, Some(glyph_map), resolver)
.map_err(CompilerError::SourceLoad)?;
if diagnostics.has_errors() {
return Err(CompilerError::ParseFail(diagnostics).into());
}
log_fea_warnings("parsing", &diagnostics);
Ok(tree)
}
fn validate(
&self,
ast: &ParseTree,
glyph_map: &GlyphMap,
static_metadata: &StaticMetadata,
) -> Result<(), Error> {
let var_info = FeaVariationInfo::new(static_metadata);
let diagnostics = fea_rs::compile::validate(ast, glyph_map, Some(&var_info));
if diagnostics.has_errors() {
return Err(CompilerError::ValidationFail(diagnostics).into());
}
log_fea_warnings("validation", &diagnostics);
Ok(())
}
}
fn get_resolver_and_root_path(features: &FeaturesSource) -> (Box<dyn SourceResolver>, PathBuf) {
match features {
FeaturesSource::File {
fea_file,
include_dir,
} => {
let project_root = include_dir
.clone()
.or_else(|| fea_file.parent().map(PathBuf::from))
.unwrap_or_default();
(
Box::new(FileSystemResolver::new(project_root)),
fea_file.clone(),
)
}
FeaturesSource::Memory {
fea_content,
include_dir,
} => (
Box::new(InMemoryResolver {
include_dir: include_dir.to_owned(),
content_path: PathBuf::new(),
content: fea_content.as_str().into(),
}),
PathBuf::new(),
),
FeaturesSource::Empty => (Box::new(InMemoryResolver::empty()), Default::default()),
}
}
impl Work<Context, AnyWorkId, Error> for FeatureCompilationWork {
fn id(&self) -> AnyWorkId {
WorkId::Features.into()
}
fn read_access(&self) -> Access<AnyWorkId> {
AccessBuilder::new()
.variant(FeWorkId::GlyphOrder)
.variant(WorkId::FeaturesAst)
.variant(WorkId::GatherBeKerning)
.variant(WorkId::Marks)
.build()
}
fn also_completes(&self) -> Vec<AnyWorkId> {
vec![
WorkId::Gpos.into(),
WorkId::Gsub.into(),
WorkId::Gdef.into(),
WorkId::ExtraFeaTables.into(),
]
}
fn exec(&self, context: &Context) -> Result<(), Error> {
let static_metadata = context.ir.static_metadata.get();
let ast = context.fea_ast.get();
let glyph_order = context.ir.glyph_order.get();
let kerns = context.fea_rs_kerns.get();
let marks = context.fea_rs_marks.get();
let mut result = self.compile(
&static_metadata,
&glyph_order,
&ast,
kerns.as_ref(),
marks.as_ref(),
)?;
if result.gdef_classes.is_none() && !static_metadata.gdef_categories.categories.is_empty() {
let gdef = result.gdef.get_or_insert_with(Default::default);
let class_def: ClassDef = static_metadata
.gdef_categories
.categories
.iter()
.filter_map(|(name, cls)| glyph_order.glyph_id(name).map(|id| (id, *cls as u16)))
.collect();
gdef.glyph_class_def.set(class_def);
}
debug!(
"Built features, gpos? {} gsub? {} gdef? {}",
result.gpos.is_some(),
result.gsub.is_some(),
result.gdef.is_some(),
);
if result.name.is_some() {
let max_existing_name_id: NameId = static_metadata
.names
.keys()
.map(|key| key.name_id)
.max()
.unwrap_or(NameId::LAST_RESERVED_NAME_ID)
.max(NameId::LAST_RESERVED_NAME_ID);
if max_existing_name_id > NameId::LAST_RESERVED_NAME_ID {
result.remap_name_ids(max_existing_name_id.to_u16() + 1);
}
}
if let Some(gpos) = result.gpos.take() {
context.gpos.set(gpos);
}
if let Some(gsub) = result.gsub.take() {
context.gsub.set(gsub);
}
if let Some(gdef) = result.gdef.take() {
context.gdef.set(gdef);
}
if result.has_non_layout_tables() {
let extras = ExtraFeaTables::from(result);
extras.log_unhandled_extras();
context.extra_fea_tables.set(extras);
}
if context.flags.contains(Flags::EMIT_IR) {
fs::write(
context
.persistent_storage
.paths
.target_file(&WorkId::Features),
"1",
)
.map_err(Error::IoError)?;
}
Ok(())
}
}
fn log_fea_warnings(stage: &str, warnings: &DiagnosticSet) {
assert!(!warnings.has_errors(), "of course we checked this already");
if !warnings.is_empty() {
log::warn!(
"FEA {stage} produced {} warnings:\n{}",
warnings.len(),
warnings.display()
);
}
}
fn get_fea_language_systems(ast: &ParseTree) -> BTreeMap<Tag, Vec<Tag>> {
let mut languages_by_script = BTreeMap::new();
for langsys in ast
.typed_root()
.statements()
.filter_map(LanguageSystem::cast)
{
languages_by_script
.entry(langsys.script().to_raw())
.or_insert(Vec::new())
.push(langsys.language().to_raw())
}
languages_by_script
}
fn get_script_language_systems(ast: &ParseTree) -> HashMap<UnicodeShortName, Vec<(Tag, Vec<Tag>)>> {
let languages_by_script = get_fea_language_systems(ast);
let mut unic_script_to_languages = HashMap::new();
for (ot_script, langs) in languages_by_script {
let Some(unicode_script) = properties::ot_tag_to_script(ot_script) else {
if ot_script != DFLT_SCRIPT {
log::warn!("no unicode script for OT script tag {ot_script}");
}
continue;
};
unic_script_to_languages
.entry(unicode_script)
.or_insert(Vec::new())
.push((ot_script, langs));
}
unic_script_to_languages
}
fn feature_writer_todo_list(features: &[Tag], ast: &ParseTree) -> HashSet<Tag> {
use fea_rs::typed;
let mut result = features.iter().copied().collect::<HashSet<_>>();
let mut existing_features = HashMap::new();
for feature in ast
.typed_root()
.statements()
.filter_map(typed::Feature::cast)
{
let tag = feature.tag().to_raw();
if result.contains(&tag) {
*existing_features.entry(tag).or_insert(false) |= feature.has_insert_marker();
}
}
for (tag, has_marker) in existing_features {
if !has_marker {
log::warn!(
"Skipping generating feature '{tag}', which is manually declared
in FEA and has no insertion comment."
);
result.remove(&tag);
}
}
result
}
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use fea_rs::compile::VariationInfo;
use fontdrasil::{
coords::{CoordConverter, NormalizedCoord, UserCoord},
types::Axis,
};
use fontir::ir::StaticMetadata;
use super::*;
fn weight_variable_static_metadata() -> StaticMetadata {
let wght = Tag::new(b"wght");
let min_wght = vec![(wght, NormalizedCoord::new(-1.0))].into();
let def_wght = vec![(wght, NormalizedCoord::new(0.0))].into();
let max_wght = vec![(wght, NormalizedCoord::new(1.0))].into();
StaticMetadata::new(
1024,
Default::default(),
vec![
Axis::for_test("wght"),
Axis {
name: "Width".to_string(),
tag: Tag::new(b"wdth"),
min: UserCoord::new(0.0),
default: UserCoord::new(0.0),
max: UserCoord::new(0.0),
hidden: false,
converter: CoordConverter::new(vec![], 0),
localized_names: Default::default(),
},
],
Default::default(),
HashSet::from([min_wght, def_wght, max_wght]),
Default::default(),
Default::default(),
Default::default(),
None,
false,
)
.unwrap()
}
fn is_default(region: &write_fonts::tables::variations::VariationRegion) -> bool {
region.region_axes.iter().all(|axis_coords| {
axis_coords.start_coord.to_f32() == 0.0
&& axis_coords.peak_coord.to_f32() == 0.0
&& axis_coords.end_coord.to_f32() == 0.0
})
}
#[test]
fn resolve_kern() {
let _ = env_logger::builder().is_test(true).try_init();
let wght = Tag::new(b"wght");
let static_metadata = weight_variable_static_metadata();
let var_info = FeaVariationInfo::new(&static_metadata);
let (default, regions) = var_info
.resolve_variable_metric(&HashMap::from([
(vec![(wght, NormalizedCoord::new(-1.0))].into(), 10),
(vec![(wght, NormalizedCoord::new(0.0))].into(), 15),
(vec![(wght, NormalizedCoord::new(1.0))].into(), 20),
]))
.unwrap();
assert!(!regions.iter().any(|(r, _)| is_default(r)));
let region_values: Vec<_> = regions.into_iter().map(|(_, v)| v + default).collect();
assert_eq!((15, vec![10, 20]), (default, region_values));
}
}