[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.misc.textTools import pad, safeEval
|
||||||
from fontTools.ttLib import getSearchRange
|
from fontTools.ttLib import getSearchRange
|
||||||
from .otBase import ValueRecordFactory, CountReference, OTTableWriter
|
from .otBase import ValueRecordFactory, CountReference, OTTableWriter
|
||||||
from .otTables import AATStateTable, AATState
|
from .otTables import (AATStateTable, AATState, AATAction,
|
||||||
|
ContextualMorphAction)
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
@ -880,8 +881,13 @@ class AATLookupWithDataOffset(BaseConverter):
|
|||||||
class STXHeader(BaseConverter):
|
class STXHeader(BaseConverter):
|
||||||
def __init__(self, name, repeat, aux, tableClass):
|
def __init__(self, name, repeat, aux, tableClass):
|
||||||
BaseConverter.__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)
|
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):
|
def read(self, reader, font, tableDict):
|
||||||
table = AATStateTable()
|
table = AATStateTable()
|
||||||
@ -893,6 +899,9 @@ class STXHeader(BaseConverter):
|
|||||||
classTableReader.seek(pos + reader.readULong())
|
classTableReader.seek(pos + reader.readULong())
|
||||||
stateArrayReader.seek(pos + reader.readULong())
|
stateArrayReader.seek(pos + reader.readULong())
|
||||||
entryTableReader.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,
|
table.GlyphClasses = self.classLookup.read(classTableReader,
|
||||||
font, tableDict)
|
font, tableDict)
|
||||||
numStates = int((entryTableReader.pos - stateArrayReader.pos)
|
numStates = int((entryTableReader.pos - stateArrayReader.pos)
|
||||||
@ -905,6 +914,9 @@ class STXHeader(BaseConverter):
|
|||||||
state.Transitions[glyphClass] = \
|
state.Transitions[glyphClass] = \
|
||||||
self._readTransition(entryTableReader,
|
self._readTransition(entryTableReader,
|
||||||
entryIndex, font)
|
entryIndex, font)
|
||||||
|
if self.perGlyphLookup is not None:
|
||||||
|
table.PerGlyphLookups = self._readPerGlyphLookups(
|
||||||
|
table, perGlyphTableReader, font)
|
||||||
return table
|
return table
|
||||||
|
|
||||||
def _readTransition(self, reader, entryIndex, font):
|
def _readTransition(self, reader, entryIndex, font):
|
||||||
@ -914,6 +926,35 @@ class STXHeader(BaseConverter):
|
|||||||
transition.decompile(entryReader, font)
|
transition.decompile(entryReader, font)
|
||||||
return transition
|
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):
|
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||||
glyphClassWriter = OTTableWriter()
|
glyphClassWriter = OTTableWriter()
|
||||||
self.classLookup.write(glyphClassWriter, font, tableDict,
|
self.classLookup.write(glyphClassWriter, font, tableDict,
|
||||||
@ -921,6 +962,8 @@ class STXHeader(BaseConverter):
|
|||||||
glyphClassData = pad(glyphClassWriter.getAllData(), 4)
|
glyphClassData = pad(glyphClassWriter.getAllData(), 4)
|
||||||
glyphClassCount = max(value.GlyphClasses.values()) + 1
|
glyphClassCount = max(value.GlyphClasses.values()) + 1
|
||||||
glyphClassTableOffset = 16 # size of STXHeader
|
glyphClassTableOffset = 16 # size of STXHeader
|
||||||
|
if self.perGlyphLookup is not None:
|
||||||
|
glyphClassTableOffset += 4
|
||||||
stateArrayWriter = OTTableWriter()
|
stateArrayWriter = OTTableWriter()
|
||||||
entries, entryIDs = [], {}
|
entries, entryIDs = [], {}
|
||||||
for state in value.States:
|
for state in value.States:
|
||||||
@ -942,16 +985,39 @@ class STXHeader(BaseConverter):
|
|||||||
entries.append(entryData)
|
entries.append(entryData)
|
||||||
stateArrayWriter.writeUShort(entryIndex)
|
stateArrayWriter.writeUShort(entryIndex)
|
||||||
stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
|
stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
|
||||||
stateArrayData = stateArrayWriter.getAllData()
|
stateArrayData = pad(stateArrayWriter.getAllData(), 4)
|
||||||
entryTableOffset = stateArrayOffset + len(stateArrayData)
|
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(glyphClassCount)
|
||||||
writer.writeULong(glyphClassTableOffset)
|
writer.writeULong(glyphClassTableOffset)
|
||||||
writer.writeULong(stateArrayOffset)
|
writer.writeULong(stateArrayOffset)
|
||||||
writer.writeULong(entryTableOffset)
|
writer.writeULong(entryTableOffset)
|
||||||
|
if self.perGlyphLookup is not None:
|
||||||
|
writer.writeULong(perGlyphOffset)
|
||||||
writer.writeData(glyphClassData)
|
writer.writeData(glyphClassData)
|
||||||
writer.writeData(stateArrayData)
|
writer.writeData(stateArrayData)
|
||||||
for entry in entries:
|
writer.writeData(entryTableData)
|
||||||
writer.writeData(entry)
|
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):
|
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||||
xmlWriter.begintag(name, attrs)
|
xmlWriter.begintag(name, attrs)
|
||||||
@ -970,6 +1036,15 @@ class STXHeader(BaseConverter):
|
|||||||
name="Transition")
|
name="Transition")
|
||||||
xmlWriter.endtag("State")
|
xmlWriter.endtag("State")
|
||||||
xmlWriter.newline()
|
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.endtag(name)
|
||||||
xmlWriter.newline()
|
xmlWriter.newline()
|
||||||
|
|
||||||
@ -983,6 +1058,10 @@ class STXHeader(BaseConverter):
|
|||||||
elif eltName == "State":
|
elif eltName == "State":
|
||||||
state = self._xmlReadState(eltAttrs, eltContent, font)
|
state = self._xmlReadState(eltAttrs, eltContent, font)
|
||||||
table.States.append(state)
|
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
|
table.GlyphClassCount = max(table.GlyphClasses.values()) + 1
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
@ -1387,12 +1387,11 @@ otData = [
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
('RearrangementMorph', [
|
('RearrangementMorph', [
|
||||||
('STXHeader(AATRearrangement)', 'StateTable', None, None, 'Finite-state transducer table.'),
|
('STXHeader(RearrangementMorphAction)', 'StateTable', None, None, 'Finite-state transducer table for indic rearrangement.'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
('ContextualMorph', [
|
('ContextualMorph', [
|
||||||
('struct', 'StateHeader', None, None, 'Header.'),
|
('STXHeader(ContextualMorphAction)', 'StateTable', None, None, 'Finite-state transducer for contextual glyph substitution.'),
|
||||||
# TODO: Add missing parts.
|
|
||||||
]),
|
]),
|
||||||
|
|
||||||
('LigatureMorph', [
|
('LigatureMorph', [
|
||||||
|
@ -18,16 +18,38 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class AATStateTable(object):
|
class AATStateTable(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.GlyphClasses = {} # GlyphName --> GlyphClass
|
self.GlyphClasses = {} # GlyphID --> GlyphClass
|
||||||
self.States = [] # List of AATState, indexed by state number
|
self.States = [] # List of AATState, indexed by state number
|
||||||
|
self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...]
|
||||||
|
|
||||||
|
|
||||||
class AATState(object):
|
class AATState(object):
|
||||||
def __init__(self):
|
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
|
staticSize = 4
|
||||||
|
_FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
|
||||||
|
|
||||||
_VERBS = {
|
_VERBS = {
|
||||||
0: "no change",
|
0: "no change",
|
||||||
@ -79,15 +101,7 @@ class AATRearrangement(object):
|
|||||||
xmlWriter.newline()
|
xmlWriter.newline()
|
||||||
xmlWriter.simpletag("NewState", value=self.NewState)
|
xmlWriter.simpletag("NewState", value=self.NewState)
|
||||||
xmlWriter.newline()
|
xmlWriter.newline()
|
||||||
flags = [f for f in ("MarkFirst", "DontAdvance", "MarkLast")
|
self._writeFlagsToXML(xmlWriter)
|
||||||
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()
|
|
||||||
xmlWriter.simpletag("Verb", value=self.Verb)
|
xmlWriter.simpletag("Verb", value=self.Verb)
|
||||||
verbComment = self._VERBS.get(self.Verb)
|
verbComment = self._VERBS.get(self.Verb)
|
||||||
if verbComment is not None:
|
if verbComment is not None:
|
||||||
@ -111,11 +125,66 @@ class AATRearrangement(object):
|
|||||||
for flag in eltAttrs["value"].split(","):
|
for flag in eltAttrs["value"].split(","):
|
||||||
self._setFlag(flag.strip())
|
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):
|
class FeatureParams(BaseTable):
|
||||||
|
|
||||||
@ -1097,7 +1166,7 @@ def _buildClasses():
|
|||||||
},
|
},
|
||||||
'morx': {
|
'morx': {
|
||||||
0: RearrangementMorph,
|
0: RearrangementMorph,
|
||||||
# 1: ContextualMorph,
|
1: ContextualMorph,
|
||||||
# 2: LigatureMorph,
|
# 2: LigatureMorph,
|
||||||
# 3: Reserved,
|
# 3: Reserved,
|
||||||
4: NoncontextualMorph,
|
4: NoncontextualMorph,
|
||||||
|
@ -90,7 +90,7 @@ MORX_REARRANGEMENT_DATA = deHexStr(
|
|||||||
'0002 0000 ' # 0: Version=2, Reserved=0
|
'0002 0000 ' # 0: Version=2, Reserved=0
|
||||||
'0000 0001 ' # 4: MorphChainCount=1
|
'0000 0001 ' # 4: MorphChainCount=1
|
||||||
'0000 0001 ' # 8: DefaultFlags=1
|
'0000 0001 ' # 8: DefaultFlags=1
|
||||||
'0000 0078 ' # 12: StructLength=120
|
'0000 0078 ' # 12: StructLength=120 (+8=128)
|
||||||
'0000 0000 ' # 16: MorphFeatureCount=0
|
'0000 0000 ' # 16: MorphFeatureCount=0
|
||||||
'0000 0001 ' # 20: MorphSubtableCount=1
|
'0000 0001 ' # 20: MorphSubtableCount=1
|
||||||
'0000 0068 ' # 24: Subtable[0].StructLength=104 (+24=128)
|
'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):
|
class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -276,6 +503,31 @@ class MORXRearrangementTest(unittest.TestCase):
|
|||||||
hexStr(MORX_REARRANGEMENT_DATA))
|
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__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
sys.exit(unittest.main())
|
sys.exit(unittest.main())
|
||||||
|
@ -370,12 +370,12 @@ class AlternateSubstTest(unittest.TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class AATRearrangementTest(unittest.TestCase):
|
class RearrangementMorphActionTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.font = FakeFont(['.notdef', 'A', 'B', 'C'])
|
self.font = FakeFont(['.notdef', 'A', 'B', 'C'])
|
||||||
|
|
||||||
def testCompile(self):
|
def testCompile(self):
|
||||||
r = otTables.AATRearrangement()
|
r = otTables.RearrangementMorphAction()
|
||||||
r.NewState = 0x1234
|
r.NewState = 0x1234
|
||||||
r.MarkFirst = r.DontAdvance = r.MarkLast = True
|
r.MarkFirst = r.DontAdvance = r.MarkLast = True
|
||||||
r.ReservedFlags, r.Verb = 0x1FF0, 0xD
|
r.ReservedFlags, r.Verb = 0x1FF0, 0xD
|
||||||
@ -384,7 +384,7 @@ class AATRearrangementTest(unittest.TestCase):
|
|||||||
self.assertEqual(hexStr(writer.getAllData()), "1234fffd")
|
self.assertEqual(hexStr(writer.getAllData()), "1234fffd")
|
||||||
|
|
||||||
def testDecompileToXML(self):
|
def testDecompileToXML(self):
|
||||||
r = otTables.AATRearrangement()
|
r = otTables.RearrangementMorphAction()
|
||||||
r.decompile(OTTableReader(deHexStr("1234fffd")), self.font)
|
r.decompile(OTTableReader(deHexStr("1234fffd")), self.font)
|
||||||
toXML = lambda w, f: r.toXML(w, f, {"Test": "Foo"}, "Transition")
|
toXML = lambda w, f: r.toXML(w, f, {"Test": "Foo"}, "Transition")
|
||||||
self.assertEqual(getXML(toXML, self.font), [
|
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__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
sys.exit(unittest.main())
|
sys.exit(unittest.main())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user