[feaLib] Sort GSUB glyph coverage tables by glyph ID

Before this change, feaLib would sort coverage tables by glyph name,
which is against the OpenType specification.  The current unittests
happen to use only glyphs where the ordering is identical whether
sorting by name or by ID; but I am about to add unittests (for GPOS)
where the ordering is different.

The ordering cannot be enforced by otTables because otTables does
not have access to the font's glyph order; therefore, the sorting
needs to happen inside feaLib.
This commit is contained in:
Sascha Brawer 2015-12-04 11:04:37 +01:00
parent cab0067c7e
commit f45fab8c3a
2 changed files with 58 additions and 39 deletions

View File

@ -44,7 +44,8 @@ class Builder(object):
raise FeatureLibError(
"Within a named lookup block, all rules must be of "
"the same lookup type and flag", location)
self.cur_lookup_ = builder_class(location, self.lookup_flag_)
self.cur_lookup_ = builder_class(
self.font, location, self.lookup_flag_)
self.lookups_.append(self.cur_lookup_)
if self.cur_lookup_name_:
# We are starting a lookup rule inside a named lookup block.
@ -305,7 +306,8 @@ class Builder(object):
class LookupBuilder(object):
def __init__(self, location, table, lookup_type, lookup_flag):
def __init__(self, font, location, table, lookup_type, lookup_flag):
self.font = font
self.location = location
self.table, self.lookup_type = table, lookup_type
self.lookup_flag = lookup_flag
@ -317,37 +319,34 @@ class LookupBuilder(object):
self.table == other.table and
self.lookup_flag == other.lookup_flag)
@staticmethod
def setBacktrackCoverage_(prefix, subtable):
def setBacktrackCoverage_(self, prefix, subtable):
subtable.BacktrackGlyphCount = len(prefix)
subtable.BacktrackCoverage = []
for p in reversed(prefix):
coverage = otTables.BacktrackCoverage()
coverage.glyphs = sorted(list(p))
coverage.glyphs = sorted(list(p), key=self.font.getGlyphID)
subtable.BacktrackCoverage.append(coverage)
@staticmethod
def setLookAheadCoverage_(suffix, subtable):
def setLookAheadCoverage_(self, suffix, subtable):
subtable.LookAheadGlyphCount = len(suffix)
subtable.LookAheadCoverage = []
for s in suffix:
coverage = otTables.LookAheadCoverage()
coverage.glyphs = sorted(list(s))
coverage.glyphs = sorted(list(s), key=self.font.getGlyphID)
subtable.LookAheadCoverage.append(coverage)
@staticmethod
def setInputCoverage_(glyphs, subtable):
def setInputCoverage_(self, glyphs, subtable):
subtable.InputGlyphCount = len(glyphs)
subtable.InputCoverage = []
for g in glyphs:
coverage = otTables.InputCoverage()
coverage.glyphs = sorted(list(g))
coverage.glyphs = sorted(list(g), key=self.font.getGlyphID)
subtable.InputCoverage.append(coverage)
class AlternateSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 3, lookup_flag)
def __init__(self, font, location, lookup_flag):
LookupBuilder.__init__(self, font, location, 'GSUB', 3, lookup_flag)
self.alternates = {}
def equals(self, other):
@ -368,8 +367,8 @@ class AlternateSubstBuilder(LookupBuilder):
class ChainContextSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 6, lookup_flag)
def __init__(self, font, location, lookup_flag):
LookupBuilder.__init__(self, font, location, 'GSUB', 6, lookup_flag)
self.substitutions = [] # (prefix, input, suffix, lookups)
def equals(self, other):
@ -403,8 +402,8 @@ class ChainContextSubstBuilder(LookupBuilder):
class LigatureSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 4, lookup_flag)
def __init__(self, font, location, lookup_flag):
LookupBuilder.__init__(self, font, location, 'GSUB', 4, lookup_flag)
self.ligatures = {} # {('f','f','i'): 'f_f_i'}
def equals(self, other):
@ -444,8 +443,8 @@ class LigatureSubstBuilder(LookupBuilder):
class MultipleSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 2, lookup_flag)
def __init__(self, font, location, lookup_flag):
LookupBuilder.__init__(self, font, location, 'GSUB', 2, lookup_flag)
self.mapping = {}
def equals(self, other):
@ -465,8 +464,8 @@ class MultipleSubstBuilder(LookupBuilder):
class ReverseChainSingleSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 8, lookup_flag)
def __init__(self, font, location, lookup_flag):
LookupBuilder.__init__(self, font, location, 'GSUB', 8, lookup_flag)
self.substitutions = [] # (prefix, suffix, mapping)
def equals(self, other):
@ -482,7 +481,7 @@ class ReverseChainSingleSubstBuilder(LookupBuilder):
lookup.SubTable.append(st)
self.setBacktrackCoverage_(prefix, st)
self.setLookAheadCoverage_(suffix, st)
coverage = sorted(list(mapping.keys()))
coverage = sorted(mapping.keys(), key=self.font.getGlyphID)
st.Coverage = otTables.Coverage()
st.Coverage.glyphs = coverage
st.GlyphCount = len(coverage)
@ -495,8 +494,8 @@ class ReverseChainSingleSubstBuilder(LookupBuilder):
class SingleSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 1, lookup_flag)
def __init__(self, font, location, lookup_flag):
LookupBuilder.__init__(self, font, location, 'GSUB', 1, lookup_flag)
self.mapping = {}
def equals(self, other):

