diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 7ff5c53f4..23c7a3c31 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -167,6 +167,16 @@ class ScriptStatement(Statement): builder.set_script(self.location, self.script) +class SingleAdjustmentPositioning(Statement): + def __init__(self, location, glyphclass, valuerecord): + Statement.__init__(self, location) + self.glyphclass, self.valuerecord = glyphclass, valuerecord + + def build(self, builder): + for glyph in self.glyphclass: + builder.add_single_pos(self.location, glyph, self.valuerecord) + + class SubtableStatement(Statement): def __init__(self, location): Statement.__init__(self, location) @@ -191,6 +201,48 @@ class ValueRecord(Statement): Statement.__init__(self, location) self.xPlacement, self.yPlacement = (xPlacement, yPlacement) self.xAdvance, self.yAdvance = (xAdvance, yAdvance) + self.xPlaDevice, self.yPlaDevice = (0, 0) + self.xAdvDevice, self.yAdvDevice = (0, 0) + + def __eq__(self, other): + return (self.xPlacement == other.xPlacement and + self.yPlacement == other.yPlacement and + self.xAdvance == other.xAdvance and + self.yAdvance == other.yAdvance and + self.xPlaDevice == other.xPlaDevice and + self.xAdvDevice == other.xAdvDevice) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return (hash(self.xPlacement) ^ hash(self.yPlacement) ^ + hash(self.xAdvance) ^ hash(self.yAdvance) ^ + hash(self.xPlaDevice) ^ hash(self.yPlaDevice) ^ + hash(self.xAdvDevice) ^ hash(self.yAdvDevice)) + + def makeString(self, vertical): + x, y = self.xPlacement, self.yPlacement + xAdvance, yAdvance = self.xAdvance, self.yAdvance + xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice + xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice + + # Try format A, if possible. + if x == 0 and y == 0: + if xAdvance == 0 and vertical: + return str(yAdvance) + elif yAdvance == 0 and not vertical: + return str(xAdvance) + + # Try format B, if possible. + if (xPlaDevice == 0 and yPlaDevice == 0 and + xAdvDevice == 0 and yAdvDevice == 0): + return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance) + + # Last resort is format C. + return "<%s %s %s %s %s %s %s %s %s %s>" % ( + x, y, xAdvance, yAdvance, + xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice) class ValueRecordDefinition(Statement): diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index 862892924..173e7fc27 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.parser import Parser from fontTools.ttLib import getTableClass -from fontTools.ttLib.tables import otTables +from fontTools.ttLib.tables import otBase, otTables import warnings @@ -304,6 +304,43 @@ class Builder(object): location) lookup.mapping[from_glyph] = to_glyph + def add_single_pos(self, location, glyph, valuerecord): + lookup = self.get_lookup_(location, SinglePosBuilder) + curValue = lookup.mapping.get(glyph) + if curValue is not None and curValue != valuerecord: + otherLoc = valuerecord.location + raise FeatureLibError( + 'Already defined different position for glyph "%s" at %s:%d:%d' + % (glyph, otherLoc[0], otherLoc[1], otherLoc[2]), + location) + lookup.mapping[glyph] = valuerecord + + +def makeOpenTypeValueRecord(v): + """ast.ValueRecord --> (otBase.ValueRecord, int ValueFormat)""" + vr = otBase.ValueRecord() + if v.xPlacement: + vr.XPlacement = v.xPlacement + if v.yPlacement: + vr.YPlacement = v.yPlacement + if v.xAdvance: + vr.XAdvance = v.xAdvance + if v.yAdvance: + vr.YAdvance = v.yAdvance + if v.xPlaDevice: + vr.XPlaDevice = v.xPlaDevice + if v.yPlaDevice: + vr.YPlaDevice = v.yPlaDevice + if v.xAdvDevice: + vr.XAdvDevice = v.xAdvDevice + if v.yAdvDevice: + vr.YAdvDevice = v.yAdvDevice + vrMask = 0 + for mask, name, _, _ in otBase.valueRecordFormat: + if getattr(vr, name, 0) != 0: + vrMask |= mask + return vr, vrMask + class LookupBuilder(object): def __init__(self, font, location, table, lookup_type, lookup_flag): @@ -512,3 +549,50 @@ class SingleSubstBuilder(LookupBuilder): lookup.LookupType = self.lookup_type lookup.SubTableCount = len(lookup.SubTable) return lookup + + +class SinglePosBuilder(LookupBuilder): + def __init__(self, font, location, lookup_flag): + LookupBuilder.__init__(self, font, location, 'GPOS', 1, lookup_flag) + self.mapping = {} # glyph -> ast.ValueRecord + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.mapping == other.mapping) + + def build(self): + subtables = [] + + # If multiple glyphs have the same ValueRecord, they can go into + # the same subtable which saves space. Therefore, we first build + # a reverse mapping from ValueRecord to glyph coverage. + values = {} + for glyph, valuerecord in self.mapping.items(): + values.setdefault(valuerecord, []).append(glyph) + + # For compliance with the OpenType specification, + # we sort the glyph coverage by glyph ID. + for glyphs in values.values(): + glyphs.sort(key=self.font.getGlyphID) + + # To make the ordering of our subtables deterministic, + # we sort subtables by the first glyph ID in their coverage. + # Not doing this would be OK for OpenType, but testing the + # compiler would be harder with non-deterministic output. + values = list(values.items()) + values.sort(key=lambda x: self.font.getGlyphID(x[1][0])) + + for valrec, glyphs in values: + st = otTables.SinglePos() + subtables.append(st) + st.Format = 1 + st.Coverage = otTables.Coverage() + st.Coverage.glyphs = glyphs + st.Value, st.ValueFormat = makeOpenTypeValueRecord(valrec) + + lookup = otTables.Lookup() + lookup.SubTable = subtables + lookup.LookupFlag = self.lookup_flag + lookup.LookupType = self.lookup_type + lookup.SubTableCount = len(lookup.SubTable) + return lookup diff --git a/Lib/fontTools/feaLib/builder_test.py b/Lib/fontTools/feaLib/builder_test.py index 61e16bcc7..2234fbc70 100644 --- a/Lib/fontTools/feaLib/builder_test.py +++ b/Lib/fontTools/feaLib/builder_test.py @@ -139,6 +139,17 @@ class BuilderTest(unittest.TestCase): " sub e by e.fina;" "} test;") + def test_singlePos_redefinition(self): + self.assertRaisesRegex( + FeatureLibError, + "Already defined different position for glyph \"A\"", + self.build, "feature test { pos A 123; pos A 456; } test;") + + def test_GPOS_type1(self): + font = makeTTFont() + addOpenTypeFeatures(self.getpath("GPOS_1.fea"), font) + self.expect_ttx(font, self.getpath("GPOS_1.ttx")) + def test_spec4h1(self): # OpenType Feature File specification, section 4.h, example 1. font = makeTTFont() diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 9c2be7ff6..f84686bf5 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -200,6 +200,14 @@ class Parser(object): self.lookups_.define(name, block) return block + def parse_position_(self, vertical): + assert self.cur_token_ in {"position", "pos"} + location = self.cur_token_location_ + glyphclass = self.parse_glyphclass_(accept_glyphname=True) + valuerec = self.parse_valuerecord_(vertical) + self.expect_symbol_(";") + return ast.SingleAdjustmentPositioning(location, glyphclass, valuerec) + def parse_script_(self): assert self.is_cur_keyword_("script") location, script = self.cur_token_location_, self.expect_script_tag_() @@ -402,6 +410,8 @@ class Parser(object): statements.append(self.parse_language_()) elif self.is_cur_keyword_("lookup"): statements.append(self.parse_lookup_(vertical)) + elif self.is_cur_keyword_({"pos", "position"}): + statements.append(self.parse_position_(vertical)) elif self.is_cur_keyword_("script"): statements.append(self.parse_script_()) elif (self.is_cur_keyword_({"sub", "substitute", diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 05dc7d87f..9cd4f8142 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -256,6 +256,28 @@ class ParserTest(unittest.TestCase): FeatureLibError, 'Unknown lookup "Huh"', self.parse, "feature liga {lookup Huh;} liga;") + def test_gpos_type1_glyph(self): + doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.SingleAdjustmentPositioning) + self.assertEqual(pos.glyphclass, {"one"}) + self.assertEqual(pos.valuerecord.makeString(vertical=False), + "<1 2 3 4>") + + def test_gpos_type1_glyphclass_horizontal(self): + doc = self.parse("feature kern {pos [one two] -300;} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.SingleAdjustmentPositioning) + self.assertEqual(pos.glyphclass, {"one", "two"}) + self.assertEqual(pos.valuerecord.makeString(vertical=False), "-300") + + def test_gpos_type1_glyphclass_vertical(self): + doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.SingleAdjustmentPositioning) + self.assertEqual(pos.glyphclass, {"one", "two"}) + self.assertEqual(pos.valuerecord.makeString(vertical=True), "-300") + def test_rsub_format_a(self): doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;") rsub = doc.statements[0].statements[0] diff --git a/Lib/fontTools/feaLib/testdata/GPOS_1.fea b/Lib/fontTools/feaLib/testdata/GPOS_1.fea new file mode 100644 index 000000000..4867974b5 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/GPOS_1.fea @@ -0,0 +1,31 @@ +languagesystem DFLT dflt; + +@sevenEightNine = [seven eight nine]; + +feature kern { + position [one two three] <-80 0 -160 0>; + position A <1 2 3 4>; + position B <1 2 3 4>; + position four 400; + position five <-80 0 -160 0>; + position six -200; + position @sevenEightNine -100; + + # The AFDKO makeotf tool accepts re-definitions of previously defined + # single adjustment positionings, provided the re-definition is using + # the same value. We replicate this behavior. + position four 400; + position four <0 0 400 0>; + position nine -100; +} kern; + +# According to the OpenType Feature File specification section 2.e.iv, +# the following should be interpreted as vertical advance adjustment +# because -100 (a value record format A) appears within a ‘vkrn’ feature. +# However, the AFDKO makeotf tool v2.0.90 (built on Nov 19, 2015) still +# makes it a horizontal advance adjustment. In our implementation, +# we follow the specification, so we produce different output than makeotf. +# https://github.com/adobe-type-tools/afdko/issues/85 +feature vkrn { + position A -100; +} vkrn; diff --git a/Lib/fontTools/feaLib/testdata/GPOS_1.ttx b/Lib/fontTools/feaLib/testdata/GPOS_1.ttx new file mode 100644 index 000000000..d019b8097 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/GPOS_1.ttx @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +