Repository: fonttools/fonttools

Issue:
[ttLib] clean up XML for cmap format 14
As a follow up to #1434:
* default values should no use “None” for glyph names
* the `cmap_format_14` element shouldn’t have a `lenght` attribute.


Discussion:
Neither should it have `format` and `numVarSelectorRecords` attributes.

Resolving patch:
diff --git a/Lib/fontTools/ttLib/tables/_c_m_a_p.py b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
--- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py
+++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
@@ -1135,7 +1135,7 @@ def decompile(self, data, ttFont):
 					startOffset += 5
 					uv = cvtToUVS(uv)
 					glyphName = self.ttFont.getGlyphName(gid)
-					localUVList.append( [uv, glyphName] )
+					localUVList.append((uv, glyphName))
 				try:
 					uvsDict[varUVS].extend(localUVList)
 				except KeyError:
@@ -1147,9 +1147,6 @@ def toXML(self, writer, ttFont):
 		writer.begintag(self.__class__.__name__, [
 				("platformID", self.platformID),
 				("platEncID", self.platEncID),
-				("format", self.format),
-				("length", self.length),
-				("numVarSelectorRecords", self.numVarSelectorRecords),
 				])
 		writer.newline()
 		uvsDict = self.uvsDict
@@ -1158,18 +1155,15 @@ def toXML(self, writer, ttFont):
 			uvList = uvsDict[uvs]
 			uvList.sort(key=lambda item: (item[1] is not None, item[0], item[1]))
 			for uv, gname in uvList:
-				if gname is None:
-					gname = "None"
-				# I use the arg rather than th keyword syntax in order to preserve the attribute order.
-				writer.simpletag("map", [ ("uvs",hex(uvs)), ("uv",hex(uv)), ("name", gname)]  )
+				attrs = [("uv", hex(uv)), ("uvs", hex(uvs))]
+				if gname is not None:
+					attrs.append(("name", gname))
+				writer.simpletag("map", attrs)
 				writer.newline()
 		writer.endtag(self.__class__.__name__)
 		writer.newline()
 
 	def fromXML(self, name, attrs, content, ttFont):
-		self.format = safeEval(attrs["format"])
-		self.length = safeEval(attrs["length"])
-		self.numVarSelectorRecords = safeEval(attrs["numVarSelectorRecords"])
 		self.language = 0xFF # provide a value so that  CmapSubtable.__lt__() won't fail
 		if not hasattr(self, "cmap"):
 			self.cmap = {} # so that clients that expect this to exist in a cmap table won't fail.
@@ -1177,6 +1171,11 @@ def fromXML(self, name, attrs, content, ttFont):
 			self.uvsDict = {}
 			uvsDict = self.uvsDict
 
+		# For backwards compatibility reasons we accept "None" as an indicator
+		# for "default mapping", unless the font actually has a glyph named
+		# "None".
+		_hasGlyphNamedNone = None
+
 		for element in content:
 			if not isinstance(element, tuple):
 				continue
@@ -1185,13 +1184,16 @@ def fromXML(self, name, attrs, content, ttFont):
 				continue
 			uvs = safeEval(attrs["uvs"])
 			uv = safeEval(attrs["uv"])
-			gname = attrs["name"]
+			gname = attrs.get("name")
 			if gname == "None":
-				gname = None
+				if _hasGlyphNamedNone is None:
+					_hasGlyphNamedNone = "None" in ttFont.getGlyphOrder()
+				if not _hasGlyphNamedNone:
+					gname = None
 			try:
-				uvsDict[uvs].append( [uv, gname])
+				uvsDict[uvs].append((uv, gname))
 			except KeyError:
-				uvsDict[uvs] = [ [uv, gname] ]
+				uvsDict[uvs] = [(uv, gname)]
 
 	def compile(self, ttFont):
 		if self.data:


Test patch:
diff --git a/Tests/fontBuilder/data/test_uvs.ttf.ttx b/Tests/fontBuilder/data/test_uvs.ttf.ttx
--- a/Tests/fontBuilder/data/test_uvs.ttf.ttx
+++ b/Tests/fontBuilder/data/test_uvs.ttf.ttx
@@ -7,9 +7,9 @@
       <map code="0x20" name="space"/><!-- SPACE -->
       <map code="0x30" name="zero"/><!-- DIGIT ZERO -->
     </cmap_format_4>
-    <cmap_format_14 platformID="0" platEncID="5" format="14" length="49" numVarSelectorRecords="2">
-      <map uvs="0xfe00" uv="0x30" name="zero.slash"/>
-      <map uvs="0xfe01" uv="0x30" name="None"/>
+    <cmap_format_14 platformID="0" platEncID="5">
+      <map uv="0x30" uvs="0xfe00" name="zero.slash"/>
+      <map uv="0x30" uvs="0xfe01"/>
     </cmap_format_14>
     <cmap_format_4 platformID="3" platEncID="1" language="0">
       <map code="0x20" name="space"/><!-- SPACE -->
