[AAT] Support morx tables with contextual substitution

This commit is contained in:
Sascha Brawer 2017-09-05 18:32:05 +02:00
parent 31b02d0bed
commit ee1662e57e
5 changed files with 456 additions and 29 deletions

View File

@ -6,7 +6,8 @@ from fontTools.misc.fixedTools import (
from fontTools.misc.textTools import pad, safeEval
from fontTools.ttLib import getSearchRange
from .otBase import ValueRecordFactory, CountReference, OTTableWriter
from .otTables import AATStateTable, AATState
from .otTables import (AATStateTable, AATState, AATAction,
ContextualMorphAction)
from functools import partial
import struct
import logging
@ -880,8 +881,13 @@ class AATLookupWithDataOffset(BaseConverter):
class STXHeader(BaseConverter):
def __init__(self, name, repeat, aux, tableClass):
BaseConverter.__init__(self, name, repeat, aux, tableClass)
assert tableClass is not None
assert issubclass(self.tableClass, AATAction)
self.classLookup = AATLookup("GlyphClasses", None, None, UShort)
if issubclass(self.tableClass, ContextualMorphAction):
self.perGlyphLookup = AATLookup("PerGlyphLookup",
None, None, GlyphID)
else:
self.perGlyphLookup = None
def read(self, reader, font, tableDict):
table = AATStateTable()
@ -893,6 +899,9 @@ class STXHeader(BaseConverter):
classTableReader.seek(pos + reader.readULong())
stateArrayReader.seek(pos + reader.readULong())
entryTableReader.seek(pos + reader.readULong())
if self.perGlyphLookup is not None:
perGlyphTableReader = reader.getSubReader(0)
perGlyphTableReader.seek(pos + reader.readULong())
table.GlyphClasses = self.classLookup.read(classTableReader,
font, tableDict)
numStates = int((entryTableReader.pos - stateArrayReader.pos)
@ -905,6 +914,9 @@ class STXHeader(BaseConverter):
state.Transitions[glyphClass] = \
self._readTransition(entryTableReader,
entryIndex, font)
if self.perGlyphLookup is not None:
table.PerGlyphLookups = self._readPerGlyphLookups(
table, perGlyphTableReader, font)
return table
def _readTransition(self, reader, entryIndex, font):
@ -914,6 +926,35 @@ class STXHeader(BaseConverter):
transition.decompile(entryReader, font)
return transition
def _countPerGlyphLookups(self, table):
# Somewhat annoyingly, the morx table does not encode
# the size of the per-glyph table. So we need to find
# the maximum value that MorphActions use as index
# into this table.
numLookups = 0
for state in table.States:
for t in state.Transitions.values():
if isinstance(t, ContextualMorphAction):
if t.MarkIndex != 0xFFFF:
numLookups = max(
numLookups,
t.MarkIndex + 1)
if t.CurrentIndex != 0xFFFF:
numLookups = max(
numLookups,
t.CurrentIndex + 1)
return numLookups
def _readPerGlyphLookups(self, table, reader, font):
pos = reader.pos
lookups = []
for _ in range(self._countPerGlyphLookups(table)):
lookupReader = reader.getSubReader(0)
lookupReader.seek(pos + reader.readULong())
lookups.append(
self.perGlyphLookup.read(lookupReader, font, {}))
return lookups
def write(self, writer, font, tableDict, value, repeatIndex=None):
glyphClassWriter = OTTableWriter()
self.classLookup.write(glyphClassWriter, font, tableDict,
@ -921,6 +962,8 @@ class STXHeader(BaseConverter):
glyphClassData = pad(glyphClassWriter.getAllData(), 4)
glyphClassCount = max(value.GlyphClasses.values()) + 1
glyphClassTableOffset = 16 # size of STXHeader
if self.perGlyphLookup is not None:
glyphClassTableOffset += 4
stateArrayWriter = OTTableWriter()
entries, entryIDs = [], {}
for state in value.States:
@ -942,16 +985,39 @@ class STXHeader(BaseConverter):
entries.append(entryData)
stateArrayWriter.writeUShort(entryIndex)
stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
stateArrayData = stateArrayWriter.getAllData()
stateArrayData = pad(stateArrayWriter.getAllData(), 4)
entryTableOffset = stateArrayOffset + len(stateArrayData)
entryTableData = pad(bytesjoin(entries), 4)
perGlyphOffset = entryTableOffset + len(entryTableData)
perGlyphData = \
pad(self._compilePerGlyphLookups(value, font), 4)
writer.writeULong(glyphClassCount)
writer.writeULong(glyphClassTableOffset)
writer.writeULong(stateArrayOffset)
writer.writeULong(entryTableOffset)
if self.perGlyphLookup is not None:
writer.writeULong(perGlyphOffset)
writer.writeData(glyphClassData)
writer.writeData(stateArrayData)
for entry in entries:
writer.writeData(entry)
writer.writeData(entryTableData)
writer.writeData(perGlyphData)
def _compilePerGlyphLookups(self, table, font):
if self.perGlyphLookup is None:
return b""
numLookups = self._countPerGlyphLookups(table)
assert len(table.PerGlyphLookups) == numLookups, (
"len(AATStateTable.PerGlyphLookups) is %d, "
"but the actions inside the table refer to %d" %
(len(table.PerGlyphLookups), numLookups))
writer = OTTableWriter()
for lookup in table.PerGlyphLookups:
lookupWriter = writer.getSubWriter()
lookupWriter.longOffset = True
self.perGlyphLookup.write(lookupWriter, font,
{}, lookup, None)
writer.writeSubTable(lookupWriter)
return writer.getAllData()
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.begintag(name, attrs)
@ -970,6 +1036,15 @@ class STXHeader(BaseConverter):
name="Transition")
xmlWriter.endtag("State")
xmlWriter.newline()
for i, lookup in enumerate(value.PerGlyphLookups):
xmlWriter.begintag("PerGlyphLookup", index=i)
xmlWriter.newline()
for glyph, val in sorted(lookup.items()):
xmlWriter.simpletag("Lookup", glyph=glyph,
value=val)
xmlWriter.newline()
xmlWriter.endtag("PerGlyphLookup")
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
@ -983,6 +1058,10 @@ class STXHeader(BaseConverter):
elif eltName == "State":
state = self._xmlReadState(eltAttrs, eltContent, font)
table.States.append(state)
elif eltName == "PerGlyphLookup":
lookup = self.perGlyphLookup.xmlRead(
eltAttrs, eltContent, font)
table.PerGlyphLookups.append(lookup)
table.GlyphClassCount = max(table.GlyphClasses.values()) + 1
return table

View File

@ -1387,12 +1387,11 @@ otData = [
]),
('RearrangementMorph', [
('STXHeader(AATRearrangement)', 'StateTable', None, None, 'Finite-state transducer table.'),
('STXHeader(RearrangementMorphAction)', 'StateTable', None, None, 'Finite-state transducer table for indic rearrangement.'),
]),
('ContextualMorph', [
('struct', 'StateHeader', None, None, 'Header.'),
# TODO: Add missing parts.
('STXHeader(ContextualMorphAction)', 'StateTable', None, None, 'Finite-state transducer for contextual glyph substitution.'),
]),
('LigatureMorph', [

View File

@ -18,16 +18,38 @@ log = logging.getLogger(__name__)
class AATStateTable(object):
def __init__(self):
self.GlyphClasses = {} # GlyphName --> GlyphClass
self.GlyphClasses = {} # GlyphID --> GlyphClass
self.States = [] # List of AATState, indexed by state number
self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...]
class AATState(object):
def __init__(self):
self.Transitions = {} # GlyphClass --> {AATRearrangement, ...}
self.Transitions = {} # GlyphClass --> AATAction
class AATRearrangement(object):
class AATAction(object):
_FLAGS = None
def _writeFlagsToXML(self, xmlWriter):
flags = [f for f in self._FLAGS if self.__dict__[f]]
if flags:
xmlWriter.simpletag("Flags", value=",".join(flags))
xmlWriter.newline()
if self.ReservedFlags != 0:
xmlWriter.simpletag(
"ReservedFlags",
value='0x%04X' % self.ReservedFlags)
xmlWriter.newline()
def _setFlag(self, flag):
assert flag in self._FLAGS, "unsupported flag %s" % flag
self.__dict__[flag] = True
class RearrangementMorphAction(AATAction):
staticSize = 4
_FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
_VERBS = {
0: "no change",
@ -79,15 +101,7 @@ class AATRearrangement(object):
xmlWriter.newline()
xmlWriter.simpletag("NewState", value=self.NewState)
xmlWriter.newline()
flags = [f for f in ("MarkFirst", "DontAdvance", "MarkLast")
if self.__dict__[f]]
if flags:
xmlWriter.simpletag("Flags", value=",".join(flags))
xmlWriter.newline()
if self.ReservedFlags != 0:
xmlWriter.simpletag("ReservedFlags",
value='0x%04X' % self.ReservedFlags)
xmlWriter.newline()
self._writeFlagsToXML(xmlWriter)
xmlWriter.simpletag("Verb", value=self.Verb)
verbComment = self._VERBS.get(self.Verb)
if verbComment is not None:
@ -111,11 +125,66 @@ class AATRearrangement(object):
for flag in eltAttrs["value"].split(","):
self._setFlag(flag.strip())
def _setFlag(self, flag):
assert flag in {"MarkFirst", "DontAdvance", "MarkLast"}, \
"unsupported flag %s" % flag
self.__dict__[flag] = True
class ContextualMorphAction(AATAction):
staticSize = 8
_FLAGS = ["SetMark", "DontAdvance"]
def __init__(self):
self.NewState = 0
self.SetMark, self.DontAdvance = False, False
self.ReservedFlags = 0
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
def compile(self, writer, font):
writer.writeUShort(self.NewState)
flags = self.ReservedFlags
if self.SetMark: flags |= 0x8000
if self.DontAdvance: flags |= 0x4000
writer.writeUShort(flags)
writer.writeUShort(self.MarkIndex)
writer.writeUShort(self.CurrentIndex)
def decompile(self, reader, font):
self.NewState = reader.readUShort()
flags = reader.readUShort()
self.SetMark = bool(flags & 0x8000)
self.DontAdvance = bool(flags & 0x4000)
self.ReservedFlags = flags & 0x3FFF
self.MarkIndex = reader.readUShort()
self.CurrentIndex = reader.readUShort()
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
xmlWriter.newline()
xmlWriter.simpletag("NewState", value=self.NewState)
xmlWriter.newline()
self._writeFlagsToXML(xmlWriter)
xmlWriter.simpletag("MarkIndex", value=self.MarkIndex)
xmlWriter.newline()
xmlWriter.simpletag("CurrentIndex",
value=self.CurrentIndex)
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
self.NewState = self.ReservedFlags = 0
self.SetMark = self.DontAdvance = False
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
content = [t for t in content if isinstance(t, tuple)]
for eltName, eltAttrs, eltContent in content:
if eltName == "NewState":
self.NewState = safeEval(eltAttrs["value"])
elif eltName == "Flags":
for flag in eltAttrs["value"].split(","):
self._setFlag(flag.strip())
elif eltName == "ReservedFlags":
self.ReservedFlags = safeEval(eltAttrs["value"])
elif eltName == "MarkIndex":
self.MarkIndex = safeEval(eltAttrs["value"])
elif eltName == "CurrentIndex":
self.CurrentIndex = safeEval(eltAttrs["value"])
class FeatureParams(BaseTable):
@ -1097,7 +1166,7 @@ def _buildClasses():
},
'morx': {
0: RearrangementMorph,
# 1: ContextualMorph,
1: ContextualMorph,
# 2: LigatureMorph,
# 3: Reserved,
4: NoncontextualMorph,

View File

@ -90,7 +90,7 @@ MORX_REARRANGEMENT_DATA = deHexStr(
'0002 0000 ' # 0: Version=2, Reserved=0
'0000 0001 ' # 4: MorphChainCount=1
'0000 0001 ' # 8: DefaultFlags=1
'0000 0078 ' # 12: StructLength=120
'0000 0078 ' # 12: StructLength=120 (+8=128)
'0000 0000 ' # 16: MorphFeatureCount=0
'0000 0001 ' # 20: MorphSubtableCount=1
'0000 0068 ' # 24: Subtable[0].StructLength=104 (+24=128)
@ -233,6 +233,233 @@ MORX_REARRANGEMENT_XML = [
]
# Taken from “Example 1: A contextal substituation table” in
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
# as retrieved on 2017-09-05.
#
# Compared to the example table in Apples specification, weve
# made the following changes:
#
# * at offsets 0..35, weve prepended 36 bytes of boilerplate
# to make the data a structurally valid morx table;
#
# * at offset 36 (offset 0 in Apples document), weve changed
# the number of glyph classes from 5 to 6 because the encoded
# finite-state machine has transitions for six different glyph
# classes (0..5);
#
# * at offset 52 (offset 16 in Apples document), weve replaced
# the presumably leftover XXX mark by an actual data offset;
#
# * at offset 72 (offset 36 in Apples document), weve changed
# the input GlyphID from 51 to 52. With the original value of 51,
# the glyph class lookup table can be encoded with equally many
# bytes in either format 2 or 6; after changing the GlyphID to 52,
# the most compact encoding is lookup format 6, as used in Apples
# example;
#
# * at offset 90 (offset 54 in Apples document), weve changed
# the value for the lookup end-of-table marker from 1 to 0.
# Fonttools always uses zero for this value, whereas Apples
# spec examples are inconsistently using one of {0, 1, 0xFFFF}
# for this filler value;
#
# * at offset 172 (offset 136 in Apples document), weve again changed
# the input GlyphID from 51 to 52, for the same reason as above.
#
# TODO: Ask Apple to fix “Example 1” in the morx specification.
MORX_CONTEXTUAL_DATA = deHexStr(
'0002 0000 ' # 0: Version=2, Reserved=0
'0000 0001 ' # 4: MorphChainCount=1
'0000 0001 ' # 8: DefaultFlags=1
'0000 00B4 ' # 12: StructLength=180 (+8=188)
'0000 0000 ' # 16: MorphFeatureCount=0
'0000 0001 ' # 20: MorphSubtableCount=1
'0000 00A4 ' # 24: Subtable[0].StructLength=164 (+24=188)
'80 ' # 28: Subtable[0].CoverageFlags=0x80
'00 00 ' # 29: Subtable[0].Reserved=0
'01 ' # 31: Subtable[0].MorphType=1/ContextualMorph
'0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1
'0000 0006 ' # 36: STXHeader.ClassCount=6
'0000 0014 ' # 40: STXHeader.ClassTableOffset=20 (+36=56)
'0000 0038 ' # 44: STXHeader.StateArrayOffset=56 (+36=92)
'0000 005C ' # 48: STXHeader.EntryTableOffset=92 (+36=128)
'0000 0074 ' # 52: STXHeader.PerGlyphTableOffset=116 (+36=152)
# Glyph class table.
'0006 0004 ' # 56: ClassTable.LookupFormat=6, .UnitSize=4
'0005 0010 ' # 60: .NUnits=5, .SearchRange=16
'0002 0004 ' # 64: .EntrySelector=2, .RangeShift=4
'0032 0004 ' # 68: Glyph=50; Class=4
'0034 0004 ' # 72: Glyph=52; Class=4
'0050 0005 ' # 76: Glyph=80; Class=5
'00C9 0004 ' # 80: Glyph=201; Class=4
'00CA 0004 ' # 84: Glyph=202; Class=4
'FFFF 0000 ' # 88: Glyph=<end>; Value=<filler>
# State array.
'0000 0000 0000 0000 0000 0001 ' # 92: State[0][0..5]
'0000 0000 0000 0000 0000 0001 ' # 104: State[1][0..5]
'0000 0000 0000 0000 0002 0001 ' # 116: State[2][0..5]
# Entry table.
'0000 0000 ' # 128: Entries[0].NewState=0, .Flags=0
'FFFF FFFF ' # 132: Entries[0].MarkSubst=None, .CurSubst=None
'0002 0000 ' # 136: Entries[1].NewState=2, .Flags=0
'FFFF FFFF ' # 140: Entries[1].MarkSubst=None, .CurSubst=None
'0000 0000 ' # 144: Entries[2].NewState=0, .Flags=0
'FFFF 0000 ' # 148: Entries[2].MarkSubst=None, .CurSubst=PerGlyph #0
# 152: <no padding needed for 4-byte alignment>
# Per-glyph lookup tables.
'0000 0004 ' # 152: Offset from this point to per-glyph lookup #0.
# Per-glyph lookup #0.
'0006 0004 ' # 156: ClassTable.LookupFormat=6, .UnitSize=4
'0004 0010 ' # 160: .NUnits=4, .SearchRange=16
'0002 0000 ' # 164: .EntrySelector=2, .RangeShift=0
'0032 0258 ' # 168: Glyph=50; ReplacementGlyph=600
'0034 0259 ' # 172: Glyph=52; ReplacementGlyph=601
'00C9 025A ' # 176: Glyph=201; ReplacementGlyph=602
'00CA 0384 ' # 180: Glyph=202; ReplacementGlyph=900
'FFFF 0000 ' # 184: Glyph=<end>; Value=<filler>
) # 188: <end>
assert len(MORX_CONTEXTUAL_DATA) == 188, len(MORX_CONTEXTUAL_DATA)
MORX_CONTEXTUAL_XML = [
'<Version value="2"/>',
'<Reserved value="0"/>',
'<!-- MorphChainCount=1 -->',
'<MorphChain index="0">',
' <DefaultFlags value="0x00000001"/>',
' <!-- StructLength=180 -->',
' <!-- MorphFeatureCount=0 -->',
' <!-- MorphSubtableCount=1 -->',
' <MorphSubtable index="0">',
' <!-- StructLength=164 -->',
' <CoverageFlags value="128"/>',
' <Reserved value="0"/>',
' <!-- MorphType=1 -->',
' <SubFeatureFlags value="0x00000001"/>',
' <ContextualMorph>',
' <StateTable>',
' <!-- GlyphClassCount=6 -->',
' <GlyphClass glyph="A" value="4"/>',
' <GlyphClass glyph="B" value="4"/>',
' <GlyphClass glyph="C" value="5"/>',
' <GlyphClass glyph="X" value="4"/>',
' <GlyphClass glyph="Y" value="4"/>',
' <State index="0">',
' <Transition onGlyphClass="0">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="1">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="2">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="3">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="4">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="5">',
' <NewState value="2"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' </State>',
' <State index="1">',
' <Transition onGlyphClass="0">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="1">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="2">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="3">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="4">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="5">',
' <NewState value="2"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' </State>',
' <State index="2">',
' <Transition onGlyphClass="0">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="1">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="2">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="3">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' <Transition onGlyphClass="4">',
' <NewState value="0"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="0"/>',
' </Transition>',
' <Transition onGlyphClass="5">',
' <NewState value="2"/>',
' <MarkIndex value="65535"/>',
' <CurrentIndex value="65535"/>',
' </Transition>',
' </State>',
' <PerGlyphLookup index="0">',
' <Lookup glyph="A" value="A.swash"/>',
' <Lookup glyph="B" value="B.swash"/>',
' <Lookup glyph="X" value="X.swash"/>',
' <Lookup glyph="Y" value="Y.swash"/>',
' </PerGlyphLookup>',
' </StateTable>',
' </ContextualMorph>',
' </MorphSubtable>',
'</MorphChain>',
]
class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase):
@classmethod
@ -276,6 +503,31 @@ class MORXRearrangementTest(unittest.TestCase):
hexStr(MORX_REARRANGEMENT_DATA))
class MORXContextualSubstitutionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.maxDiff = None
g = ['.notdef'] + ['g.%d' % i for i in range (1, 910)]
g[80] = 'C'
g[50], g[52], g[201], g[202] = 'A', 'B', 'X', 'Y'
g[600], g[601], g[602], g[900] = (
'A.swash', 'B.swash', 'X.swash', 'Y.swash')
cls.font = FakeFont(g)
def test_decompile_toXML(self):
table = newTable('morx')
table.decompile(MORX_CONTEXTUAL_DATA, self.font)
self.assertEqual(getXML(table.toXML), MORX_CONTEXTUAL_XML)
def test_compile_fromXML(self):
table = newTable('morx')
for name, attrs, content in parseXML(MORX_CONTEXTUAL_XML):
table.fromXML(name, attrs, content, font=self.font)
self.assertEqual(hexStr(table.compile(self.font)),
hexStr(MORX_CONTEXTUAL_DATA))
if __name__ == '__main__':
import sys
sys.exit(unittest.main())

View File

@ -370,12 +370,12 @@ class AlternateSubstTest(unittest.TestCase):
})
class AATRearrangementTest(unittest.TestCase):
class RearrangementMorphActionTest(unittest.TestCase):
def setUp(self):
self.font = FakeFont(['.notdef', 'A', 'B', 'C'])
def testCompile(self):
r = otTables.AATRearrangement()
r = otTables.RearrangementMorphAction()
r.NewState = 0x1234
r.MarkFirst = r.DontAdvance = r.MarkLast = True
r.ReservedFlags, r.Verb = 0x1FF0, 0xD
@ -384,7 +384,7 @@ class AATRearrangementTest(unittest.TestCase):
self.assertEqual(hexStr(writer.getAllData()), "1234fffd")
def testDecompileToXML(self):
r = otTables.AATRearrangement()
r = otTables.RearrangementMorphAction()
r.decompile(OTTableReader(deHexStr("1234fffd")), self.font)
toXML = lambda w, f: r.toXML(w, f, {"Test": "Foo"}, "Transition")
self.assertEqual(getXML(toXML, self.font), [
@ -397,6 +397,34 @@ class AATRearrangementTest(unittest.TestCase):
])
class ContextualMorphActionTest(unittest.TestCase):
def setUp(self):
self.font = FakeFont(['.notdef', 'A', 'B', 'C'])
def testCompile(self):
a = otTables.ContextualMorphAction()
a.NewState = 0x1234
a.SetMark, a.DontAdvance, a.ReservedFlags = True, True, 0x3117
a.MarkIndex, a.CurrentIndex = 0xDEAD, 0xBEEF
writer = OTTableWriter()
a.compile(writer, self.font)
self.assertEqual(hexStr(writer.getAllData()), "1234f117deadbeef")
def testDecompileToXML(self):
a = otTables.ContextualMorphAction()
a.decompile(OTTableReader(deHexStr("1234f117deadbeef")), self.font)
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="SetMark,DontAdvance"/>',
' <ReservedFlags value="0x3117"/>',
' <MarkIndex value="57005"/>', # 0xDEAD = 57005
' <CurrentIndex value="48879"/>', # 0xBEEF = 48879
'</Transition>',
])
if __name__ == "__main__":
import sys
sys.exit(unittest.main())