[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)
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):

View File

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

View File

@ -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()

View File

@ -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",

View File

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

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>