[AAT] Support morx tables with Rearrangement subtables

This commit is contained in:
Sascha Brawer 2017-09-04 12:29:55 +02:00
parent 49fc88244b
commit 31b02d0bed
5 changed files with 431 additions and 8 deletions

View File

@ -3,15 +3,17 @@ from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import (
fixedToFloat as fi2fl, floatToFixed as fl2fi, ensureVersionIsLong as fi2ve,
versionToFixed as ve2fi)
from fontTools.misc.textTools import safeEval
from fontTools.misc.textTools import pad, safeEval
from fontTools.ttLib import getSearchRange
from .otBase import ValueRecordFactory, CountReference, OTTableWriter
from .otTables import AATStateTable, AATState
from functools import partial
import struct
import logging
log = logging.getLogger(__name__)
istuple = lambda t: isinstance(t, tuple)
def buildConverters(tableSpec, tableNamespace):
@ -874,6 +876,128 @@ class AATLookupWithDataOffset(BaseConverter):
lookup.xmlWrite(xmlWriter, font, value, name, attrs)
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader
class STXHeader(BaseConverter):
def __init__(self, name, repeat, aux, tableClass):
BaseConverter.__init__(self, name, repeat, aux, tableClass)
assert tableClass is not None
self.classLookup = AATLookup("GlyphClasses", None, None, UShort)
def read(self, reader, font, tableDict):
table = AATStateTable()
pos = reader.pos
classTableReader = reader.getSubReader(0)
stateArrayReader = reader.getSubReader(0)
entryTableReader = reader.getSubReader(0)
table.GlyphClassCount = reader.readULong()
classTableReader.seek(pos + reader.readULong())
stateArrayReader.seek(pos + reader.readULong())
entryTableReader.seek(pos + reader.readULong())
table.GlyphClasses = self.classLookup.read(classTableReader,
font, tableDict)
numStates = int((entryTableReader.pos - stateArrayReader.pos)
/ (table.GlyphClassCount * 2))
for stateIndex in range(numStates):
state = AATState()
table.States.append(state)
for glyphClass in range(table.GlyphClassCount):
entryIndex = stateArrayReader.readUShort()
state.Transitions[glyphClass] = \
self._readTransition(entryTableReader,
entryIndex, font)
return table
def _readTransition(self, reader, entryIndex, font):
transition = self.tableClass()
entryReader = reader.getSubReader(
reader.pos + entryIndex * transition.staticSize)
transition.decompile(entryReader, font)
return transition
def write(self, writer, font, tableDict, value, repeatIndex=None):
glyphClassWriter = OTTableWriter()
self.classLookup.write(glyphClassWriter, font, tableDict,
value.GlyphClasses, repeatIndex=None)
glyphClassData = pad(glyphClassWriter.getAllData(), 4)
glyphClassCount = max(value.GlyphClasses.values()) + 1
glyphClassTableOffset = 16 # size of STXHeader
stateArrayWriter = OTTableWriter()
entries, entryIDs = [], {}
for state in value.States:
for glyphClass in range(glyphClassCount):
transition = state.Transitions[glyphClass]
entryWriter = OTTableWriter()
transition.compile(entryWriter, font)
entryData = entryWriter.getAllData()
assert len(entryData) == transition.staticSize, ( \
"%s has staticSize %d, "
"but actually wrote %d bytes" % (
repr(transition),
transition.staticSize,
len(entryData)))
entryIndex = entryIDs.get(entryData)
if entryIndex is None:
entryIndex = len(entries)
entryIDs[entryData] = entryIndex
entries.append(entryData)
stateArrayWriter.writeUShort(entryIndex)
stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
stateArrayData = stateArrayWriter.getAllData()
entryTableOffset = stateArrayOffset + len(stateArrayData)
writer.writeULong(glyphClassCount)
writer.writeULong(glyphClassTableOffset)
writer.writeULong(stateArrayOffset)
writer.writeULong(entryTableOffset)
writer.writeData(glyphClassData)
writer.writeData(stateArrayData)
for entry in entries:
writer.writeData(entry)
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.begintag(name, attrs)
xmlWriter.newline()
xmlWriter.comment("GlyphClassCount=%s" %value.GlyphClassCount)
xmlWriter.newline()
for g, klass in sorted(value.GlyphClasses.items()):
xmlWriter.simpletag("GlyphClass", glyph=g, value=klass)
xmlWriter.newline()
for stateIndex, state in enumerate(value.States):
xmlWriter.begintag("State", index=stateIndex)
xmlWriter.newline()
for glyphClass, trans in sorted(state.Transitions.items()):
trans.toXML(xmlWriter, font=font,
attrs={"onGlyphClass": glyphClass},
name="Transition")
xmlWriter.endtag("State")
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
def xmlRead(self, attrs, content, font):
table = AATStateTable()
for eltName, eltAttrs, eltContent in filter(istuple, content):
if eltName == "GlyphClass":
glyph = eltAttrs["glyph"]
value = eltAttrs["value"]
table.GlyphClasses[glyph] = safeEval(value)
elif eltName == "State":
state = self._xmlReadState(eltAttrs, eltContent, font)
table.States.append(state)
table.GlyphClassCount = max(table.GlyphClasses.values()) + 1
return table
def _xmlReadState(self, attrs, content, font):
state = AATState()
for eltName, eltAttrs, eltContent in filter(istuple, content):
if eltName == "Transition":
glyphClass = safeEval(eltAttrs["onGlyphClass"])
transition = self.tableClass()
transition.fromXML(eltName, eltAttrs,
eltContent, font)
state.Transitions[glyphClass] = transition
return state
class DeltaValue(BaseConverter):
def read(self, reader, font, tableDict):
@ -1048,6 +1172,7 @@ converterMapping = {
# "Template" types
"AATLookup": lambda C: partial(AATLookup, tableClass=C),
"AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C),
"STXHeader": lambda C: partial(STXHeader, tableClass=C),
"OffsetTo": lambda C: partial(Table, tableClass=C),
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
}

