From 86454e79decb39a08fa00c36b76d9fc8ea23fd2f Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Thu, 21 Sep 2017 02:27:29 +0200 Subject: [PATCH] [morx] Compile ligature actions subtable for AAT ligatures Tests fail because other subtables still need to be implemented. --- Lib/fontTools/ttLib/tables/otConverters.py | 52 ++++++++++++++++++++- Lib/fontTools/ttLib/tables/otTables.py | 54 +++++++++++++++++++++- Tests/ttLib/tables/_m_o_r_x_test.py | 26 +++++++---- 3 files changed, 119 insertions(+), 13 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 44964182c..4e1faf709 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -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() diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index dd6b2e34e..aca3b6d76 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -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() diff --git a/Tests/ttLib/tables/_m_o_r_x_test.py b/Tests/ttLib/tables/_m_o_r_x_test.py index e53940efe..113b28cc7 100644 --- a/Tests/ttLib/tables/_m_o_r_x_test.py +++ b/Tests/ttLib/tables/_m_o_r_x_test.py @@ -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: # State array. @@ -574,7 +575,6 @@ MORX_LIGATURE_XML = [ ' ', ' ', ' ', - ' ', ' ', ' ', ' ', @@ -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