[AAT] Support morx
tables with contextual substitution
This commit is contained in:
parent
31b02d0bed
commit
ee1662e57e
@ -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
|
||||
|
||||
|
@ -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', [
|
||||
|
@ -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,
|
||||
|
@ -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 Apple’s specification, we’ve
|
||||
# made the following changes:
|
||||
#
|
||||
# * at offsets 0..35, we’ve prepended 36 bytes of boilerplate
|
||||
# to make the data a structurally valid ‘morx’ table;
|
||||
#
|
||||
# * at offset 36 (offset 0 in Apple’s document), we’ve 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 Apple’s document), we’ve replaced
|
||||
# the presumably leftover ‘XXX’ mark by an actual data offset;
|
||||
#
|
||||
# * at offset 72 (offset 36 in Apple’s document), we’ve 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 Apple’s
|
||||
# example;
|
||||
#
|
||||
# * at offset 90 (offset 54 in Apple’s document), we’ve changed
|
||||
# the value for the lookup end-of-table marker from 1 to 0.
|
||||
# Fonttools always uses zero for this value, whereas Apple’s
|
||||
# spec examples are inconsistently using one of {0, 1, 0xFFFF}
|
||||
# for this filler value;
|
||||
#
|
||||
# * at offset 172 (offset 136 in Apple’s document), we’ve 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())
|
||||
|
@ -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())
|
||||
|
Loading…
x
Reference in New Issue
Block a user