[morx] Compile ligature actions subtable for AAT ligatures
Tests fail because other subtables still need to be implemented.
This commit is contained in:
parent
04f01f245b
commit
86454e79de
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -471,16 +471,17 @@ MORX_CONTEXTUAL_XML = [
|
||||
# to make the data a structurally valid ‘morx’ table;
|
||||
#
|
||||
# * at offsets 88..91 (offsets 52..55 in Apple’s document), we’ve
|
||||
# changed the range of the third segment from 23..24 to 25..28,
|
||||
# matching the comments (but not the values) in Apple’s document;
|
||||
# changed the range of the third segment from 23..24 to 26..28.
|
||||
# The hexdump values in Apple’s 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 Apple’s document), we’ve
|
||||
# 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 Apple’s 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
|
||||
# Apple’s 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user