[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
|
glyphClassTableOffset = 16 # size of STXHeader
|
||||||
if self.perGlyphLookup is not None:
|
if self.perGlyphLookup is not None:
|
||||||
glyphClassTableOffset += 4
|
glyphClassTableOffset += 4
|
||||||
|
|
||||||
|
ligActionData, ligActionIndex = None, None
|
||||||
|
if issubclass(self.tableClass, LigatureMorphAction):
|
||||||
|
ligActionData, ligActionIndex = \
|
||||||
|
self._compileLigActions(value, font)
|
||||||
|
ligActionData = pad(ligActionData, 4)
|
||||||
|
|
||||||
stateArrayWriter = OTTableWriter()
|
stateArrayWriter = OTTableWriter()
|
||||||
entries, entryIDs = [], {}
|
entries, entryIDs = [], {}
|
||||||
for state in value.States:
|
for state in value.States:
|
||||||
for glyphClass in range(glyphClassCount):
|
for glyphClass in range(glyphClassCount):
|
||||||
transition = state.Transitions[glyphClass]
|
transition = state.Transitions[glyphClass]
|
||||||
entryWriter = OTTableWriter()
|
entryWriter = OTTableWriter()
|
||||||
transition.compile(entryWriter, font)
|
transition.compile(entryWriter, font,
|
||||||
|
ligActionIndex)
|
||||||
entryData = entryWriter.getAllData()
|
entryData = entryWriter.getAllData()
|
||||||
assert len(entryData) == transition.staticSize, ( \
|
assert len(entryData) == transition.staticSize, ( \
|
||||||
"%s has staticSize %d, "
|
"%s has staticSize %d, "
|
||||||
@ -1002,16 +1010,29 @@ class STXHeader(BaseConverter):
|
|||||||
perGlyphOffset = entryTableOffset + len(entryTableData)
|
perGlyphOffset = entryTableOffset + len(entryTableData)
|
||||||
perGlyphData = \
|
perGlyphData = \
|
||||||
pad(self._compilePerGlyphLookups(value, font), 4)
|
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(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:
|
if self.perGlyphLookup is not None:
|
||||||
writer.writeULong(perGlyphOffset)
|
writer.writeULong(perGlyphOffset)
|
||||||
|
if ligActionOffset is not None:
|
||||||
|
writer.writeULong(ligActionOffset)
|
||||||
|
writer.writeULong(componentBaseOffset)
|
||||||
|
writer.writeULong(ligListOffset)
|
||||||
writer.writeData(glyphClassData)
|
writer.writeData(glyphClassData)
|
||||||
writer.writeData(stateArrayData)
|
writer.writeData(stateArrayData)
|
||||||
writer.writeData(entryTableData)
|
writer.writeData(entryTableData)
|
||||||
writer.writeData(perGlyphData)
|
writer.writeData(perGlyphData)
|
||||||
|
if ligActionData is not None:
|
||||||
|
writer.writeData(ligActionData)
|
||||||
|
|
||||||
def _compilePerGlyphLookups(self, table, font):
|
def _compilePerGlyphLookups(self, table, font):
|
||||||
if self.perGlyphLookup is None:
|
if self.perGlyphLookup is None:
|
||||||
@ -1030,6 +1051,35 @@ class STXHeader(BaseConverter):
|
|||||||
writer.writeSubTable(lookupWriter)
|
writer.writeSubTable(lookupWriter)
|
||||||
return writer.getAllData()
|
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):
|
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||||
xmlWriter.begintag(name, attrs)
|
xmlWriter.begintag(name, attrs)
|
||||||
xmlWriter.newline()
|
xmlWriter.newline()
|
||||||
|
@ -11,6 +11,7 @@ from fontTools.misc.textTools import safeEval
|
|||||||
from .otBase import BaseTable, FormatSwitchingBaseTable
|
from .otBase import BaseTable, FormatSwitchingBaseTable
|
||||||
import operator
|
import operator
|
||||||
import logging
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -78,7 +79,8 @@ class RearrangementMorphAction(AATAction):
|
|||||||
self.MarkLast = False
|
self.MarkLast = False
|
||||||
self.ReservedFlags = 0
|
self.ReservedFlags = 0
|
||||||
|
|
||||||
def compile(self, writer, font):
|
def compile(self, writer, font, ligActionIndex):
|
||||||
|
assert ligActionIndex is None
|
||||||
writer.writeUShort(self.NewState)
|
writer.writeUShort(self.NewState)
|
||||||
assert self.Verb >= 0 and self.Verb <= 15, self.Verb
|
assert self.Verb >= 0 and self.Verb <= 15, self.Verb
|
||||||
flags = self.Verb | self.ReservedFlags
|
flags = self.Verb | self.ReservedFlags
|
||||||
@ -137,7 +139,8 @@ class ContextualMorphAction(AATAction):
|
|||||||
self.ReservedFlags = 0
|
self.ReservedFlags = 0
|
||||||
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
|
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)
|
writer.writeUShort(self.NewState)
|
||||||
flags = self.ReservedFlags
|
flags = self.ReservedFlags
|
||||||
if self.SetMark: flags |= 0x8000
|
if self.SetMark: flags |= 0x8000
|
||||||
@ -214,6 +217,20 @@ class LigatureMorphAction(AATAction):
|
|||||||
self.ReservedFlags = 0
|
self.ReservedFlags = 0
|
||||||
self.Actions = []
|
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):
|
def decompile(self, reader, font, ligActionReader):
|
||||||
assert ligActionReader is not None
|
assert ligActionReader is not None
|
||||||
self.NewState = reader.readUShort()
|
self.NewState = reader.readUShort()
|
||||||
@ -234,6 +251,16 @@ class LigatureMorphAction(AATAction):
|
|||||||
else:
|
else:
|
||||||
self.Actions = []
|
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):
|
def _decompileLigActions(self, ligActionReader, ligActionIndex):
|
||||||
actions = []
|
actions = []
|
||||||
last = False
|
last = False
|
||||||
@ -251,6 +278,29 @@ class LigatureMorphAction(AATAction):
|
|||||||
action.GlyphIndexDelta = delta
|
action.GlyphIndexDelta = delta
|
||||||
return actions
|
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):
|
def toXML(self, xmlWriter, font, attrs, name):
|
||||||
xmlWriter.begintag(name, **attrs)
|
xmlWriter.begintag(name, **attrs)
|
||||||
xmlWriter.newline()
|
xmlWriter.newline()
|
||||||
|
@ -471,16 +471,17 @@ MORX_CONTEXTUAL_XML = [
|
|||||||
# to make the data a structurally valid ‘morx’ table;
|
# to make the data a structurally valid ‘morx’ table;
|
||||||
#
|
#
|
||||||
# * at offsets 88..91 (offsets 52..55 in Apple’s document), we’ve
|
# * 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,
|
# changed the range of the third segment from 23..24 to 26..28.
|
||||||
# matching the comments (but not the values) in Apple’s document;
|
# 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
|
# * 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
|
# changed the glyph class of the third segment from 5 to 6, which
|
||||||
# this change, the second and third glyph class have the same glyph
|
# matches the values from the comments to the spec (but not the
|
||||||
# class, so an encoder may merge them into a single segment beause
|
# Apple’s hexdump).
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# TODO: Ask Apple to fix “Example 2” in the ‘morx’ specification.
|
# TODO: Ask Apple to fix “Example 2” in the ‘morx’ specification.
|
||||||
MORX_LIGATURE_DATA = deHexStr(
|
MORX_LIGATURE_DATA = deHexStr(
|
||||||
@ -511,7 +512,7 @@ MORX_LIGATURE_DATA = deHexStr(
|
|||||||
'0001 0006 ' # 72: .EntrySelector=1, .RangeShift=6
|
'0001 0006 ' # 72: .EntrySelector=1, .RangeShift=6
|
||||||
'0016 0014 0004 ' # 76: GlyphID 20..22 [a..c] -> GlyphClass 4
|
'0016 0014 0004 ' # 76: GlyphID 20..22 [a..c] -> GlyphClass 4
|
||||||
'0018 0017 0005 ' # 82: GlyphID 23..24 [d..e] -> GlyphClass 5
|
'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>
|
'FFFF FFFF 0000 ' # 94: <end of lookup>
|
||||||
|
|
||||||
# State array.
|
# State array.
|
||||||
@ -574,7 +575,6 @@ MORX_LIGATURE_XML = [
|
|||||||
' <GlyphClass glyph="c" value="4"/>',
|
' <GlyphClass glyph="c" value="4"/>',
|
||||||
' <GlyphClass glyph="d" value="5"/>',
|
' <GlyphClass glyph="d" value="5"/>',
|
||||||
' <GlyphClass glyph="e" value="5"/>',
|
' <GlyphClass glyph="e" value="5"/>',
|
||||||
' <GlyphClass glyph="f" value="6"/>',
|
|
||||||
' <GlyphClass glyph="g" value="6"/>',
|
' <GlyphClass glyph="g" value="6"/>',
|
||||||
' <GlyphClass glyph="h" value="6"/>',
|
' <GlyphClass glyph="h" value="6"/>',
|
||||||
' <GlyphClass glyph="i" value="6"/>',
|
' <GlyphClass glyph="i" value="6"/>',
|
||||||
@ -773,6 +773,12 @@ class MORXLigatureSubstitutionTest(unittest.TestCase):
|
|||||||
table.decompile(MORX_LIGATURE_DATA, self.font)
|
table.decompile(MORX_LIGATURE_DATA, self.font)
|
||||||
self.assertEqual(getXML(table.toXML), MORX_LIGATURE_XML)
|
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__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
|
Loading…
x
Reference in New Issue
Block a user