From d37380834101d661b9ed1974e391ee6cc41d6075 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Wed, 20 Sep 2017 18:14:22 +0200 Subject: [PATCH] [AAT] Decode the `morx` ligature actions table --- Lib/fontTools/ttLib/tables/otConverters.py | 13 +++-- Lib/fontTools/ttLib/tables/otTables.py | 63 ++++++++++++++++++---- Tests/ttLib/tables/_m_o_r_x_test.py | 14 ++--- Tests/ttLib/tables/otTables_test.py | 27 +++++++++- 4 files changed, 94 insertions(+), 23 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 0f3bc9985..44964182c 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -8,7 +8,7 @@ from fontTools.ttLib import getSearchRange from .otBase import (CountReference, FormatSwitchingBaseTable, OTTableWriter, ValueRecordFactory) from .otTables import (AATStateTable, AATState, AATAction, - ContextualMorphAction) + ContextualMorphAction, LigatureMorphAction) from functools import partial import struct import logging @@ -901,6 +901,7 @@ class STXHeader(BaseConverter): classTableReader = reader.getSubReader(0) stateArrayReader = reader.getSubReader(0) entryTableReader = reader.getSubReader(0) + ligActionReader = None table.GlyphClassCount = reader.readULong() classTableReader.seek(pos + reader.readULong()) stateArrayReader.seek(pos + reader.readULong()) @@ -908,6 +909,9 @@ class STXHeader(BaseConverter): if self.perGlyphLookup is not None: perGlyphTableReader = reader.getSubReader(0) perGlyphTableReader.seek(pos + reader.readULong()) + if issubclass(self.tableClass, LigatureMorphAction): + ligActionReader = reader.getSubReader(0) + ligActionReader.seek(pos + reader.readULong()) table.GlyphClasses = self.classLookup.read(classTableReader, font, tableDict) numStates = int((entryTableReader.pos - stateArrayReader.pos) @@ -919,17 +923,18 @@ class STXHeader(BaseConverter): entryIndex = stateArrayReader.readUShort() state.Transitions[glyphClass] = \ self._readTransition(entryTableReader, - entryIndex, font) + entryIndex, font, + ligActionReader) if self.perGlyphLookup is not None: table.PerGlyphLookups = self._readPerGlyphLookups( table, perGlyphTableReader, font) return table - def _readTransition(self, reader, entryIndex, font): + def _readTransition(self, reader, entryIndex, font, ligActionReader): transition = self.tableClass() entryReader = reader.getSubReader( reader.pos + entryIndex * transition.staticSize) - transition.decompile(entryReader, font) + transition.decompile(entryReader, font, ligActionReader) return transition def _countPerGlyphLookups(self, table): diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 981edc4e1..dd6b2e34e 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -87,7 +87,8 @@ class RearrangementMorphAction(AATAction): if self.MarkLast: flags |= 0x2000 writer.writeUShort(flags) - def decompile(self, reader, font): + def decompile(self, reader, font, ligActionReader): + assert ligActionReader is None self.NewState = reader.readUShort() flags = reader.readUShort() self.Verb = flags & 0xF @@ -145,7 +146,8 @@ class ContextualMorphAction(AATAction): writer.writeUShort(self.MarkIndex) writer.writeUShort(self.CurrentIndex) - def decompile(self, reader, font): + def decompile(self, reader, font, ligActionReader): + assert ligActionReader is None self.NewState = reader.readUShort() flags = reader.readUShort() self.SetMark = bool(flags & 0x8000) @@ -187,30 +189,67 @@ class ContextualMorphAction(AATAction): self.CurrentIndex = safeEval(eltAttrs["value"]) +class LigAction(object): + def __init__(self): + self.Store = False + # GlyphIndexDelta is a (possibly negative) delta that gets + # added to the glyph ID at the top of the AAT runtime + # execution stack. It is *not* a byte offset into the + # morx table. The result of the addition, which is performed + # at run time by the shaping engine, is an index into + # the ligature components table. See 'morx' specification. + # In the AAT specification, this field is called Offset; + # but its meaning is quite different from other offsets + # in either AAT or OpenType, so we use a different name. + self.GlyphIndexDelta = 0 + + class LigatureMorphAction(AATAction): staticSize = 6 - _FLAGS = ["SetComponent", "DontAdvance", "PerformAction"] + _FLAGS = ["SetComponent", "DontAdvance"] def __init__(self): self.NewState = 0 self.SetComponent, self.DontAdvance = False, False - self.PerformAction = False self.ReservedFlags = 0 - self.LigActionIndex = 0 + self.Actions = [] - def decompile(self, reader, font): + def decompile(self, reader, font, ligActionReader): + assert ligActionReader is not None self.NewState = reader.readUShort() flags = reader.readUShort() self.SetComponent = bool(flags & 0x8000) self.DontAdvance = bool(flags & 0x4000) - self.PerformAction = bool(flags & 0x2000) + performAction = bool(flags & 0x2000) # As of 2017-09-12, the 'morx' specification says that # the reserved bitmask in ligature subtables is 0x3FFF. # However, the specification also defines a flag 0x2000, # so the reserved value should actually be 0x1FFF. # TODO: Report this specification bug to Apple. self.ReservedFlags = flags & 0x1FFF - self.LigActionIndex = reader.readUShort() + ligActionIndex = reader.readUShort() + if performAction: + self.Actions = self._decompileLigActions( + ligActionReader, ligActionIndex) + else: + self.Actions = [] + + def _decompileLigActions(self, ligActionReader, ligActionIndex): + actions = [] + last = False + reader = ligActionReader.getSubReader( + ligActionReader.pos + ligActionIndex * 4) + while not last: + value = reader.readULong() + last = bool(value & 0x80000000) + action = LigAction() + actions.append(action) + action.Store = bool(value & 0x40000000) + delta = value & 0x3FFFFFFF + if delta >= 0x20000000: # sign-extend 30-bit value + delta = -0x40000000 + delta + action.GlyphIndexDelta = delta + return actions def toXML(self, xmlWriter, font, attrs, name): xmlWriter.begintag(name, **attrs) @@ -218,9 +257,11 @@ class LigatureMorphAction(AATAction): xmlWriter.simpletag("NewState", value=self.NewState) xmlWriter.newline() self._writeFlagsToXML(xmlWriter) - if self.PerformAction: - xmlWriter.simpletag("LigActionIndex", - value=self.LigActionIndex) + for action in self.Actions: + attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)] + if action.Store: + attribs.append(("Flags", "Store")) + xmlWriter.simpletag("Action", attribs) xmlWriter.newline() xmlWriter.endtag(name) xmlWriter.newline() diff --git a/Tests/ttLib/tables/_m_o_r_x_test.py b/Tests/ttLib/tables/_m_o_r_x_test.py index eb8b5027b..77962932c 100644 --- a/Tests/ttLib/tables/_m_o_r_x_test.py +++ b/Tests/ttLib/tables/_m_o_r_x_test.py @@ -528,12 +528,12 @@ MORX_LIGATURE_DATA = deHexStr( '0003 8000 ' # 168: Entries[2].NewState=3, .Flags=0x8000 (SetComponent) '0000 ' # 172: Entries[2].ActionIndex= because no 0x2000 flag '0000 A000 ' # 174: Entries[3].NewState=0, .Flags=0xA000 (SetComponent,Act) - '0000 ' # 178: Entries[3].ActionIndex=0 (start at LigAction[0]) + '0000 ' # 178: Entries[3].ActionIndex=0 (start at Action[0]) # Ligature actions table. - '3FFF FFE7 ' # 180: Action 0, part 1 - '3FFF FFED ' # 184: Action 0, part 2 - 'BFFF FFF2 ' # 188: Action 0, part 3 + '3FFF FFE7 ' # 180: Action[0].Flags=0, .GlyphIndexDelta=-25 + '3FFF FFED ' # 184: Action[1].Flags=0, .GlyphIndexDelta=-19 + 'BFFF FFF2 ' # 188: Action[2].Flags=, .GlyphIndexDelta=-14 # Ligature component table. '0000 0001 ' # 192: LigComponent[0]=0, LigComponent[1]=1 @@ -674,8 +674,10 @@ MORX_LIGATURE_XML = [ ' ', ' ', ' ', - ' ', - ' ', + ' ', + ' ', + ' ', + ' ', ' ', ' ', ' ', diff --git a/Tests/ttLib/tables/otTables_test.py b/Tests/ttLib/tables/otTables_test.py index 4ea0d57eb..10c4e5868 100644 --- a/Tests/ttLib/tables/otTables_test.py +++ b/Tests/ttLib/tables/otTables_test.py @@ -385,7 +385,8 @@ class RearrangementMorphActionTest(unittest.TestCase): def testDecompileToXML(self): r = otTables.RearrangementMorphAction() - r.decompile(OTTableReader(deHexStr("1234fffd")), self.font) + r.decompile(OTTableReader(deHexStr("1234fffd")), + self.font, ligActionReader=None) toXML = lambda w, f: r.toXML(w, f, {"Test": "Foo"}, "Transition") self.assertEqual(getXML(toXML, self.font), [ '', @@ -412,7 +413,8 @@ class ContextualMorphActionTest(unittest.TestCase): def testDecompileToXML(self): a = otTables.ContextualMorphAction() - a.decompile(OTTableReader(deHexStr("1234f117deadbeef")), self.font) + a.decompile(OTTableReader(deHexStr("1234f117deadbeef")), + self.font, ligActionReader=None) toXML = lambda w, f: a.toXML(w, f, {"Test": "Foo"}, "Transition") self.assertEqual(getXML(toXML, self.font), [ '', @@ -425,6 +427,27 @@ class ContextualMorphActionTest(unittest.TestCase): ]) +class LigatureMorphActionTest(unittest.TestCase): + def setUp(self): + self.font = FakeFont(['.notdef', 'A', 'B', 'C']) + + def testDecompileToXML(self): + a = otTables.LigatureMorphAction() + ligActionReader = OTTableReader(deHexStr("DEADBEEF 7FFFFFFE 80000003")) + a.decompile(OTTableReader(deHexStr("1234FAB30001")), + self.font, ligActionReader) + toXML = lambda w, f: a.toXML(w, f, {"Test": "Foo"}, "Transition") + self.assertEqual(getXML(toXML, self.font), [ + '', + ' ', # 0x1234 = 4660 + ' ', + ' ', + ' ', + ' ', + '', + ]) + + if __name__ == "__main__": import sys sys.exit(unittest.main())