[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)
|
||||
|
||||
|
||||
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):
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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]
|
||||
|
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