[feaLib] Implement GPOS type 1, Single Adjustment Positioning
This commit is contained in:
parent
f45fab8c3a
commit
b99f1c9af4
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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",
|
||||||
|
@ -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]
|
||||||
|
31
Lib/fontTools/feaLib/testdata/GPOS_1.fea
vendored
Normal file
31
Lib/fontTools/feaLib/testdata/GPOS_1.fea
vendored
Normal 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
114
Lib/fontTools/feaLib/testdata/GPOS_1.ttx
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user