Skip to content

Commit

Permalink
[read-fonts] Basic closure of glyphs over GSUB
Browse files Browse the repository at this point in the history
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
cmyr committed Feb 7, 2024
1 parent 74cb7c1 commit bd0e333
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 0 deletions.
2 changes: 2 additions & 0 deletions read-fonts/src/tables/gsub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub use super::layout::{
};
use super::layout::{ExtensionLookup, LookupFlag, Subtables};

#[cfg(feature = "std")]
mod closure;
#[cfg(test)]
#[path = "../tests/test_gsub.rs"]
mod tests;
Expand Down
188 changes: 188 additions & 0 deletions read-fonts/src/tables/gsub/closure.rs
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(())
}
}

0 comments on commit bd0e333

Please sign in to comment.