Repository: fonttools/fonttools

Issue:
XML format does not retain LookupType for lookups with no subtables
Eg, we currently can only encode:
```
<Lookup index="1" empty="1"/>
```
the LookupType is implied by the type of the subtables.  If there's no subtable, we don't record lookup type and roundtripping changes it (probably to zero).


Discussion:
The problem is not limited to XML.  I made LookupType a computed member in otData machinery.  Which means its value is ignored and written by subtables.  If there are no subtables, we write 0.  Need to reconciliate that.
Maybe we should NOT ignore LookupType attr and just make sure it matches the subtables.

Resolving patch:
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -36,26 +36,10 @@ class BaseTTXConverter(DefaultTable):
 
 	def decompile(self, data, font):
 		from . import otTables
-		cachingStats = None if True else {}
-		class GlobalState(object):
-			def __init__(self, tableType, cachingStats):
-				self.tableType = tableType
-				self.cachingStats = cachingStats
-		globalState = GlobalState(tableType=self.tableTag,
-					cachingStats=cachingStats)
-		reader = OTTableReader(data, globalState)
+		reader = OTTableReader(data, tableTag=self.tableTag)
 		tableClass = getattr(otTables, self.tableTag)
 		self.table = tableClass()
 		self.table.decompile(reader, font)
-		if cachingStats:
-			stats = sorted([(v, k) for k, v in cachingStats.items()])
-			stats.reverse()
-			log.debug("cachingStats for %s", self.tableTag)
-			for v, k in stats:
-				if v < 2:
-					break
-				log.debug("%s %s", v, k)
-			log.debug("--- %s", len(stats))
 
 	def compile(self, font):
 		""" Create a top-level OTFWriter for the GPOS/GSUB table.
@@ -78,15 +62,11 @@ def compile(self, font):
 
 				If a lookup subtable overflows an offset, we have to start all over.
 		"""
-		class GlobalState(object):
-			def __init__(self, tableType):
-				self.tableType = tableType
-		globalState = GlobalState(tableType=self.tableTag)
 		overflowRecord = None
 
 		while True:
 			try:
-				writer = OTTableWriter(globalState)
+				writer = OTTableWriter(tableTag=self.tableTag)
 				self.table.compile(writer, font)
 				return writer.getAllData()
 
@@ -124,14 +104,14 @@ class OTTableReader(object):
 
 	"""Helper class to retrieve data from an OpenType table."""
 
-	__slots__ = ('data', 'offset', 'pos', 'globalState', 'localState')
+	__slots__ = ('data', 'offset', 'pos', 'localState', 'tableTag')
 
-	def __init__(self, data, globalState={}, localState=None, offset=0):
+	def __init__(self, data, localState=None, offset=0, tableTag=None):
 		self.data = data
 		self.offset = offset
 		self.pos = offset
-		self.globalState = globalState
 		self.localState = localState
+		self.tableTag = tableTag
 
 	def advance(self, count):
 		self.pos += count
@@ -140,13 +120,13 @@ def seek(self, pos):
 		self.pos = pos
 
 	def copy(self):
-		other = self.__class__(self.data, self.globalState, self.localState, self.offset)
+		other = self.__class__(self.data, self.localState, self.offset, self.tableTag)
 		other.pos = self.pos
 		return other
 
 	def getSubReader(self, offset):
 		offset = self.offset + offset
-		return self.__class__(self.data, self.globalState, self.localState, offset)
+		return self.__class__(self.data, self.localState, offset, self.tableTag)
 
 	def readUShort(self):
 		pos = self.pos
@@ -237,11 +217,11 @@ class OTTableWriter(object):
 
 	"""Helper class to gather and assemble data for OpenType tables."""
 
-	def __init__(self, globalState, localState=None):
+	def __init__(self, localState=None, tableTag=None):
 		self.items = []
 		self.pos = None
-		self.globalState = globalState
 		self.localState = localState
+		self.tableTag = tableTag
 		self.longOffset = False
 		self.parent = None
 
@@ -424,7 +404,7 @@ def getAllData(self):
 	# interface for gathering data, as used by table.compile()
 
 	def getSubWriter(self):
-		subwriter = self.__class__(self.globalState, self.localState)
+		subwriter = self.__class__(self.localState, self.tableTag)
 		subwriter.parent = self # because some subtables have idential values, we discard
 					# the duplicates under the getAllData method. Hence some
 					# subtable writers can have more than one parent writer.
@@ -509,7 +489,7 @@ def getOverflowErrorRecord(self, item):
 						LookupListIndex = p1.parent.repeatIndex
 						SubTableIndex = p1.repeatIndex
 
-		return OverflowErrorRecord( (self.globalState.tableType, LookupListIndex, SubTableIndex, itemName, itemIndex) )
+		return OverflowErrorRecord( (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex) )
 
 
 class CountReference(object):
@@ -594,10 +574,10 @@ def decompile(self, reader, font):
 		converters = self.getConverters()
 		for conv in converters:
 			if conv.name == "SubTable":
