[feaLib] Support GPOS type 2, format 2: Class-based kerning tables

This commit is contained in:
Sascha Brawer 2015-12-23 11:35:49 +01:00
parent 26c02e4d95
commit 6254974112
4 changed files with 274 additions and 25 deletions

View File

@ -471,8 +471,8 @@ class Builder(object):
def add_class_pair_pos(self, location, glyphclass1, value1,
glyphclass2, value2):
raise FeatureLibError("Class-based kerning is not yet implemented",
location)
lookup = self.get_lookup_(location, ClassPairPosBuilder)
lookup.add_pair(location, glyphclass1, value1, glyphclass2, value2)
def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
lookup = self.get_lookup_(location, SpecificPairPosBuilder)
@ -1046,6 +1046,107 @@ class SingleSubstBuilder(LookupBuilder):
return self.buildLookup_([subtable])
class ClassPairPosSubtableBuilder(object):
def __init__(self, builder, valueFormat1, valueFormat2):
self.builder_ = builder
self.classDef1_, self.classDef2_ = None, None
self.coverage_ = set()
self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2)
self.valueFormat1_, self.valueFormat2_ = valueFormat1, valueFormat2
self.forceSubtableBreak_ = False
self.subtables_ = []
def addPair(self, gc1, value1, gc2, value2):
mergeable = (not self.forceSubtableBreak_ and
self.classDef1_ is not None and
self.classDef1_.canAdd(gc1) and
self.classDef2_ is not None and
self.classDef2_.canAdd(gc2))
if not mergeable:
self.flush_()
self.classDef1_ = ClassDefBuilder(otTables.ClassDef1)
self.classDef2_ = ClassDefBuilder(otTables.ClassDef2)
self.coverage_ = set()
self.values_ = {}
self.classDef1_.add(gc1)
self.classDef2_.add(gc2)
self.coverage_.update(gc1)
self.values_[(gc1, gc2)] = (value1, value2)
def addSubtableBreak(self):
self.forceSubtableBreak_ = True
def subtables(self):
self.flush_()
return self.subtables_
def flush_(self):
if self.classDef1_ is None or self.classDef2_ is None:
return
st = otTables.PairPos()
st.Format = 2
st.Coverage = self.builder_.buildCoverage_(self.coverage_)
st.ValueFormat1 = self.valueFormat1_
st.ValueFormat2 = self.valueFormat2_
st.ClassDef1 = self.classDef1_.build(omit_class_zero=True)
st.ClassDef2 = self.classDef2_.build(omit_class_zero=False)
classes1 = self.classDef1_.classes()
classes2 = self.classDef2_.classes()
st.Class1Count, st.Class2Count = len(classes1), len(classes2)
st.Class1Record = []
for c1 in classes1:
rec1 = otTables.Class1Record()
rec1.Class2Record = []
st.Class1Record.append(rec1)
for c2 in classes2:
rec2 = otTables.Class2Record()
val1, val2 = self.values_.get((c1, c2), (None, None))
rec2.Value1, rec2.Value2 = val1, val2
rec1.Class2Record.append(rec2)
self.subtables_.append(st)
class ClassPairPosBuilder(LookupBuilder):
SUBTABLE_BREAK_ = "SUBTABLE_BREAK"
def __init__(self, font, location):
LookupBuilder.__init__(self, font, location, 'GPOS', 2)
self.pairs = [] # [(location, gc1, value1, gc2, value2)*]
def add_pair(self, location, glyphclass1, value1, glyphclass2, value2):
self.pairs.append((location, glyphclass1, value1, glyphclass2, value2))
def add_subtable_break(self, location):
self.pairs.append((location,
self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_))
def equals(self, other):
return (LookupBuilder.equals(self, other) and
self.pairs == other.pairs)
def build(self):
builders = {}
builder = None
for location, glyphclass1, value1, glyphclass2, value2 in self.pairs:
if glyphclass1 is self.SUBTABLE_BREAK_:
if builder is not None:
builder.addSubtableBreak()
continue
val1, valFormat1 = makeOpenTypeValueRecord(value1)
val2, valFormat2 = makeOpenTypeValueRecord(value2)
builder = builders.get((valFormat1, valFormat2))
if builder is None:
builder = ClassPairPosSubtableBuilder(
self, valFormat1, valFormat2)
builders[(valFormat1, valFormat2)] = builder
builder.addPair(glyphclass1, val1, glyphclass2, val2)
subtables = []
for key in sorted(builders.keys()):
subtables.extend(builders[key].subtables())
return self.buildLookup_(subtables)
class SinglePosBuilder(LookupBuilder):
def __init__(self, font, location):
LookupBuilder.__init__(self, font, location, 'GPOS', 1)
@ -1126,38 +1227,47 @@ class SinglePosBuilder(LookupBuilder):
class ClassDefBuilder(object):
"""Helper for building ClassDef tables."""
def __init__(self, otClass):
self.classes = set()
self.glyphs = {}
self.otClass = otClass
self.classes_ = set()
self.glyphs_ = {}
self.otClass_ = otClass
def canAdd(self, glyphs):
glyphs = frozenset(glyphs)
if glyphs in self.classes:
if glyphs in self.classes_:
return True
for glyph in glyphs:
if glyph in self.glyphs:
if glyph in self.glyphs_:
return False
return True
def add(self, glyphs):
glyphs = frozenset(glyphs)
if glyphs in self.classes:
if glyphs in self.classes_:
return
self.classes.add(glyphs)
self.classes_.add(glyphs)
for glyph in glyphs:
assert glyph not in self.glyphs
self.glyphs[glyph] = glyphs
assert glyph not in self.glyphs_
self.glyphs_[glyph] = glyphs
def build(self):
def classes(self):
# In ClassDef1 tables, class id #0 does not need to be encoded
# because zero is the default. Therefore, we use id #0 for the
# glyph class that has the largest number of members.
#
# TODO: Instead of counting the number of glyphs in each class,
# we should determine the encoded size. If the glyphs in a large
# class form a contiguous range, the encoding is actually quite
# compact, whereas a non-contiguous set might need a lot of
# bytes in the output file.
return sorted(self.classes_, key=len, reverse=True)
def build(self, omit_class_zero):
glyphClasses = {}
# Class id #0 does not need to be encoded because zero is the default
# when no class is specified. Therefore, we use id #0 for the glyph
# class that has the largest number of members.
classes = sorted(self.classes, key=len, reverse=True)
for classID, glyphs in enumerate(classes):
if classID != 0:
for glyph in glyphs:
glyphClasses[glyph] = classID
classDef = self.otClass()
for classID, glyphs in enumerate(self.classes()):
if classID == 0 and omit_class_zero:
continue
for glyph in glyphs:
glyphClasses[glyph] = classID
classDef = self.otClass_()
classDef.classDefs = glyphClasses
return classDef