View File

@ -1387,7 +1387,7 @@ otData = [
]),
('RearrangementMorph', [
('struct', 'StateHeader', None, None, 'Header.'),
('STXHeader(AATRearrangement)', 'StateTable', None, None, 'Finite-state transducer table.'),
]),
('ContextualMorph', [

View File

@ -1,10 +1,11 @@
# coding: utf-8
"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various
OpenType subtables.
Most are constructed upon import from data in otData.py, all are populated with
converter objects from otConverters.py.
"""
from __future__ import print_function, division, absolute_import
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.textTools import safeEval
from .otBase import BaseTable, FormatSwitchingBaseTable
@ -15,6 +16,107 @@ import logging
log = logging.getLogger(__name__)
class AATStateTable(object):
def __init__(self):
self.GlyphClasses = {} # GlyphName --> GlyphClass
self.States = [] # List of AATState, indexed by state number
class AATState(object):
def __init__(self):
self.Transitions = {} # GlyphClass --> {AATRearrangement, ...}
class AATRearrangement(object):
staticSize = 4
_VERBS = {
0: "no change",
1: "Ax ⇒ xA",
2: "xD ⇒ Dx",
3: "AxD ⇒ DxA",
4: "ABx ⇒ xAB",
5: "ABx ⇒ xBA",
6: "xCD ⇒ CDx",
7: "xCD ⇒ DCx",
8: "AxCD ⇒ CDxA",
9: "AxCD ⇒ DCxA",
10: "ABxD ⇒ DxAB",
11: "ABxD ⇒ DxBA",
12: "ABxCD ⇒ CDxAB",
13: "ABxCD ⇒ CDxBA",
14: "ABxCD ⇒ DCxAB",
15: "ABxCD ⇒ DCxBA",
}
def __init__(self):
self.NewState = 0
self.Verb = 0
self.MarkFirst = False
self.DontAdvance = False
self.MarkLast = False
self.ReservedFlags = 0
def compile(self, writer, font):
writer.writeUShort(self.NewState)
assert self.Verb >= 0 and self.Verb <= 15, self.Verb
flags = self.Verb | self.ReservedFlags
if self.MarkFirst: flags |= 0x8000
if self.DontAdvance: flags |= 0x4000
if self.MarkLast: flags |= 0x2000
writer.writeUShort(flags)
def decompile(self, reader, font):
self.NewState = reader.readUShort()
flags = reader.readUShort()
self.Verb = flags & 0xF
self.MarkFirst = bool(flags & 0x8000)
self.DontAdvance = bool(flags & 0x4000)
self.MarkLast = bool(flags & 0x2000)
self.ReservedFlags = flags & 0x1FF0
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
xmlWriter.newline()
xmlWriter.simpletag("NewState", value=self.NewState)
xmlWriter.newline()
flags = [f for f in ("MarkFirst", "DontAdvance", "MarkLast")
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)
verbComment = self._VERBS.get(self.Verb)
if verbComment is not None:
xmlWriter.comment(verbComment)
xmlWriter.newline()
xmlWriter.endtag(name)
xmlWriter.newline()
def fromXML(self, name, attrs, content, font):
self.NewState = self.Verb = self.ReservedFlags = 0
self.MarkFirst = self.DontAdvance = self.MarkLast = False
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 == "Verb":
self.Verb = safeEval(eltAttrs["value"])
elif eltName == "ReservedFlags":
self.ReservedFlags = safeEval(eltAttrs["value"])
elif eltName == "Flags":
for flag in eltAttrs["value"].split(","):
self._setFlag(flag.strip())
def _setFlag(self, flag):
assert flag in {"MarkFirst", "DontAdvance", "MarkLast"}, \
"unsupported flag %s" % flag
self.__dict__[flag] = True
class FeatureParams(BaseTable):
def compile(self, writer, font):
@ -649,7 +751,6 @@ class LigatureSubst(FormatSwitchingBaseTable):
ligs.append(lig)
#
# For each subtable format there is a class. However, we don't really distinguish
# between "field name" and "format name": often these are the same. Yet there's
# a whole bunch of fields with different names. The following dict is a mapping
@ -995,7 +1096,7 @@ def _buildClasses():
4: NoncontextualMorph,
},
'morx': {
# 0: RearrangementMorph,
0: RearrangementMorph,
# 1: ContextualMorph,
# 2: LigatureMorph,
# 3: Reserved,

View File

@ -1,3 +1,4 @@
# coding: utf-8
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.testTools import FakeFont, getXML, parseXML
@ -85,6 +86,153 @@ MORX_NONCONTEXTUAL_XML = [
]
MORX_REARRANGEMENT_DATA = deHexStr(
'0002 0000 ' # 0: Version=2, Reserved=0
'0000 0001 ' # 4: MorphChainCount=1
'0000 0001 ' # 8: DefaultFlags=1
'0000 0078 ' # 12: StructLength=120
'0000 0000 ' # 16: MorphFeatureCount=0
'0000 0001 ' # 20: MorphSubtableCount=1
'0000 0068 ' # 24: Subtable[0].StructLength=104 (+24=128)
'80 ' # 28: Subtable[0].CoverageFlags=0x80
'00 00 ' # 29: Subtable[0].Reserved=0
'00 ' # 31: Subtable[0].MorphType=0/RearrangementMorph
'0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1
'0000 0006 ' # 36: STXHeader.ClassCount=6
'0000 0010 ' # 40: STXHeader.ClassTableOffset=16 (+36=52)
'0000 0028 ' # 44: STXHeader.StateArrayOffset=40 (+36=76)
'0000 004C ' # 48: STXHeader.EntryTableOffset=76 (+36=112)
'0006 0004 ' # 52: ClassTable.LookupFormat=6, .UnitSize=4
'0002 0008 ' # 56: .NUnits=2, .SearchRange=8
'0001 0000 ' # 60: .EntrySelector=1, .RangeShift=0
'0001 0005 ' # 64: Glyph=A; Class=5
'0003 0004 ' # 68: Glyph=C; Class=4
'FFFF 0000 ' # 72: Glyph=<end>; Value=0
'0000 0001 0002 0003 0002 0001 ' # 76: State[0][0..5]
'0003 0003 0003 0003 0003 0003 ' # 88: State[1][0..5]
'0001 0003 0003 0003 0002 0002 ' # 100: State[2][0..5]
'0002 FFFF ' # 112: Entries[0].NewState=2, .Flags=0xFFFF
'0001 A00D ' # 116: Entries[1].NewState=1, .Flags=0xA00D
'0000 8006 ' # 120: Entries[2].NewState=0, .Flags=0x8006
'0002 0000 ' # 124: Entries[3].NewState=2, .Flags=0x0000
) # 128: <end>
assert len(MORX_REARRANGEMENT_DATA) == 128, len(MORX_REARRANGEMENT_DATA)
MORX_REARRANGEMENT_XML = [
'<Version value="2"/>',
'<Reserved value="0"/>',
'<!-- MorphChainCount=1 -->',
'<MorphChain index="0">',
' <DefaultFlags value="0x00000001"/>',
' <!-- StructLength=120 -->',
' <!-- MorphFeatureCount=0 -->',
' <!-- MorphSubtableCount=1 -->',
' <MorphSubtable index="0">',
' <!-- StructLength=104 -->',
' <CoverageFlags value="128"/>',
' <Reserved value="0"/>',
' <!-- MorphType=0 -->',
' <SubFeatureFlags value="0x00000001"/>',
' <RearrangementMorph>',
' <StateTable>',
' <!-- GlyphClassCount=6 -->',
' <GlyphClass glyph="A" value="5"/>',
' <GlyphClass glyph="C" value="4"/>',
' <State index="0">',
' <Transition onGlyphClass="0">',
' <NewState value="2"/>',
' <Flags value="MarkFirst,DontAdvance,MarkLast"/>',
' <ReservedFlags value="0x1FF0"/>',
' <Verb value="15"/><!-- ABxCD ⇒ DCxBA -->',
' </Transition>',
' <Transition onGlyphClass="1">',
' <NewState value="1"/>',
' <Flags value="MarkFirst,MarkLast"/>',
' <Verb value="13"/><!-- ABxCD ⇒ CDxBA -->',
' </Transition>',
' <Transition onGlyphClass="2">',
' <NewState value="0"/>',
' <Flags value="MarkFirst"/>',
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
' </Transition>',
' <Transition onGlyphClass="3">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="4">',
' <NewState value="0"/>',
' <Flags value="MarkFirst"/>',
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
' </Transition>',
' <Transition onGlyphClass="5">',
' <NewState value="1"/>',
' <Flags value="MarkFirst,MarkLast"/>',
' <Verb value="13"/><!-- ABxCD ⇒ CDxBA -->',
' </Transition>',
' </State>',
' <State index="1">',
' <Transition onGlyphClass="0">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="1">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="2">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="3">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="4">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="5">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' </State>',
' <State index="2">',
' <Transition onGlyphClass="0">',
' <NewState value="1"/>',
' <Flags value="MarkFirst,MarkLast"/>',
' <Verb value="13"/><!-- ABxCD ⇒ CDxBA -->',
' </Transition>',
' <Transition onGlyphClass="1">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="2">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="3">',
' <NewState value="2"/>',
' <Verb value="0"/><!-- no change -->',
' </Transition>',
' <Transition onGlyphClass="4">',
' <NewState value="0"/>',
' <Flags value="MarkFirst"/>',
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
' </Transition>',
' <Transition onGlyphClass="5">',
' <NewState value="0"/>',
' <Flags value="MarkFirst"/>',
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
' </Transition>',
' </State>',
' </StateTable>',
' </RearrangementMorph>',
' </MorphSubtable>',
'</MorphChain>',
]
class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase):
@classmethod
@ -108,7 +256,26 @@ class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase):
hexStr(MORX_NONCONTEXTUAL_DATA))
class MORXRearrangementTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.maxDiff = None
cls.font = FakeFont(['.nodef', 'A', 'B', 'C'])
def test_decompile_toXML(self):
table = newTable('morx')
table.decompile(MORX_REARRANGEMENT_DATA, self.font)
self.assertEqual(getXML(table.toXML), MORX_REARRANGEMENT_XML)
def test_compile_fromXML(self):
table = newTable('morx')
for name, attrs, content in parseXML(MORX_REARRANGEMENT_XML):
table.fromXML(name, attrs, content, font=self.font)
self.assertEqual(hexStr(table.compile(self.font)),
hexStr(MORX_REARRANGEMENT_DATA))
if __name__ == '__main__':
import sys
sys.exit(unittest.main())

View File

@ -1,7 +1,10 @@
from __future__ import print_function, division, absolute_import
# coding: utf-8
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
from fontTools.misc.testTools import parseXML, FakeFont
from fontTools.misc.testTools import getXML, parseXML, FakeFont
from fontTools.misc.textTools import deHexStr, hexStr
from fontTools.misc.xmlWriter import XMLWriter
from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter
import fontTools.ttLib.tables.otTables as otTables
import unittest
@ -367,6 +370,33 @@ class AlternateSubstTest(unittest.TestCase):
})
class AATRearrangementTest(unittest.TestCase):
def setUp(self):
self.font = FakeFont(['.notdef', 'A', 'B', 'C'])
def testCompile(self):
r = otTables.AATRearrangement()
r.NewState = 0x1234
r.MarkFirst = r.DontAdvance = r.MarkLast = True
r.ReservedFlags, r.Verb = 0x1FF0, 0xD
writer = OTTableWriter()
r.compile(writer, self.font)
self.assertEqual(hexStr(writer.getAllData()), "1234fffd")
def testDecompileToXML(self):
r = otTables.AATRearrangement()
r.decompile(OTTableReader(deHexStr("1234fffd")), self.font)
toXML = lambda w, f: r.toXML(w, f, {"Test": "Foo"}, "Transition")
self.assertEqual(getXML(toXML, self.font), [
'<Transition Test="Foo">',
' <NewState value="4660"/>', # 0x1234 = 4660
' <Flags value="MarkFirst,DontAdvance,MarkLast"/>',
' <ReservedFlags value="0x1FF0"/>',
' <Verb value="13"/><!-- ABxCD ⇒ CDxBA -->',
'</Transition>',
])
if __name__ == "__main__":
import sys
sys.exit(unittest.main())