From 8e518f9c3ed1818d0a3bcbdf66f7485a4682159b Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 6 Mar 2024 18:50:31 -0500 Subject: [PATCH] [fontbe] Implement kerning alignment This was way more confusing than it should've been, but ultimately I believe this is correct. It brings the oswald diff from 8.6k/3.3k to 7.5k/2.2k, which is very promising. --- fontbe/src/kern.rs | 107 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/fontbe/src/kern.rs b/fontbe/src/kern.rs index fa51337eb..7aed2487e 100644 --- a/fontbe/src/kern.rs +++ b/fontbe/src/kern.rs @@ -1,17 +1,22 @@ //! Generates an [FeaRsKerns] datastructure to be fed to fea-rs -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use fea_rs::{ compile::{PairPosBuilder, ValueRecord as ValueRecordBuilder}, GlyphSet, }; -use fontdrasil::orchestration::{Access, AccessBuilder, Work}; +use fontdrasil::{ + coords::NormalizedLocation, + orchestration::{Access, AccessBuilder, Work}, + types::GlyphName, +}; use fontir::{ - ir::{KernPair, KernParticipant}, + ir::{KernGroup, KernPair, KernParticipant, KerningGroups, KerningInstance}, orchestration::WorkId as FeWorkId, }; use log::debug; +use ordered_float::OrderedFloat; use write_fonts::types::GlyphId; use crate::{ @@ -99,10 +104,12 @@ impl Work for GatherIrKerningWork { .collect::>(); // Add IR kerns to builder. IR kerns are split by location so put them back together again. - let kern_by_pos: HashMap<_, _> = ir_kerns + let mut kern_by_pos: HashMap<_, _> = ir_kerns .iter() - .map(|(_, ki)| (ki.location.clone(), ki.as_ref())) + .map(|(_, ki)| (ki.location.clone(), ki.as_ref().to_owned())) .collect(); + + align_kerning(&ir_groups, &mut kern_by_pos); // Use a BTreeMap because it seems the order we process pairs matters. Maybe we should sort instead...? let mut adjustments: BTreeMap = Default::default(); @@ -140,6 +147,96 @@ impl Work for GatherIrKerningWork { } } +/// 'align' the kerning, ensuring each pair is defined for each location. +/// +/// missing pairs are filled in via the UFO kerning value lookup algorithm: +/// +/// +fn align_kerning( + groups: &KerningGroups, + instances: &mut HashMap, +) { + let union_kerning = instances + .values() + .flat_map(|instance| instance.kerns.keys()) + .cloned() + .collect::>(); + + let side1_glyph_to_group_map = groups + .groups + .iter() + .filter(|(group, _)| matches!(group, KernGroup::Side1(_))) + .flat_map(|(group, glyphs)| glyphs.iter().map(move |glyph| (glyph, group))) + .collect::>(); + let side2_glyph_to_group_map = groups + .groups + .iter() + .filter(|(group, _)| matches!(group, KernGroup::Side2(_))) + .flat_map(|(group, glyphs)| glyphs.iter().map(move |glyph| (glyph, group))) + .collect::>(); + + for instance in instances.values_mut() { + let missing_pairs = union_kerning + .iter() + .filter(|pair| !instance.kerns.contains_key(pair)) + .collect::>(); + + for pair in missing_pairs { + let value = lookup_kerning_value( + pair, + instance, + &side1_glyph_to_group_map, + &side2_glyph_to_group_map, + ); + log::info!("got aligned value {value} for kern pair {pair:?}"); + instance.kerns.insert(pair.to_owned(), value); + } + } +} + +// +fn lookup_kerning_value( + pair: &KernPair, + kerning: &KerningInstance, + side1_glyphs: &HashMap<&GlyphName, &KernGroup>, + side2_glyphs: &HashMap<&GlyphName, &KernGroup>, +) -> OrderedFloat { + fn get_group_if_glyph( + side: &KernParticipant, + map: &HashMap<&GlyphName, &KernGroup>, + ) -> Option { + match side { + KernParticipant::Glyph(glyph) => map + .get(&glyph) + .map(|group| KernParticipant::Group((*group).clone())), + KernParticipant::Group(_) => None, + } + } + + let (first, second) = pair; + // for each side: if it's a group, we only check the group. + // if it's a glyph, we check both the glyph as well as the group containing that glyph. + let first_group = get_group_if_glyph(first, side1_glyphs); + let second_group = get_group_if_glyph(first, side2_glyphs); + let first = Some(first).filter(|side| side.is_glyph()); + let second = Some(second).filter(|side| side.is_glyph()); + + for (first, second) in [ + (first.cloned(), second_group.clone()), + (first_group.clone(), second.cloned()), + (first_group.clone(), second_group.clone()), + ] { + if let Some(pair) = first.zip(second) { + if let Some(value) = kerning.kerns.get(&pair) { + return *value; + } + } + } + + // then fallback to zero + 0.0.into() +} + impl Work for KerningFragmentWork { fn id(&self) -> AnyWorkId { WorkId::KernFragment(self.segment).into()