[AAT] Decode the morx ligature actions table

This commit is contained in:
Sascha Brawer 2017-09-20 18:14:22 +02:00
parent a7ddb0b609
commit d373808341
4 changed files with 94 additions and 23 deletions

View File

@ -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):

View File

@ -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()

View File

@ -528,12 +528,12 @@ MORX_LIGATURE_DATA = deHexStr(
'0003 8000 ' # 168: Entries[2].NewState=3, .Flags=0x8000 (SetComponent)
'0000 ' # 172: Entries[2].ActionIndex=<n/a> 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=<end of list>, .GlyphIndexDelta=-14
# Ligature component table.
'0000 0001 ' # 192: LigComponent[0]=0, LigComponent[1]=1
@ -674,8 +674,10 @@ MORX_LIGATURE_XML = [
' </Transition>',
' <Transition onGlyphClass="6">',
' <NewState value="0"/>',
' <Flags value="SetComponent,PerformAction"/>',
' <LigActionIndex value="0"/>',
' <Flags value="SetComponent"/>',
' <Action GlyphIndexDelta="-25"/>',
' <Action GlyphIndexDelta="-19"/>',
' <Action GlyphIndexDelta="-14"/>',
' </Transition>',
' </State>',
' </StateTable>',

View File

@ -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), [
'<Transition Test="Foo">',
@ -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), [
'<Transition Test="Foo">',
@ -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), [
'<Transition Test="Foo">',
' <NewState value="4660"/>', # 0x1234 = 4660
' <Flags value="SetComponent,DontAdvance"/>',
' <ReservedFlags value="0x1AB3"/>',
' <Action GlyphIndexDelta="-2" Flags="Store"/>',
' <Action GlyphIndexDelta="3"/>',
'</Transition>',
])
if __name__ == "__main__":
import sys
sys.exit(unittest.main())