-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[read-fonts] Basic closure of glyphs over GSUB
This differs from the fonttools implementation slightly, but I'm not sure if those differences are functional or just a result of how fonttools is organized. This also isn't tested, because the trivial tests don't seem super helpful and the complicated tests seem really tricky to write? The best idea I have for testing this is to use FEA to write out the substitutions, and then compile that with fea-rs and test that. Maybe this is actually okay, since read-fonts can have a dev-dependency on fea-rs...
- Loading branch information
Showing
2 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
//! Computing the closure over a set of glyphs | ||
//! | ||
//! This means taking a set of glyphs and finding the set of glyphs that are | ||
//! reachable from those glyphs via substitution. | ||
|
||
use std::collections::HashSet; | ||
|
||
use font_types::GlyphId; | ||
|
||
use crate::ReadError; | ||
|
||
use super::{ | ||
AlternateSubstFormat1, Gsub, LigatureSubstFormat1, MultipleSubstFormat1, SingleSubst, | ||
SingleSubstFormat1, SingleSubstFormat2, SubstitutionSubtables, | ||
}; | ||
|
||
#[cfg(feature = "std")] | ||
impl<'a> Gsub<'a> { | ||
/// Return the set of glyphs reachable from the input set via any substituion. | ||
pub fn closure_glyphs(&self, glyphs: HashSet<GlyphId>) -> Result<HashSet<GlyphId>, ReadError> { | ||
// we need to do this iteratively, since any glyph found in one pass | ||
// over the lookups could also be the target of substitutions. | ||
|
||
let mut all_glyphs = glyphs; | ||
let mut new_glyphs = HashSet::new(); | ||
|
||
// we always call this once, and then keep calling if it produces | ||
// additional glyphs | ||
self.closure_glyphs_once(&all_glyphs, &mut new_glyphs)?; | ||
while !new_glyphs.is_subset(&all_glyphs) { | ||
all_glyphs.extend(new_glyphs.drain()); | ||
self.closure_glyphs_once(&all_glyphs, &mut new_glyphs)?; | ||
} | ||
|
||
Ok(all_glyphs) | ||
} | ||
|
||
fn closure_glyphs_once( | ||
&self, | ||
input: &HashSet<GlyphId>, | ||
output: &mut HashSet<GlyphId>, | ||
) -> Result<(), ReadError> { | ||
let lookup_list = self.lookup_list()?; | ||
for lookup in lookup_list.lookups().iter() { | ||
let subtables = lookup?.subtables()?; | ||
subtables.closure_glyphs(input, output)?; | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<'a> SubstitutionSubtables<'a> { | ||
fn closure_glyphs( | ||
&self, | ||
input: &HashSet<GlyphId>, | ||
output: &mut HashSet<GlyphId>, | ||
) -> Result<(), ReadError> { | ||
match self { | ||
SubstitutionSubtables::Single(tables) => tables | ||
.iter() | ||
.try_for_each(|t| t?.closure_glyphs(input, output)), | ||
SubstitutionSubtables::Multiple(tables) => tables | ||
.iter() | ||
.try_for_each(|t| t?.closure_glyphs(input, output)), | ||
SubstitutionSubtables::Alternate(tables) => tables | ||
.iter() | ||
.try_for_each(|t| t?.closure_glyphs(input, output)), | ||
SubstitutionSubtables::Ligature(tables) => tables | ||
.iter() | ||
.try_for_each(|t| t?.closure_glyphs(input, output)), | ||
_ => Ok(()), | ||
} | ||
} | ||
} | ||
|
||
impl<'a> SingleSubst<'a> { | ||
fn closure_glyphs( | ||
&self, | ||
input: &HashSet<GlyphId>, | ||
output: &mut HashSet<GlyphId>, | ||
) -> Result<(), ReadError> { | ||
for (target, replacement) in self.iter_subs()? { | ||
if input.contains(&target) { | ||
output.insert(replacement); | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
fn iter_subs(&self) -> Result<impl Iterator<Item = (GlyphId, GlyphId)> + '_, ReadError> { | ||
let (left, right) = match self { | ||
SingleSubst::Format1(t) => (Some(t.iter_subs()?), None), | ||
SingleSubst::Format2(t) => (None, Some(t.iter_subs()?)), | ||
}; | ||
Ok(left | ||
.into_iter() | ||
.flatten() | ||
.chain(right.into_iter().flatten())) | ||
} | ||
} | ||
|
||
impl<'a> SingleSubstFormat1<'a> { | ||
fn iter_subs(&self) -> Result<impl Iterator<Item = (GlyphId, GlyphId)> + '_, ReadError> { | ||
let delta = self.delta_glyph_id(); | ||
let coverage = self.coverage()?; | ||
Ok(coverage.iter().filter_map(move |gid| { | ||
let raw = (gid.to_u16() as i32).checked_add(delta as i32); | ||
let raw = raw.and_then(|raw| u16::try_from(raw).ok())?; | ||
Some((gid, GlyphId::new(raw))) | ||
})) | ||
} | ||
} | ||
|
||
impl<'a> SingleSubstFormat2<'a> { | ||
fn iter_subs(&self) -> Result<impl Iterator<Item = (GlyphId, GlyphId)> + '_, ReadError> { | ||
let coverage = self.coverage()?; | ||
let subs = self.substitute_glyph_ids(); | ||
Ok(coverage.iter().zip(subs.iter().map(|id| id.get()))) | ||
} | ||
} | ||
|
||
impl<'a> MultipleSubstFormat1<'a> { | ||
fn closure_glyphs( | ||
&self, | ||
input: &HashSet<GlyphId>, | ||
output: &mut HashSet<GlyphId>, | ||
) -> Result<(), ReadError> { | ||
let coverage = self.coverage()?; | ||
let sequences = self.sequences(); | ||
for (gid, replacements) in coverage.iter().zip(sequences.iter()) { | ||
let replacements = replacements?; | ||
if input.contains(&gid) { | ||
output.extend( | ||
replacements | ||
.substitute_glyph_ids() | ||
.iter() | ||
.map(|gid| gid.get()), | ||
); | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<'a> AlternateSubstFormat1<'a> { | ||
fn closure_glyphs( | ||
&self, | ||
input: &HashSet<GlyphId>, | ||
output: &mut HashSet<GlyphId>, | ||
) -> Result<(), ReadError> { | ||
let coverage = self.coverage()?; | ||
let alts = self.alternate_sets(); | ||
for (gid, alts) in coverage.iter().zip(alts.iter()) { | ||
let alts = alts?; | ||
if input.contains(&gid) { | ||
output.extend(alts.alternate_glyph_ids().iter().map(|gid| gid.get())); | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<'a> LigatureSubstFormat1<'a> { | ||
fn closure_glyphs( | ||
&self, | ||
input: &HashSet<GlyphId>, | ||
output: &mut HashSet<GlyphId>, | ||
) -> Result<(), ReadError> { | ||
let coverage = self.coverage()?; | ||
let ligs = self.ligature_sets(); | ||
for (gid, lig_set) in coverage.iter().zip(ligs.iter()) { | ||
let lig_set = lig_set?; | ||
if input.contains(&gid) { | ||
for lig in lig_set.ligatures().iter() { | ||
let lig = lig?; | ||
if lig | ||
.component_glyph_ids() | ||
.iter() | ||
.all(|gid| input.contains(&gid.get())) | ||
{ | ||
output.insert(lig.ligature_glyph()); | ||
} | ||
} | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} |