From 9f4cc2f1cb8cdc2d86c102c13a2e9f95f1a44580 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 7 Jul 2020 12:38:15 +0100 Subject: [PATCH] Introduce the concept of a ClassContextualRuleSet Currently unused but will be super helpful for optimizing the formats of contextual lookups. * Splits the rules of a class contextual lookup based on explicit subtable breaks * Returns various properties on the ruleset to help determine appropriate layout format. * (More properties, such as "touched glyphs", planned - will be added when needed.) --- Lib/fontTools/otlLib/builder.py | 59 +++++++++++++++++++++++++++++++++ Tests/otlLib/builder_test.py | 57 +++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 5731f51c4..968a327a6 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -244,11 +244,70 @@ class AlternateSubstBuilder(LookupBuilder): self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ +class ChainContextualRuleset(): + def __init__(self): + self.rules = [] + + def addRule(self, prefix, glyphs, suffix, lookups): + self.rules.append((prefix, glyphs, suffix, lookups)) + + @property + def hasPrefixOrSuffix(self): + # Do we have any prefixes/suffixes? If this is False for all + # rulesets, we can express the whole lookup as GPOS5/GSUB7. + for (prefix, glyphs, suffix, lookups) in self.rules: + if len(prefix) > 0 or len(suffix) > 0: return True + return False + + @property + def hasAnyGlyphClasses(self): + # Do we use glyph classes anywhere in the rules? If this is False + # we can express this subtable as a Format 1. + for (prefix, glyphs, suffix, lookups) in self.rules: + for coverage in (prefix, glyphs, suffix): + if any([len(x) > 1 for x in coverage]): + return True + return False + + def format1Classdefs(self): + PREFIX, GLYPHS, SUFFIX = 0,1,2 + classdefbuilders = [] + for ix in [PREFIX, GLYPHS, SUFFIX]: + context = [] + for r in self.rules: + context.append(r[ix]) + classes = self._classBuilderForContext(context) + if not classes: + return None + classdefbuilders.append(classes) + return classdefbuilders + + def _classBuilderForContext(self, context): + classdefbuilder = ClassDefBuilder(useClass0=False) + for position in context: + for glyphset in position: + if not classdefbuilder.canAdd(glyphset): + return None + classdefbuilder.add(glyphset) + return classdefbuilder + class ChainContextualBuilder(LookupBuilder): def equals(self, other): return (LookupBuilder.equals(self, other) and self.rules == other.rules) + def rulesets(self): + # Return a list of ChainContextRuleset objects, taking explicit + # subtable breaks into account + ruleset = [ ChainContextualRuleset() ] + for (prefix, glyphs, suffix, lookups) in self.rules: + if prefix == self.SUBTABLE_BREAK_: + ruleset.append(ChainContextualRuleset() ) + continue + ruleset[-1].addRule(prefix, glyphs, suffix, lookups) + # Squish any empty subtables + return [x for x in ruleset if len(x.rules) > 0] + def build(self): """Build the lookup. diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py index 667e0826f..27acba70d 100644 --- a/Tests/otlLib/builder_test.py +++ b/Tests/otlLib/builder_test.py @@ -1392,6 +1392,63 @@ def test_stat_infinities(): assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff" +class ChainContextualRulesetTest(object): + def test_makeRulesets(self): + font = ttLib.TTFont() + font.setGlyphOrder(["a","b","c","d","A","B","C","D","E"]) + sb = builder.ChainContextSubstBuilder(font, None) + prefix = [ ["a"], ["b"] ] + input_ = [ ["c"] ] + suffix = [ ] + lookups = [ None ] + sb.rules.append((prefix, input_, suffix, lookups)) + + prefix = [ ["a"], ["d"] ] + sb.rules.append((prefix, input_, suffix, lookups)) + + sb.add_subtable_break(None) + + # Second subtable has some glyph classes + prefix = [ ["A"] ] + input_ = [ ["E"] ] + sb.rules.append((prefix, input_, suffix, lookups)) + input_ = [ ["C","D"] ] + sb.rules.append((prefix, input_, suffix, lookups)) + prefix = [ ["A", "B"] ] + input_ = [ ["E"] ] + sb.rules.append((prefix, input_, suffix, lookups)) + + sb.add_subtable_break(None) + + # Third subtable has no pre/post context + prefix = [] + suffix = [] + sb.rules.append((prefix, input_, suffix, lookups)) + input_ = [ ["C","D"] ] + sb.rules.append((prefix, input_, suffix, lookups)) + + rulesets = sb.rulesets() + assert len(rulesets) == 3 + assert rulesets[0].hasPrefixOrSuffix + assert not rulesets[0].hasAnyGlyphClasses + cd = rulesets[0].format1Classdefs() + assert set(cd[0].classes()[1:]) == set([("d",),("b",),("a",)]) + assert set(cd[1].classes()[1:]) == set([("c",)]) + assert set(cd[2].classes()[1:]) == set() + + assert rulesets[1].hasPrefixOrSuffix + assert rulesets[1].hasAnyGlyphClasses + assert not rulesets[1].format1Classdefs() + + assert not rulesets[2].hasPrefixOrSuffix + assert rulesets[2].hasAnyGlyphClasses + assert rulesets[2].format1Classdefs() + cd = rulesets[2].format1Classdefs() + assert set(cd[0].classes()[1:]) == set() + assert set(cd[1].classes()[1:]) == set([("C","D"), ("E",)]) + assert set(cd[2].classes()[1:]) == set() + + if __name__ == "__main__": import sys