[morx] Compile ligature actions subtable for AAT ligatures

Tests fail because other subtables still need to be implemented.
This commit is contained in:
Sascha Brawer 2017-09-21 02:27:29 +02:00
parent 04f01f245b
commit 86454e79de
3 changed files with 119 additions and 13 deletions

View File

@ -975,13 +975,21 @@ class STXHeader(BaseConverter):
glyphClassTableOffset = 16 # size of STXHeader
if self.perGlyphLookup is not None:
glyphClassTableOffset += 4
ligActionData, ligActionIndex = None, None
if issubclass(self.tableClass, LigatureMorphAction):
ligActionData, ligActionIndex = \
self._compileLigActions(value, font)
ligActionData = pad(ligActionData, 4)
stateArrayWriter = OTTableWriter()
entries, entryIDs = [], {}
for state in value.States:
for glyphClass in range(glyphClassCount):
transition = state.Transitions[glyphClass]
entryWriter = OTTableWriter()
transition.compile(entryWriter, font)
transition.compile(entryWriter, font,
ligActionIndex)
entryData = entryWriter.getAllData()
assert len(entryData) == transition.staticSize, ( \
"%s has staticSize %d, "
@ -1002,16 +1010,29 @@ class STXHeader(BaseConverter):
perGlyphOffset = entryTableOffset + len(entryTableData)
perGlyphData = \
pad(self._compilePerGlyphLookups(value, font), 4)
if ligActionData is None:
ligActionOffset = None
else:
assert len(perGlyphData) == 0
ligActionOffset = entryTableOffset + len(entryTableData)
componentBaseOffset = ligActionOffset + len(ligActionData)
ligListOffset = 0xCAFEBABE
writer.writeULong(glyphClassCount)
writer.writeULong(glyphClassTableOffset)
writer.writeULong(stateArrayOffset)
writer.writeULong(entryTableOffset)
if self.perGlyphLookup is not None:
writer.writeULong(perGlyphOffset)
if ligActionOffset is not None:
writer.writeULong(ligActionOffset)
writer.writeULong(componentBaseOffset)
writer.writeULong(ligListOffset)
writer.writeData(glyphClassData)
writer.writeData(stateArrayData)
writer.writeData(entryTableData)
writer.writeData(perGlyphData)
if ligActionData is not None:
writer.writeData(ligActionData)
def _compilePerGlyphLookups(self, table, font):
if self.perGlyphLookup is None:
@ -1030,6 +1051,35 @@ class STXHeader(BaseConverter):
writer.writeSubTable(lookupWriter)
return writer.getAllData()
def _compileLigActions(self, table, font):
assert issubclass(self.tableClass, LigatureMorphAction)
ligActions = set()
for state in table.States:
for _glyphClass, trans in state.Transitions.items():
ligActions.add(trans.compileLigActions())
result, ligActionIndex = b"", {}
# Sort the compiled actions in decreasing order of
# length, so that the longer sequence come before the
# shorter ones. For each compiled action ABCD, its
# suffixes BCD, CD, and D do not be encoded separately
# (in case they occur); instead, we can just store an
# index that points into the middle of the longer
# sequence. Every compiled AAT ligature sequence is
# terminated with an end-of-sequence flag, which can
# only be set on the last element of the sequence.
# Therefore, it is sufficient to consider just the
# suffixes.
for a in sorted(ligActions, key=lambda x:(-len(x), x)):
if a not in ligActionIndex:
for i in range(0, len(a), 4):
suffix = a[i:]
suffixIndex = (len(result) + i) // 4
ligActionIndex.setdefault(
suffix, suffixIndex)
result += a
assert len(result) % self.tableClass.staticSize == 0
return (result, ligActionIndex)
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.begintag(name, attrs)
xmlWriter.newline()

View File

@ -11,6 +11,7 @@ from fontTools.misc.textTools import safeEval
from .otBase import BaseTable, FormatSwitchingBaseTable
import operator
import logging
import struct
log = logging.getLogger(__name__)
@ -78,7 +79,8 @@ class RearrangementMorphAction(AATAction):
self.MarkLast = False
self.ReservedFlags = 0
def compile(self, writer, font):
def compile(self, writer, font, ligActionIndex):
assert ligActionIndex is None
writer.writeUShort(self.NewState)
assert self.Verb >= 0 and self.Verb <= 15, self.Verb
flags = self.Verb | self.ReservedFlags
@ -137,7 +139,8 @@ class ContextualMorphAction(AATAction):
self.ReservedFlags = 0
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
def compile(self, writer, font):
def compile(self, writer, font, ligActionIndex):
assert ligActionIndex is None
writer.writeUShort(self.NewState)
flags = self.ReservedFlags
if self.SetMark: flags |= 0x8000
@ -214,6 +217,20 @@ class LigatureMorphAction(AATAction):
self.ReservedFlags = 0
self.Actions = []
def compile(self, writer, font, ligActionIndex):
assert ligActionIndex is not None
writer.writeUShort(self.NewState)
flags = self.ReservedFlags
if self.SetComponent: flags |= 0x8000
if self.DontAdvance: flags |= 0x4000
if len(self.Actions) > 0: flags |= 0x2000
writer.writeUShort(flags)
if len(self.Actions) > 0:
actions = self.compileLigActions()
writer.writeUShort(ligActionIndex[actions])
else:
writer.writeUShort(0)
def decompile(self, reader, font, ligActionReader):
assert ligActionReader is not None
self.NewState = reader.readUShort()
@ -234,6 +251,16 @@ class LigatureMorphAction(AATAction):
else:
self.Actions = []
def compileLigActions(self):
result = []
for i, action in enumerate(self.Actions):
last = (i == len(self.Actions) - 1)
value = action.GlyphIndexDelta & 0x3FFFFFFF
value |= 0x80000000 if last else 0
value |= 0x40000000 if action.Store else 0
result.append(struct.pack(">L", value))
return bytesjoin(result)
def _decompileLigActions(self, ligActionReader, ligActionIndex):
actions = []
last = False
@ -251,6 +278,29 @@ class LigatureMorphAction(AATAction):
action.GlyphIndexDelta = delta
return actions
def fromXML(self, name, attrs, content, font):
self.NewState = self.ReservedFlags = 0
self.SetComponent = self.DontAdvance = False
self.ReservedFlags = 0
self.Actions = []
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 == "Action":
action = LigAction()
flags = eltAttrs.get("Flags", "").split(",")
flags = [f.strip() for f in flags]
action.Store = "Store" in flags
action.GlyphIndexDelta = safeEval(
eltAttrs["GlyphIndexDelta"])
self.Actions.append(action)
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
xmlWriter.newline()

View File

@ -471,16 +471,17 @@ MORX_CONTEXTUAL_XML = [
# to make the data a structurally valid morx table;
#
# * at offsets 88..91 (offsets 52..55 in Apples document), weve
# changed the range of the third segment from 23..24 to 25..28,
# matching the comments (but not the values) in Apples document;
# changed the range of the third segment from 23..24 to 26..28.
# The hexdump values in Apples specification are completely wrong;
# the values from the comments would work, but they can be encoded
# more compactly than in the specification example. For round-trip
# testing, we omit the f glyph, which makes AAT lookup format 2
# the most compact encoding;
#
# * at offsets 92..93 (offsets 56..57 in Apples document), weve
# changed the glyphclass of the third segment from 5 to 6. Without
# this change, the second and third glyph class have the same glyph
# class, so an encoder may merge them into a single segment beause
# the adjacent GlyphID ranges. Without changing the glyph class of
# the third segment from 5 to 6, our round-trip compilation tests
# would be broken. This also matches Apples comments in the spec.
# changed the glyph class of the third segment from 5 to 6, which
# matches the values from the comments to the spec (but not the
# Apples hexdump).
#
# TODO: Ask Apple to fix “Example 2” in the morx specification.
MORX_LIGATURE_DATA = deHexStr(
@ -511,7 +512,7 @@ MORX_LIGATURE_DATA = deHexStr(
'0001 0006 ' # 72: .EntrySelector=1, .RangeShift=6
'0016 0014 0004 ' # 76: GlyphID 20..22 [a..c] -> GlyphClass 4
'0018 0017 0005 ' # 82: GlyphID 23..24 [d..e] -> GlyphClass 5
'001C 0019 0006 ' # 88: GlyphID 25..26 [f..i] -> GlyphClass 6
'001C 001A 0006 ' # 88: GlyphID 26..28 [g..i] -> GlyphClass 6
'FFFF FFFF 0000 ' # 94: <end of lookup>
# State array.
@ -574,7 +575,6 @@ MORX_LIGATURE_XML = [
' <GlyphClass glyph="c" value="4"/>',
' <GlyphClass glyph="d" value="5"/>',
' <GlyphClass glyph="e" value="5"/>',
' <GlyphClass glyph="f" value="6"/>',
' <GlyphClass glyph="g" value="6"/>',
' <GlyphClass glyph="h" value="6"/>',
' <GlyphClass glyph="i" value="6"/>',
@ -773,6 +773,12 @@ class MORXLigatureSubstitutionTest(unittest.TestCase):
table.decompile(MORX_LIGATURE_DATA, self.font)
self.assertEqual(getXML(table.toXML), MORX_LIGATURE_XML)
def test_compile_fromXML(self):
table = newTable('morx')
for name, attrs, content in parseXML(MORX_LIGATURE_XML):
table.fromXML(name, attrs, content, font=self.font)
self.assertEqual(hexStr(table.compile(self.font)),
hexStr(MORX_LIGATURE_DATA))
if __name__ == '__main__':
import sys