[feaLib] Implement GPOS type 1, Single Adjustment Positioning

This commit is contained in:
Sascha Brawer 2015-12-04 11:16:43 +01:00
parent f45fab8c3a
commit b99f1c9af4
7 changed files with 325 additions and 1 deletions

View File

@ -167,6 +167,16 @@ class ScriptStatement(Statement):
builder.set_script(self.location, self.script) 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): class SubtableStatement(Statement):
def __init__(self, location): def __init__(self, location):
Statement.__init__(self, location) Statement.__init__(self, location)
@ -191,6 +201,48 @@ class ValueRecord(Statement):
Statement.__init__(self, location) Statement.__init__(self, location)
self.xPlacement, self.yPlacement = (xPlacement, yPlacement) self.xPlacement, self.yPlacement = (xPlacement, yPlacement)
self.xAdvance, self.yAdvance = (xAdvance, yAdvance) 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): class ValueRecordDefinition(Statement):

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.parser import Parser from fontTools.feaLib.parser import Parser
from fontTools.ttLib import getTableClass from fontTools.ttLib import getTableClass
from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables import otBase, otTables
import warnings import warnings
@ -304,6 +304,43 @@ class Builder(object):
location) location)
lookup.mapping[from_glyph] = to_glyph 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): class LookupBuilder(object):
def __init__(self, font, location, table, lookup_type, lookup_flag): def __init__(self, font, location, table, lookup_type, lookup_flag):
@ -512,3 +549,50 @@ class SingleSubstBuilder(LookupBuilder):
lookup.LookupType = self.lookup_type lookup.LookupType = self.lookup_type
lookup.SubTableCount = len(lookup.SubTable) lookup.SubTableCount = len(lookup.SubTable)
return lookup 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

View File

@ -139,6 +139,17 @@ class BuilderTest(unittest.TestCase):
" sub e by e.fina;" " sub e by e.fina;"
"} test;") "} 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): def test_spec4h1(self):
# OpenType Feature File specification, section 4.h, example 1. # OpenType Feature File specification, section 4.h, example 1.
font = makeTTFont() font = makeTTFont()

View File

@ -200,6 +200,14 @@ class Parser(object):
self.lookups_.define(name, block) self.lookups_.define(name, block)
return 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): def parse_script_(self):
assert self.is_cur_keyword_("script") assert self.is_cur_keyword_("script")
location, script = self.cur_token_location_, self.expect_script_tag_() location, script = self.cur_token_location_, self.expect_script_tag_()
@ -402,6 +410,8 @@ class Parser(object):
statements.append(self.parse_language_()) statements.append(self.parse_language_())
elif self.is_cur_keyword_("lookup"): elif self.is_cur_keyword_("lookup"):
statements.append(self.parse_lookup_(vertical)) 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"): elif self.is_cur_keyword_("script"):
statements.append(self.parse_script_()) statements.append(self.parse_script_())
elif (self.is_cur_keyword_({"sub", "substitute", elif (self.is_cur_keyword_({"sub", "substitute",

View File

@ -256,6 +256,28 @@ class ParserTest(unittest.TestCase):
FeatureLibError, 'Unknown lookup "Huh"', FeatureLibError, 'Unknown lookup "Huh"',
self.parse, "feature liga {lookup Huh;} liga;") 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): def test_rsub_format_a(self):
doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;") doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;")
rsub = doc.statements[0].statements[0] rsub = doc.statements[0].statements[0]

View File

@ -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;

114
Lib/fontTools/feaLib/testdata/GPOS_1.ttx vendored Normal file
View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GSUB>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=2 -->
<FeatureRecord index="0">
<FeatureTag value="kern"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="1">
<FeatureTag value="vkrn"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="1"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=2 -->
<Lookup index="0">
<!-- LookupType=1 -->
<LookupFlag value="0"/>
<!-- SubTableCount=5 -->
<SinglePos index="0" Format="1">
<Coverage>
<Glyph value="one"/>
<Glyph value="two"/>
<Glyph value="three"/>
<Glyph value="five"/>
</Coverage>
<ValueFormat value="5"/>
<Value XPlacement="-80" XAdvance="-160"/>
</SinglePos>
<SinglePos index="1" Format="1">
<Coverage>
<Glyph value="four"/>
</Coverage>
<ValueFormat value="4"/>
<Value XAdvance="400"/>
</SinglePos>
<SinglePos index="2" Format="1">
<Coverage>
<Glyph value="six"/>
</Coverage>
<ValueFormat value="4"/>
<Value XAdvance="-200"/>
</SinglePos>
<SinglePos index="3" Format="1">
<Coverage>
<Glyph value="seven"/>
<Glyph value="eight"/>
<Glyph value="nine"/>
</Coverage>
<ValueFormat value="4"/>
<Value XAdvance="-100"/>
</SinglePos>
<SinglePos index="4" Format="1">
<Coverage>
<Glyph value="A"/>
<Glyph value="B"/>
</Coverage>
<ValueFormat value="15"/>
<Value XPlacement="1" YPlacement="2" XAdvance="3" YAdvance="4"/>
</SinglePos>
</Lookup>
<Lookup index="1">
<!-- LookupType=1 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SinglePos index="0" Format="1">
<Coverage>
<Glyph value="A"/>
</Coverage>
<ValueFormat value="8"/>
<Value YAdvance="-100"/>
</SinglePos>
</Lookup>
</LookupList>
</GPOS>
</ttFont>