View File

@ -13,6 +13,26 @@ import tempfile
import unittest
def makeTTFont():
glyphs = (
".notdef space slash fraction "
"zero one two three four five six seven eight nine "
"zero.oldstyle one.oldstyle two.oldstyle three.oldstyle "
"four.oldstyle five.oldstyle six.oldstyle seven.oldstyle "
"eight.oldstyle nine.oldstyle onehalf "
"A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "
"a b c d e f g h i j k l m n o p q r s t u v w x y z "
"A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc "
"N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc "
"A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3 "
"d.alt n.end s.end "
"c_h c_k c_s c_t f_f f_f_i f_i f_l o_f_f_i "
).split()
font = TTFont()
font.setGlyphOrder(glyphs)
return font
class BuilderTest(unittest.TestCase):
def __init__(self, methodName):
unittest.TestCase.__init__(self, methodName)
@ -68,12 +88,12 @@ class BuilderTest(unittest.TestCase):
path = self.temp_path(suffix=".fea")
with codecs.open(path, "wb", "utf-8") as outfile:
outfile.write(featureFile)
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(path, font)
return font
def test_alternateSubst(self):
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("GSUB_3.fea"), font)
self.expect_ttx(font, self.getpath("GSUB_3.ttx"))
@ -89,7 +109,7 @@ class BuilderTest(unittest.TestCase):
"} test;")
def test_alternateSubst(self):
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("GSUB_2.fea"), font)
self.expect_ttx(font, self.getpath("GSUB_2.ttx"))
@ -105,7 +125,7 @@ class BuilderTest(unittest.TestCase):
"} test;")
def test_reverseChainingSingleSubst(self):
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("GSUB_8.fea"), font)
self.expect_ttx(font, self.getpath("GSUB_8.ttx"))
@ -121,36 +141,36 @@ class BuilderTest(unittest.TestCase):
def test_spec4h1(self):
# OpenType Feature File specification, section 4.h, example 1.
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("spec4h1.fea"), font)
self.expect_ttx(font, self.getpath("spec4h1.ttx"))
def test_spec5d1(self):
# OpenType Feature File specification, section 5.d, example 1.
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("spec5d1.fea"), font)
self.expect_ttx(font, self.getpath("spec5d1.ttx"))
def test_spec5d2(self):
# OpenType Feature File specification, section 5.d, example 2.
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("spec5d2.fea"), font)
self.expect_ttx(font, self.getpath("spec5d2.ttx"))
def test_spec5fi1(self):
# OpenType Feature File specification, section 5.f.i, example 1.
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("spec5fi1.fea"), font)
self.expect_ttx(font, self.getpath("spec5fi1.ttx"))
def test_spec5h1(self):
# OpenType Feature File specification, section 5.h, example 1.
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("spec5h1.fea"), font)
self.expect_ttx(font, self.getpath("spec5h1.ttx"))
def test_languagesystem(self):
builder = Builder(None, TTFont())
builder = Builder(None, makeTTFont())
builder.add_language_system(None, 'latn', 'FRA')
builder.add_language_system(None, 'cyrl', 'RUS')
builder.start_feature(location=None, name='test')
@ -164,7 +184,7 @@ class BuilderTest(unittest.TestCase):
self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;")
def test_languagesystem_none_specified(self):
builder = Builder(None, TTFont())
builder = Builder(None, makeTTFont())
builder.start_feature(location=None, name='test')
self.assertEqual(builder.language_systems, {('DFLT', 'dflt')})
@ -176,7 +196,7 @@ class BuilderTest(unittest.TestCase):
self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
def test_script(self):
builder = Builder(None, TTFont())
builder = Builder(None, makeTTFont())
builder.start_feature(location=None, name='test')
builder.set_script(location=None, script='cyrl')
self.assertEqual(builder.language_systems,
@ -202,7 +222,7 @@ class BuilderTest(unittest.TestCase):
self.build, "feature size { script latn; } size;")
def test_language(self):
builder = Builder(None, TTFont())
builder = Builder(None, makeTTFont())
builder.add_language_system(None, 'latn', 'FRA ')
builder.start_feature(location=None, name='test')
builder.set_script(location=None, script='cyrl')
@ -234,7 +254,7 @@ class BuilderTest(unittest.TestCase):
self.build, "feature size { language FRA; } size;")
def test_language_required(self):
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("language_required.fea"), font)
self.expect_ttx(font, self.getpath("language_required.ttx"))
@ -256,7 +276,7 @@ class BuilderTest(unittest.TestCase):
"} test;")
def test_lookup(self):
font = TTFont()
font = makeTTFont()
addOpenTypeFeatures(self.getpath("lookup.fea"), font)
self.expect_ttx(font, self.getpath("lookup.ttx"))