[feaLib] Support GPOS type 2, format 2: Class-based kerning tables
This commit is contained in:
parent
26c02e4d95
commit
6254974112
@ -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
|
||||
|
@ -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"})
|
||||
|
9
Lib/fontTools/feaLib/testdata/GPOS_2b.fea
vendored
Normal file
9
Lib/fontTools/feaLib/testdata/GPOS_2b.fea
vendored
Normal 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;
|
119
Lib/fontTools/feaLib/testdata/GPOS_2b.ttx
vendored
Normal file
119
Lib/fontTools/feaLib/testdata/GPOS_2b.ttx
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user