[AAT] Support mort and morx tables with non-contextual substitutions

Other metamorphosis types are not yet supported and will raise an error
upon decompilation. The TTX tool catches the error and continues to emit
a hexdump of the table contents, just as before this change.
This commit is contained in:
Sascha Brawer 2017-08-29 12:52:30 +02:00
parent 7bb171ed4a
commit 90f257cc60
9 changed files with 299 additions and 18 deletions

View File

@ -71,6 +71,8 @@ def _moduleFinderHint():
from . import _l_t_a_g
from . import _m_a_x_p
from . import _m_e_t_a
from . import _m_o_r_t
from . import _m_o_r_x
from . import _n_a_m_e
from . import _o_p_b_d
from . import _p_o_s_t

View File

@ -0,0 +1,8 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from .otBase import BaseTTXConverter
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html
class table__m_o_r_t(BaseTTXConverter):
pass

View File

@ -0,0 +1,8 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from .otBase import BaseTTXConverter
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
class table__m_o_r_x(BaseTTXConverter):
pass

View File

@ -45,6 +45,10 @@ def buildConverters(tableSpec, tableNamespace):
converterClass = Struct
else:
converterClass = eval(tp, tableNamespace, converterMapping)
if tp in ('MortChain', 'MortSubtable',
'MorxChain', 'MorxSubtable'):
tableClass = tableNamespace.get(tp)
else:
tableClass = tableNamespace.get(tableName)
if tableClass is not None:
conv = converterClass(name, repeat, aux, tableClass=tableClass)
@ -967,8 +971,10 @@ converterMapping = {
"VarIdxMapValue": VarIdxMapValue,
"VarDataValue": VarDataValue,
# AAT
"MorphChain": StructWithLength,
"MorphSubtable":StructWithLength,
"MortChain": StructWithLength,
"MortSubtable": StructWithLength,
"MorxChain": StructWithLength,
"MorxSubtable": StructWithLength,
# "Template" types
"AATLookup": lambda C: partial(AATLookup, tableClass=C),
"OffsetTo": lambda C: partial(Table, tableClass=C),

View File

@ -1289,25 +1289,50 @@ otData = [
#
# morx
# mort
#
# TODO: use 'struct' when field.type == field.name
('mort', [
('Version', 'Version', None, None, 'Version of the mort table.'),
('uint32', 'MorphChainCount', None, None, 'Number of metamorphosis chains.'),
('MortChain', 'MorphChain', 'MorphChainCount', 0, 'Array of metamorphosis chains.'),
]),
('MortChain', [
('Flags32', 'DefaultFlags', None, None, 'The default specification for subtables.'),
('uint32', 'StructLength', None, None, 'Total byte count, including this header; must be a multiple of 4.'),
('uint16', 'MorphFeatureCount', None, None, 'Number of metamorphosis feature entries.'),
('uint16', 'MorphSubtableCount', None, None, 'The number of subtables in the chain.'),
('struct', 'MorphFeature', 'MorphFeatureCount', 0, 'Array of metamorphosis features.'),
('MortSubtable', 'MorphSubtable', 'MorphSubtableCount', 0, 'Array of metamorphosis subtables.'),
]),
('MortSubtable', [
('uint16', 'StructLength', None, None, 'Total subtable length, including this header.'),
('uint8', 'CoverageFlags', None, None, 'Most significant byte of coverage flags.'),
('uint8', 'MorphType', None, None, 'Subtable type.'),
('Flags32', 'SubFeatureFlags', None, None, 'The 32-bit mask identifying which subtable this is (the subtable being executed if the AND of this value and the processed defaultFlags is nonzero).'),
('SubStruct', 'SubStruct', None, None, 'SubTable.'),
]),
#
# morx
#
('morx', [
('uint16', 'Version', None, None, 'Version of the morx table.'),
('uint16', 'Reserved', None, None, 'Reserved (set to zero).'),
('uint32', 'ChainCount', None, None, 'Number of MorphChains.'),
('MorphChain', 'MorphChain', 'ChainCount', 0, 'Array of MorphChains.'),
('uint32', 'MorphChainCount', None, None, 'Number of extended metamorphosis chains.'),
('MorxChain', 'MorphChain', 'MorphChainCount', 0, 'Array of extended metamorphosis chains.'),
]),
('MorphChain', [
('MorxChain', [
('Flags32', 'DefaultFlags', None, None, 'The default specification for subtables.'),
('uint32', 'StructLength', None, None, 'Total byte count, including this header; must be a multiple of 4.'),
('uint32', 'MorphFeatureCount', None, None, 'Number of feature subtable entries.'),
('uint32', 'MorphSubtableCount', None, None, 'The number of subtables in the chain.'),
('MorphFeature', 'MorphFeature', 'MorphFeatureCount', 0, 'Array of MorphFeatures.'),
('MorphSubtable', 'MorphSubtable', 'MorphSubtableCount', 0, 'Array of MorphSubtables.'),
('MorphFeature', 'MorphFeature', 'MorphFeatureCount', 0, 'Array of metamorphosis features.'),
('MorxSubtable', 'MorphSubtable', 'MorphSubtableCount', 0, 'Array of extended metamorphosis subtables.'),
]),
('MorphFeature', [
@ -1320,7 +1345,7 @@ otData = [
# Apple TrueType Reference Manual, chapter “The morx table”,
# section “Metamorphosis Subtables”.
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
('MorphSubtable', [
('MorxSubtable', [
('uint32', 'StructLength', None, None, 'Total subtable length, including this header.'),
('uint8', 'CoverageFlags', None, None, 'Most significant byte of coverage flags.'),
('uint16', 'Reserved', None, None, 'Unused.'),
@ -1353,7 +1378,7 @@ otData = [
]),
('NoncontextualMorph', [
('AATLookup(GlyphID)', 'mapping', None, None, 'The noncontextual glyph substitution table.'),
('AATLookup(GlyphID)', 'Substitution', None, None, 'The noncontextual glyph substitution table.'),
]),
('InsertionMorph', [

View File

@ -991,13 +991,16 @@ def _buildClasses():
8: ChainContextPos,
9: ExtensionPos,
},
'mort': {
4: NoncontextualMorph,
},
'morx': {
0: RearrangementMorph,
1: ContextualMorph,
2: LigatureMorph,
# 0: RearrangementMorph,
# 1: ContextualMorph,
# 2: LigatureMorph,
# 3: Reserved,
4: NoncontextualMorph,
5: InsertionMorph,
# 5: InsertionMorph,
},
}
lookupTypes['JSTF'] = lookupTypes['GPOS'] # JSTF contains GPOS

View File

@ -102,8 +102,8 @@ The following tables are currently supported:
OS/2, SING, STAT, SVG, TSI0, TSI1, TSI2, TSI3, TSI5, TSIB, TSID,
TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX, VORG, VVAR, avar, bsln, cmap,
cvar, cvt, feat, fpgm, fvar, gasp, glyf, gvar, hdmx, head, hhea,
hmtx, kern, lcar, loca, ltag, maxp, meta, name, opbd, post, prep,
prop, sbix, trak, vhea and vmtx
hmtx, kern, lcar, loca, ltag, maxp, meta, mort, morx, name, opbd,
post, prep, prop, sbix, trak, vhea and vmtx
.. end table list
Other tables are dumped as hexadecimal data.

View File

@ -0,0 +1,115 @@
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.testTools import FakeFont, getXML, parseXML
from fontTools.misc.textTools import deHexStr, hexStr
from fontTools.ttLib import newTable
import unittest
# Glyph Metamorphosis Table Examples
# Example 1: Non-contextual Glyph Substitution
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html
# The example given by Apple's 'mort' specification is suboptimally
# encoded: it uses AAT lookup format 6 even though format 8 would be
# more compact. Because our encoder always uses the most compact
# encoding, this breaks our round-trip testing. Therefore, we changed
# the example to use GlyphID 13 instead of 12 for the 'parenright'
# character; the non-contiguous glyph range for the AAT lookup makes
# format 6 to be most compact.
MORT_NONCONTEXTUAL_DATA = deHexStr(
'0001 0000 ' # 0: Version=1.0
'0000 0001 ' # 4: MorphChainCount=1
'0000 0001 ' # 8: DefaultFlags=1
'0000 0050 ' # 12: StructLength=80
'0003 0001 ' # 16: MorphFeatureCount=3, MorphSubtableCount=1
'0004 0000 ' # 20: Feature[0].FeatureType=4/VertSubst, .FeatureSetting=on
'0000 0001 ' # 24: Feature[0].EnableFlags=0x00000001
'FFFF FFFF ' # 28: Feature[0].DisableFlags=0xFFFFFFFF
'0004 0001 ' # 32: Feature[1].FeatureType=4/VertSubst, .FeatureSetting=off
'0000 0000 ' # 36: Feature[1].EnableFlags=0x00000000
'FFFF FFFE ' # 40: Feature[1].DisableFlags=0xFFFFFFFE
'0000 0001 ' # 44: Feature[2].FeatureType=0/GlyphEffects, .FeatSetting=off
'0000 0000 ' # 48: Feature[2].EnableFlags=0 (required for last feature)
'0000 0000 ' # 52: Feature[2].EnableFlags=0 (required for last feature)
'0020 ' # 56: Subtable[0].StructLength=32
'80 ' # 58: Subtable[0].CoverageFlags=0x80
'04 ' # 59: Subtable[0].MorphType=4/NoncontextualMorph
'0000 0001 ' # 60: Subtable[0].SubFeatureFlags=0x1
'0006 0004 ' # 64: LookupFormat=6, UnitSize=4
'0002 0008 ' # 68: NUnits=2, SearchRange=8
'0001 0000 ' # 72: EntrySelector=1, RangeShift=0
'000B 0087 ' # 76: Glyph=11 (parenleft); Value=135 (parenleft.vertical)
'000D 0088 ' # 80: Glyph=13 (parenright); Value=136 (parenright.vertical)
'FFFF 0000 ' # 84: Glyph=<end>; Value=0
) # 88: <end>
assert len(MORT_NONCONTEXTUAL_DATA) == 88
MORT_NONCONTEXTUAL_XML = [
'<Version value="0x00010000"/>',
'<!-- MorphChainCount=1 -->',
'<MorphChain index="0">',
' <DefaultFlags value="0x00000001"/>',
' <!-- StructLength=80 -->',
' <!-- MorphFeatureCount=3 -->',
' <!-- MorphSubtableCount=1 -->',
' <MorphFeature index="0">',
' <FeatureType value="4"/>',
' <FeatureSetting value="0"/>',
' <EnableFlags value="0x00000001"/>',
' <DisableFlags value="0xFFFFFFFF"/>',
' </MorphFeature>',
' <MorphFeature index="1">',
' <FeatureType value="4"/>',
' <FeatureSetting value="1"/>',
' <EnableFlags value="0x00000000"/>',
' <DisableFlags value="0xFFFFFFFE"/>',
' </MorphFeature>',
' <MorphFeature index="2">',
' <FeatureType value="0"/>',
' <FeatureSetting value="1"/>',
' <EnableFlags value="0x00000000"/>',
' <DisableFlags value="0x00000000"/>',
' </MorphFeature>',
' <MorphSubtable index="0">',
' <!-- StructLength=32 -->',
' <CoverageFlags value="128"/>',
' <!-- MorphType=4 -->',
' <SubFeatureFlags value="0x00000001"/>',
' <NoncontextualMorph>',
' <Substitution>',
' <Lookup glyph="parenleft" value="parenleft.vertical"/>',
' <Lookup glyph="parenright" value="parenright.vertical"/>',
' </Substitution>',
' </NoncontextualMorph>',
' </MorphSubtable>',
'</MorphChain>',
]
class MORTNoncontextualGlyphSubstitutionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.maxDiff = None
glyphs = ['.notdef'] + ['g.%d' % i for i in range (1, 140)]
glyphs[11], glyphs[13] = 'parenleft', 'parenright'
glyphs[135], glyphs[136] = 'parenleft.vertical', 'parenright.vertical'
cls.font = FakeFont(glyphs)
def test_decompile_toXML(self):
table = newTable('mort')
table.decompile(MORT_NONCONTEXTUAL_DATA, self.font)
self.assertEqual(getXML(table.toXML), MORT_NONCONTEXTUAL_XML)
def test_compile_fromXML(self):
table = newTable('mort')
for name, attrs, content in parseXML(MORT_NONCONTEXTUAL_XML):
table.fromXML(name, attrs, content, font=self.font)
self.assertEqual(hexStr(table.compile(self.font)),
hexStr(MORT_NONCONTEXTUAL_DATA))
if __name__ == '__main__':
import sys
sys.exit(unittest.main())

View File

@ -0,0 +1,114 @@
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.testTools import FakeFont, getXML, parseXML
from fontTools.misc.textTools import deHexStr, hexStr
from fontTools.ttLib import newTable
import unittest
# A simple 'morx' table with non-contextual glyph substitution.
# Unfortunately, the Apple spec for 'morx' does not contain a complete example.
# The test case has therefore been adapted from the example 'mort' table in
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html
MORX_NONCONTEXTUAL_DATA = deHexStr(
'0002 0000 ' # 0: Version=2, Reserved=0
'0000 0001 ' # 4: MorphChainCount=1
'0000 0001 ' # 8: DefaultFlags=1
'0000 0058 ' # 12: StructLength=88
'0000 0003 ' # 16: MorphFeatureCount=3
'0000 0001 ' # 20: MorphSubtableCount=1
'0004 0000 ' # 24: Feature[0].FeatureType=4/VertSubst, .FeatureSetting=on
'0000 0001 ' # 28: Feature[0].EnableFlags=0x00000001
'FFFF FFFF ' # 32: Feature[0].DisableFlags=0xFFFFFFFF
'0004 0001 ' # 36: Feature[1].FeatureType=4/VertSubst, .FeatureSetting=off
'0000 0000 ' # 40: Feature[1].EnableFlags=0x00000000
'FFFF FFFE ' # 44: Feature[1].DisableFlags=0xFFFFFFFE
'0000 0001 ' # 48: Feature[2].FeatureType=0/GlyphEffects, .FeatSetting=off
'0000 0000 ' # 52: Feature[2].EnableFlags=0 (required for last feature)
'0000 0000 ' # 56: Feature[2].EnableFlags=0 (required for last feature)
'0000 0024 ' # 60: Subtable[0].StructLength=36
'80 ' # 64: Subtable[0].CoverageFlags=0x80
'00 00 ' # 65: Subtable[0].Reserved=0
'04 ' # 67: Subtable[0].MorphType=4/NoncontextualMorph
'0000 0001 ' # 68: Subtable[0].SubFeatureFlags=0x1
'0006 0004 ' # 72: LookupFormat=6, UnitSize=4
'0002 0008 ' # 76: NUnits=2, SearchRange=8
'0001 0000 ' # 80: EntrySelector=1, RangeShift=0
'000B 0087 ' # 84: Glyph=11 (parenleft); Value=135 (parenleft.vertical)
'000D 0088 ' # 88: Glyph=13 (parenright); Value=136 (parenright.vertical)
'FFFF 0000 ' # 92: Glyph=<end>; Value=0
) # 96: <end>
assert len(MORX_NONCONTEXTUAL_DATA) == 96
MORX_NONCONTEXTUAL_XML = [
'<Version value="2"/>',
'<Reserved value="0"/>',
'<!-- MorphChainCount=1 -->',
'<MorphChain index="0">',
' <DefaultFlags value="0x00000001"/>',
' <!-- StructLength=88 -->',
' <!-- MorphFeatureCount=3 -->',
' <!-- MorphSubtableCount=1 -->',
' <MorphFeature index="0">',
' <FeatureType value="4"/>',
' <FeatureSetting value="0"/>',
' <EnableFlags value="0x00000001"/>',
' <DisableFlags value="0xFFFFFFFF"/>',
' </MorphFeature>',
' <MorphFeature index="1">',
' <FeatureType value="4"/>',
' <FeatureSetting value="1"/>',
' <EnableFlags value="0x00000000"/>',
' <DisableFlags value="0xFFFFFFFE"/>',
' </MorphFeature>',
' <MorphFeature index="2">',
' <FeatureType value="0"/>',
' <FeatureSetting value="1"/>',
' <EnableFlags value="0x00000000"/>',
' <DisableFlags value="0x00000000"/>',
' </MorphFeature>',
' <MorphSubtable index="0">',
' <!-- StructLength=36 -->',
' <CoverageFlags value="128"/>',
' <Reserved value="0"/>',
' <!-- MorphType=4 -->',
' <SubFeatureFlags value="0x00000001"/>',
' <NoncontextualMorph>',
' <Substitution>',
' <Lookup glyph="parenleft" value="parenleft.vertical"/>',
' <Lookup glyph="parenright" value="parenright.vertical"/>',
' </Substitution>',
' </NoncontextualMorph>',
' </MorphSubtable>',
'</MorphChain>',
]
class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.maxDiff = None
glyphs = ['.notdef'] + ['g.%d' % i for i in range (1, 140)]
glyphs[11], glyphs[13] = 'parenleft', 'parenright'
glyphs[135], glyphs[136] = 'parenleft.vertical', 'parenright.vertical'
cls.font = FakeFont(glyphs)
def test_decompile_toXML(self):
table = newTable('morx')
table.decompile(MORX_NONCONTEXTUAL_DATA, self.font)
self.assertEqual(getXML(table.toXML), MORX_NONCONTEXTUAL_XML)
def test_compile_fromXML(self):
table = newTable('morx')
for name, attrs, content in parseXML(MORX_NONCONTEXTUAL_XML):
table.fromXML(name, attrs, content, font=self.font)
self.assertEqual(hexStr(table.compile(self.font)),
hexStr(MORX_NONCONTEXTUAL_DATA))
if __name__ == '__main__':
import sys
sys.exit(unittest.main())