View File

@ -168,7 +168,7 @@ class BuilderTest(unittest.TestCase):
self.expect_ttx(font, self.getpath("%s.ttx" % name))
def test_GPOS(self):
for name in "1 2 3 4 5 6 8".split():
for name in "1 2 2b 3 4 5 6 8".split():
font = makeTTFont()
addOpenTypeFeatures(self.getpath("GPOS_%s.fea" % name), font)
self.expect_ttx(font, self.getpath("GPOS_%s.ttx" % name))
@ -318,16 +318,27 @@ class ClassDefBuilderTest(unittest.TestCase):
builder.add({"a", "b"})
builder.add({"c"})
builder.add({"e", "f", "g", "h"})
cdef = builder.build()
cdef = builder.build(omit_class_zero=True)
self.assertIsInstance(cdef, otTables.ClassDef2)
# The largest class {"e", "f", "g", "h"} should become class ID #0.
# Zero is the default class ID, so it does not get encoded at all.
self.assertEqual(cdef.classDefs, {
"a": 1,
"b": 1,
"c": 2
})
cdef = builder.build(omit_class_zero=False)
self.assertIsInstance(cdef, otTables.ClassDef2)
self.assertEqual(cdef.classDefs, {
"a": 1,
"b": 1,
"c": 2,
"e": 0,
"f": 0,
"g": 0,
"h": 0,
})
def test_canAdd(self):
b = ClassDefBuilder(otTables.ClassDef1)
b.add({"a", "b", "c", "d"})

View File

@ -0,0 +1,9 @@
@PUNC = [comma semicolon period];
feature kern {
pos [A] @PUNC 1;
pos [B C] [comma] 2;
pos [D E F] [comma] 3;
pos [D E F] [semicolon period] 4;
pos [G] @PUNC <5 5 5 5>;
} kern;

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="kern"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<!-- LookupType=2 -->
<LookupFlag value="0"/>
<!-- SubTableCount=3 -->
<PairPos index="0" Format="2">
<Coverage>
<Glyph value="A"/>
</Coverage>
<ValueFormat1 value="4"/>
<ValueFormat2 value="0"/>
<ClassDef1>
</ClassDef1>
<ClassDef2>
<ClassDef glyph="comma" class="0"/>
<ClassDef glyph="period" class="0"/>
<ClassDef glyph="semicolon" class="0"/>
</ClassDef2>
<!-- Class1Count=1 -->
<!-- Class2Count=1 -->
<Class1Record index="0">
<Class2Record index="0">
<Value1 XAdvance="1"/>
</Class2Record>
</Class1Record>
</PairPos>
<PairPos index="1" Format="2">
<Coverage>
<Glyph value="B"/>
<Glyph value="C"/>
<Glyph value="D"/>
<Glyph value="E"/>
<Glyph value="F"/>
</Coverage>
<ValueFormat1 value="4"/>
<ValueFormat2 value="0"/>
<ClassDef1>
<ClassDef glyph="B" class="1"/>
<ClassDef glyph="C" class="1"/>
</ClassDef1>
<ClassDef2>
<ClassDef glyph="comma" class="1"/>
<ClassDef glyph="period" class="0"/>
<ClassDef glyph="semicolon" class="0"/>
</ClassDef2>
<!-- Class1Count=2 -->
<!-- Class2Count=2 -->
<Class1Record index="0">
<Class2Record index="0">
<Value1 XAdvance="4"/>
</Class2Record>
<Class2Record index="1">
<Value1 XAdvance="3"/>
</Class2Record>
</Class1Record>
<Class1Record index="1">
<Class2Record index="0">
</Class2Record>
<Class2Record index="1">
<Value1 XAdvance="2"/>
</Class2Record>
</Class1Record>
</PairPos>
<PairPos index="2" Format="2">
<Coverage>
<Glyph value="G"/>
</Coverage>
<ValueFormat1 value="15"/>
<ValueFormat2 value="0"/>
<ClassDef1>
</ClassDef1>
<ClassDef2>
<ClassDef glyph="comma" class="0"/>
<ClassDef glyph="period" class="0"/>
<ClassDef glyph="semicolon" class="0"/>
</ClassDef2>
<!-- Class1Count=1 -->
<!-- Class2Count=1 -->
<Class1Record index="0">
<Class2Record index="0">
<Value1 XPlacement="5" YPlacement="5" XAdvance="5" YAdvance="5"/>
</Class2Record>
</Class1Record>
</PairPos>
</Lookup>
</LookupList>
</GPOS>
</ttFont>