-				conv = conv.getConverter(reader.globalState.tableType,
+				conv = conv.getConverter(reader.tableTag,
 						table["LookupType"])
 			if conv.name == "ExtSubTable":
-				conv = conv.getConverter(reader.globalState.tableType,
+				conv = conv.getConverter(reader.tableTag,
 						table["ExtensionLookupType"])
 			if conv.name == "FeatureParams":
 				conv = conv.getConverter(reader["FeatureTag"])
@@ -666,8 +646,10 @@ def compile(self, writer, font):
 				if conv.isPropagated:
 					writer[conv.name] = ref
 			elif conv.isLookupType:
+				# We make sure that subtables have the same lookup type,
+				# and that the type is the same as the one set on the
+				# Lookup object, if any is set.
 				ref = writer.writeCountReference(table, conv.name, conv.staticSize, table.get(conv.name))
-				table[conv.name] = None
 				writer['LookupType'] = ref
 			else:
 				if conv.aux and not eval(conv.aux, None, table):
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -23,7 +23,7 @@ def buildConverters(tableSpec, tableNamespace):
 		if name.startswith("ValueFormat"):
 			assert tp == "uint16"
 			converterClass = ValueFormat
-		elif name.endswith("Count") or name.endswith("LookupType"):
+		elif name.endswith("Count"):
 			assert tp in ("uint16", "uint32")
 			converterClass = ComputedUShort if tp == 'uint16' else ComputedULong
 		elif name == "SubTable":


Test patch:
diff --git a/Lib/fontTools/otlLib/builder_test.py b/Lib/fontTools/otlLib/builder_test.py
--- a/Lib/fontTools/otlLib/builder_test.py
+++ b/Lib/fontTools/otlLib/builder_test.py
@@ -449,7 +449,7 @@ def test_buildLookup(self):
         lookup = builder.buildLookup([s1, s2], flags=7)
         self.assertEqual(getXML(lookup.toXML),
                          ['<Lookup>',
-                          '  <!-- LookupType=1 -->',
+                          '  <LookupType value="1"/>',
                           '  <LookupFlag value="7"/>',
                           '  <!-- SubTableCount=2 -->',
                           '  <SingleSubst index="0">',
@@ -495,7 +495,7 @@ def test_buildLookup_markFilterSet(self):
         lookup = builder.buildLookup([s], flags, markFilterSet=999)
         self.assertEqual(getXML(lookup.toXML),
                          ['<Lookup>',
-                          '  <!-- LookupType=1 -->',
+                          '  <LookupType value="1"/>',
                           '  <LookupFlag value="17"/>',
                           '  <!-- SubTableCount=1 -->',
                           '  <SingleSubst index="0">',
diff --git a/Lib/fontTools/ttLib/tables/otBase_test.py b/Lib/fontTools/ttLib/tables/otBase_test.py
--- a/Lib/fontTools/ttLib/tables/otBase_test.py
+++ b/Lib/fontTools/ttLib/tables/otBase_test.py
@@ -61,32 +61,32 @@ def test_getSubReader(self):
 
 class OTTableWriterTest(unittest.TestCase):
     def test_writeShort(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         writer.writeShort(-12345)
         self.assertEqual(writer.getData(), deHexStr("CF C7"))
 
     def test_writeLong(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         writer.writeLong(-12345678)
         self.assertEqual(writer.getData(), deHexStr("FF 43 9E B2"))
 
     def test_writeUInt8(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         writer.writeUInt8(0xBE)
         self.assertEqual(writer.getData(), deHexStr("BE"))
 
     def test_writeUShort(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         writer.writeUShort(0xBEEF)
         self.assertEqual(writer.getData(), deHexStr("BE EF"))
 
     def test_writeUInt24(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         writer.writeUInt24(0xBEEF77)
         self.assertEqual(writer.getData(), deHexStr("BE EF 77"))
 
     def test_writeULong(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         writer.writeULong(0xBEEFCAFE)
         self.assertEqual(writer.getData(), deHexStr("BE EF CA FE"))
 
diff --git a/Lib/fontTools/ttLib/tables/otConverters_test.py b/Lib/fontTools/ttLib/tables/otConverters_test.py
--- a/Lib/fontTools/ttLib/tables/otConverters_test.py
+++ b/Lib/fontTools/ttLib/tables/otConverters_test.py
@@ -25,7 +25,7 @@ def test_read(self):
         self.assertEqual(reader.pos, 2)
 
     def test_write(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         self.converter.write(writer, self.font, {}, "B")
         self.assertEqual(writer.getData(), deHexStr("0002"))
 
@@ -44,7 +44,7 @@ def test_read(self):
         self.assertEqual(self.converter.read(reader, font, {}), 0x123)
 
     def test_write(self):
-        writer = OTTableWriter(globalState={})
+        writer = OTTableWriter()
         self.converter.write(writer, self.makeFont(), {}, 0x123)
         self.assertEqual(writer.getData(), deHexStr("0123"))