diff --git a/Tests/ttLib/tables/_c_m_a_p_test.py b/Tests/ttLib/tables/_c_m_a_p_test.py
--- a/Tests/ttLib/tables/_c_m_a_p_test.py
+++ b/Tests/ttLib/tables/_c_m_a_p_test.py
@@ -11,6 +11,7 @@
 CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
 DATA_DIR = os.path.join(CURR_DIR, 'data')
 CMAP_FORMAT_14_TTX = os.path.join(DATA_DIR, "_c_m_a_p_format_14.ttx")
+CMAP_FORMAT_14_BW_COMPAT_TTX = os.path.join(DATA_DIR, "_c_m_a_p_format_14_bw_compat.ttx")
 
 def strip_VariableItems(string):
     # ttlib changes with the fontTools version
@@ -99,8 +100,8 @@ def test_format_14(self):
 		subtable = self.makeSubtable(14, 0, 5, 0)
 		subtable.cmap = {}  # dummy
 		subtable.uvsDict = {
-			0xFE00: [[0x0030, "zero.slash"]],
-			0xFE01: [(0x0030, None)],  # yes, tuple here, list above, to match decompile
+			0xFE00: [(0x0030, "zero.slash")],
+			0xFE01: [(0x0030, None)],
 		}
 		fb = FontBuilder(1024, isTTF=True)
 		font = fb.font
@@ -122,6 +123,9 @@ def test_format_14(self):
 		with open(CMAP_FORMAT_14_TTX) as f:
 			expected = strip_VariableItems(f.read())
 		self.assertEqual(ttx, expected)
+		with open(CMAP_FORMAT_14_BW_COMPAT_TTX) as f:
+			font.importXML(f)
+		self.assertEqual(font["cmap"].getcmap(0, 5).uvsDict, subtable.uvsDict)
 
 
 if __name__ == "__main__":
diff --git a/Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx b/Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx
--- a/Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx
+++ b/Tests/ttLib/tables/data/_c_m_a_p_format_14.ttx
@@ -3,9 +3,9 @@
 
   <cmap>
     <tableVersion version="0"/>
-    <cmap_format_14 platformID="0" platEncID="5" format="14" length="49" numVarSelectorRecords="2">
-      <map uvs="0xfe00" uv="0x30" name="zero.slash"/>
-      <map uvs="0xfe01" uv="0x30" name="None"/>
+    <cmap_format_14 platformID="0" platEncID="5">
+      <map uv="0x30" uvs="0xfe00" name="zero.slash"/>
+      <map uv="0x30" uvs="0xfe01"/>
     </cmap_format_14>
   </cmap>
 
diff --git a/Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx b/Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx
new file mode 100644
--- /dev/null
+++ b/Tests/ttLib/tables/data/_c_m_a_p_format_14_bw_compat.ttx
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.35">
+
+  <cmap>
+    <tableVersion version="0"/>
+    <cmap_format_14 platformID="0" platEncID="5">
+      <map uvs="0xfe00" uv="0x30" name="zero.slash"/>
+      <map uvs="0xfe01" uv="0x30" name="None"/><!-- testing whether the old format that uses name="None" is still accepted -->
+    </cmap_format_14>
+  </cmap>
+
+</ttFont>
diff --git a/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap
--- a/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap
+++ b/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap
@@ -3,14 +3,14 @@
 
   <cmap>
     <tableVersion version="0"/>
-    <cmap_format_14 platformID="0" platEncID="5" format="14" length="47" numVarSelectorRecords="1">
-      <map uvs="0xe0100" uv="0x4e00" name="None"/>
-      <map uvs="0xe0100" uv="0x4e03" name="None"/>
-      <map uvs="0xe0100" uv="0x4e04" name="None"/>
-      <map uvs="0xe0100" uv="0x4e05" name="None"/>
-      <map uvs="0xe0100" uv="0x4e06" name="None"/>
-      <map uvs="0xe0100" uv="0x4e10" name="g25"/>
-      <map uvs="0xe0100" uv="0x4e11" name="g26"/>
+    <cmap_format_14 platformID="0" platEncID="5">
+      <map uv="0x4e00" uvs="0xe0100"/>
+      <map uv="0x4e03" uvs="0xe0100"/>
+      <map uv="0x4e04" uvs="0xe0100"/>
+      <map uv="0x4e05" uvs="0xe0100"/>
+      <map uv="0x4e06" uvs="0xe0100"/>
+      <map uv="0x4e10" uvs="0xe0100" name="g25"/>
+      <map uv="0x4e11" uvs="0xe0100" name="g26"/>
     </cmap_format_14>
     <cmap_format_4 platformID="3" platEncID="1" language="0">
       <map code="0x4e00" name="g10"/><!-- CJK UNIFIED IDEOGRAPH-4E00 -->