From 90f257cc605608378f7712fcf2a919a1250dbe5b Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Tue, 29 Aug 2017 12:52:30 +0200 Subject: [PATCH] [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. --- Lib/fontTools/ttLib/tables/__init__.py | 2 + Lib/fontTools/ttLib/tables/_m_o_r_t.py | 8 ++ Lib/fontTools/ttLib/tables/_m_o_r_x.py | 8 ++ Lib/fontTools/ttLib/tables/otConverters.py | 12 ++- Lib/fontTools/ttLib/tables/otData.py | 43 ++++++-- Lib/fontTools/ttLib/tables/otTables.py | 11 +- README.rst | 4 +- Tests/ttLib/tables/_m_o_r_t_test.py | 115 +++++++++++++++++++++ Tests/ttLib/tables/_m_o_r_x_test.py | 114 ++++++++++++++++++++ 9 files changed, 299 insertions(+), 18 deletions(-) create mode 100644 Lib/fontTools/ttLib/tables/_m_o_r_t.py create mode 100644 Lib/fontTools/ttLib/tables/_m_o_r_x.py create mode 100644 Tests/ttLib/tables/_m_o_r_t_test.py create mode 100644 Tests/ttLib/tables/_m_o_r_x_test.py diff --git a/Lib/fontTools/ttLib/tables/__init__.py b/Lib/fontTools/ttLib/tables/__init__.py index 95a272e73..134f26327 100644 --- a/Lib/fontTools/ttLib/tables/__init__.py +++ b/Lib/fontTools/ttLib/tables/__init__.py @@ -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 diff --git a/Lib/fontTools/ttLib/tables/_m_o_r_t.py b/Lib/fontTools/ttLib/tables/_m_o_r_t.py new file mode 100644 index 000000000..b87b42541 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_m_o_r_t.py @@ -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 diff --git a/Lib/fontTools/ttLib/tables/_m_o_r_x.py b/Lib/fontTools/ttLib/tables/_m_o_r_x.py new file mode 100644 index 000000000..1619d8d6a --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_m_o_r_x.py @@ -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 diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 1ea102e0e..f7bef2df1 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -45,7 +45,11 @@ def buildConverters(tableSpec, tableNamespace): converterClass = Struct else: converterClass = eval(tp, tableNamespace, converterMapping) - tableClass = tableNamespace.get(tableName) + 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) else: @@ -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), diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 60d441314..0296b9132 100755 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -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', [ diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 2c6ca24b7..d7f27905c 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -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 diff --git a/README.rst b/README.rst index a808993b4..1bb6a5967 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/Tests/ttLib/tables/_m_o_r_t_test.py b/Tests/ttLib/tables/_m_o_r_t_test.py new file mode 100644 index 000000000..70696e829 --- /dev/null +++ b/Tests/ttLib/tables/_m_o_r_t_test.py @@ -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=; Value=0 +) # 88: +assert len(MORT_NONCONTEXTUAL_DATA) == 88 + + +MORT_NONCONTEXTUAL_XML = [ + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +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()) diff --git a/Tests/ttLib/tables/_m_o_r_x_test.py b/Tests/ttLib/tables/_m_o_r_x_test.py new file mode 100644 index 000000000..57f474ffe --- /dev/null +++ b/Tests/ttLib/tables/_m_o_r_x_test.py @@ -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=; Value=0 +) # 96: +assert len(MORX_NONCONTEXTUAL_DATA) == 96 + + +MORX_NONCONTEXTUAL_XML = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +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()) +