1# -*- coding: utf-8 -*- 2from __future__ import print_function, division, absolute_import, unicode_literals 3from fontTools.misc.py23 import * 4from fontTools.misc import sstruct 5from fontTools.misc.loggingTools import CapturingLogHandler 6from fontTools.misc.testTools import FakeFont 7from fontTools.misc.xmlWriter import XMLWriter 8import struct 9import unittest 10from fontTools.ttLib import newTable 11from fontTools.ttLib.tables._n_a_m_e import ( 12 table__n_a_m_e, NameRecord, nameRecordFormat, nameRecordSize, makeName, log) 13 14 15def names(nameTable): 16 result = [(n.nameID, n.platformID, n.platEncID, n.langID, n.string) 17 for n in nameTable.names] 18 result.sort() 19 return result 20 21 22class NameTableTest(unittest.TestCase): 23 24 def test_getDebugName(self): 25 table = table__n_a_m_e() 26 table.names = [ 27 makeName("Bold", 258, 1, 0, 0), # Mac, MacRoman, English 28 makeName("Gras", 258, 1, 0, 1), # Mac, MacRoman, French 29 makeName("Fett", 258, 1, 0, 2), # Mac, MacRoman, German 30 makeName("Sem Fracções", 292, 1, 0, 8) # Mac, MacRoman, Portuguese 31 ] 32 self.assertEqual("Bold", table.getDebugName(258)) 33 self.assertEqual("Sem Fracções", table.getDebugName(292)) 34 self.assertEqual(None, table.getDebugName(999)) 35 36 def test_setName(self): 37 table = table__n_a_m_e() 38 table.setName("Regular", 2, 1, 0, 0) 39 table.setName("Version 1.000", 5, 3, 1, 0x409) 40 table.setName("寬鬆", 276, 1, 2, 0x13) 41 self.assertEqual("Regular", table.getName(2, 1, 0, 0).toUnicode()) 42 self.assertEqual("Version 1.000", table.getName(5, 3, 1, 0x409).toUnicode()) 43 self.assertEqual("寬鬆", table.getName(276, 1, 2, 0x13).toUnicode()) 44 self.assertTrue(len(table.names) == 3) 45 table.setName("緊縮", 276, 1, 2, 0x13) 46 self.assertEqual("緊縮", table.getName(276, 1, 2, 0x13).toUnicode()) 47 self.assertTrue(len(table.names) == 3) 48 # passing bytes issues a warning 49 with CapturingLogHandler(log, "WARNING") as captor: 50 table.setName(b"abc", 0, 1, 0, 0) 51 self.assertTrue( 52 len([r for r in captor.records if "string is bytes" in r.msg]) == 1) 53 # anything other than unicode or bytes raises an error 54 with self.assertRaises(TypeError): 55 table.setName(1.000, 5, 1, 0, 0) 56 57 def test_addName(self): 58 table = table__n_a_m_e() 59 nameIDs = [] 60 for string in ("Width", "Weight", "Custom"): 61 nameIDs.append(table.addName(string)) 62 63 self.assertEqual(nameIDs[0], 256) 64 self.assertEqual(nameIDs[1], 257) 65 self.assertEqual(nameIDs[2], 258) 66 self.assertEqual(len(table.names), 6) 67 self.assertEqual(table.names[0].string, "Width") 68 self.assertEqual(table.names[1].string, "Width") 69 self.assertEqual(table.names[2].string, "Weight") 70 self.assertEqual(table.names[3].string, "Weight") 71 self.assertEqual(table.names[4].string, "Custom") 72 self.assertEqual(table.names[5].string, "Custom") 73 74 with self.assertRaises(ValueError): 75 table.addName('Invalid nameID', minNameID=32767) 76 with self.assertRaises(TypeError): 77 table.addName(b"abc") # must be unicode string 78 79 def test_addMultilingualName(self): 80 # Microsoft Windows has language codes for “English” (en) 81 # and for “Standard German as used in Switzerland” (de-CH). 82 # In this case, we expect that the implementation just 83 # encodes the name for the Windows platform; Apple platforms 84 # have been able to decode Windows names since the early days 85 # of OSX (~2001). However, Windows has no language code for 86 # “Swiss German as used in Liechtenstein” (gsw-LI), so we 87 # expect that the implementation populates the 'ltag' table 88 # to represent that particular, rather exotic BCP47 code. 89 font = FakeFont(glyphs=[".notdef", "A"]) 90 nameTable = font.tables['name'] = newTable("name") 91 with CapturingLogHandler(log, "WARNING") as captor: 92 widthID = nameTable.addMultilingualName({ 93 "en": "Width", 94 "de-CH": "Breite", 95 "gsw-LI": "Bräiti", 96 }, ttFont=font, mac=False) 97 self.assertEqual(widthID, 256) 98 xHeightID = nameTable.addMultilingualName({ 99 "en": "X-Height", 100 "gsw-LI": "X-Hööchi" 101 }, ttFont=font, mac=False) 102 self.assertEqual(xHeightID, 257) 103 captor.assertRegex("cannot add Windows name in language gsw-LI") 104 self.assertEqual(names(nameTable), [ 105 (256, 0, 4, 0, "Bräiti"), 106 (256, 3, 1, 0x0409, "Width"), 107 (256, 3, 1, 0x0807, "Breite"), 108 (257, 0, 4, 0, "X-Hööchi"), 109 (257, 3, 1, 0x0409, "X-Height"), 110 ]) 111 self.assertEqual(set(font.tables.keys()), {"ltag", "name"}) 112 self.assertEqual(font["ltag"].tags, ["gsw-LI"]) 113 114 def test_addMultilingualName_legacyMacEncoding(self): 115 # Windows has no language code for Latin; MacOS has a code; 116 # and we actually can convert the name to the legacy MacRoman 117 # encoding. In this case, we expect that the name gets encoded 118 # as Macintosh name (platformID 1) with the corresponding Mac 119 # language code (133); the 'ltag' table should not be used. 120 font = FakeFont(glyphs=[".notdef", "A"]) 121 nameTable = font.tables['name'] = newTable("name") 122 with CapturingLogHandler(log, "WARNING") as captor: 123 nameTable.addMultilingualName({"la": "SPQR"}, 124 ttFont=font) 125 captor.assertRegex("cannot add Windows name in language la") 126 self.assertEqual(names(nameTable), [(256, 1, 0, 131, "SPQR")]) 127 self.assertNotIn("ltag", font.tables.keys()) 128 129 def test_addMultilingualName_legacyMacEncodingButUnencodableName(self): 130 # Windows has no language code for Latin; MacOS has a code; 131 # but we cannot encode the name into this encoding because 132 # it contains characters that are not representable. 133 # In this case, we expect that the name gets encoded as 134 # Unicode name (platformID 0) with the language tag being 135 # added to the 'ltag' table. 136 font = FakeFont(glyphs=[".notdef", "A"]) 137 nameTable = font.tables['name'] = newTable("name") 138 with CapturingLogHandler(log, "WARNING") as captor: 139 nameTable.addMultilingualName({"la": "ⱾƤℚⱤ"}, 140 ttFont=font) 141 captor.assertRegex("cannot add Windows name in language la") 142 self.assertEqual(names(nameTable), [(256, 0, 4, 0, "ⱾƤℚⱤ")]) 143 self.assertIn("ltag", font.tables) 144 self.assertEqual(font["ltag"].tags, ["la"]) 145 146 def test_addMultilingualName_legacyMacEncodingButNoCodec(self): 147 # Windows has no language code for “Azeri written in the 148 # Arabic script” (az-Arab); MacOS would have a code (50); 149 # but we cannot encode the name into the legacy encoding 150 # because we have no codec for MacArabic in fonttools. 151 # In this case, we expect that the name gets encoded as 152 # Unicode name (platformID 0) with the language tag being 153 # added to the 'ltag' table. 154 font = FakeFont(glyphs=[".notdef", "A"]) 155 nameTable = font.tables['name'] = newTable("name") 156 with CapturingLogHandler(log, "WARNING") as captor: 157 nameTable.addMultilingualName({"az-Arab": "آذربايجان ديلی"}, 158 ttFont=font) 159 captor.assertRegex("cannot add Windows name in language az-Arab") 160 self.assertEqual(names(nameTable), [(256, 0, 4, 0, "آذربايجان ديلی")]) 161 self.assertIn("ltag", font.tables) 162 self.assertEqual(font["ltag"].tags, ["az-Arab"]) 163 164 def test_addMultilingualName_noTTFont(self): 165 # If the ttFont argument is not passed, the implementation 166 # should add whatever names it can, but it should not crash 167 # just because it cannot build an ltag table. 168 nameTable = newTable("name") 169 with CapturingLogHandler(log, "WARNING") as captor: 170 nameTable.addMultilingualName({"en": "A", "la": "ⱾƤℚⱤ"}) 171 captor.assertRegex("cannot store language la into 'ltag' table") 172 173 def test_decompile_badOffset(self): 174 # https://github.com/fonttools/fonttools/issues/525 175 table = table__n_a_m_e() 176 badRecord = { 177 "platformID": 1, 178 "platEncID": 3, 179 "langID": 7, 180 "nameID": 1, 181 "length": 3, 182 "offset": 8765 # out of range 183 } 184 data = bytesjoin([ 185 struct.pack(tostr(">HHH"), 1, 1, 6 + nameRecordSize), 186 sstruct.pack(nameRecordFormat, badRecord)]) 187 table.decompile(data, ttFont=None) 188 self.assertEqual(table.names, []) 189 190 191class NameRecordTest(unittest.TestCase): 192 193 def test_toUnicode_utf16be(self): 194 name = makeName("Foo Bold", 111, 0, 2, 7) 195 self.assertEqual("utf_16_be", name.getEncoding()) 196 self.assertEqual("Foo Bold", name.toUnicode()) 197 198 def test_toUnicode_macroman(self): 199 name = makeName("Foo Italic", 222, 1, 0, 7) # MacRoman 200 self.assertEqual("mac_roman", name.getEncoding()) 201 self.assertEqual("Foo Italic", name.toUnicode()) 202 203 def test_toUnicode_macromanian(self): 204 name = makeName(b"Foo Italic\xfb", 222, 1, 0, 37) # Mac Romanian 205 self.assertEqual("mac_romanian", name.getEncoding()) 206 self.assertEqual("Foo Italic"+unichr(0x02DA), name.toUnicode()) 207 208 def test_toUnicode_UnicodeDecodeError(self): 209 name = makeName(b"\1", 111, 0, 2, 7) 210 self.assertEqual("utf_16_be", name.getEncoding()) 211 self.assertRaises(UnicodeDecodeError, name.toUnicode) 212 213 def toXML(self, name): 214 writer = XMLWriter(BytesIO()) 215 name.toXML(writer, ttFont=None) 216 xml = writer.file.getvalue().decode("utf_8").strip() 217 return xml.split(writer.newlinestr.decode("utf_8"))[1:] 218 219 def test_toXML_utf16be(self): 220 name = makeName("Foo Bold", 111, 0, 2, 7) 221 self.assertEqual([ 222 '<namerecord nameID="111" platformID="0" platEncID="2" langID="0x7">', 223 ' Foo Bold', 224 '</namerecord>' 225 ], self.toXML(name)) 226 227 def test_toXML_utf16be_odd_length1(self): 228 name = makeName(b"\0F\0o\0o\0", 111, 0, 2, 7) 229 self.assertEqual([ 230 '<namerecord nameID="111" platformID="0" platEncID="2" langID="0x7">', 231 ' Foo', 232 '</namerecord>' 233 ], self.toXML(name)) 234 235 def test_toXML_utf16be_odd_length2(self): 236 name = makeName(b"\0Fooz", 111, 0, 2, 7) 237 self.assertEqual([ 238 '<namerecord nameID="111" platformID="0" platEncID="2" langID="0x7">', 239 ' Fooz', 240 '</namerecord>' 241 ], self.toXML(name)) 242 243 def test_toXML_utf16be_double_encoded(self): 244 name = makeName(b"\0\0\0F\0\0\0o", 111, 0, 2, 7) 245 self.assertEqual([ 246 '<namerecord nameID="111" platformID="0" platEncID="2" langID="0x7">', 247 ' Fo', 248 '</namerecord>' 249 ], self.toXML(name)) 250 251 def test_toXML_macroman(self): 252 name = makeName("Foo Italic", 222, 1, 0, 7) # MacRoman 253 self.assertEqual([ 254 '<namerecord nameID="222" platformID="1" platEncID="0" langID="0x7" unicode="True">', 255 ' Foo Italic', 256 '</namerecord>' 257 ], self.toXML(name)) 258 259 def test_toXML_macroman_actual_utf16be(self): 260 name = makeName("\0F\0o\0o", 222, 1, 0, 7) 261 self.assertEqual([ 262 '<namerecord nameID="222" platformID="1" platEncID="0" langID="0x7" unicode="True">', 263 ' Foo', 264 '</namerecord>' 265 ], self.toXML(name)) 266 267 def test_toXML_unknownPlatEncID_nonASCII(self): 268 name = makeName(b"B\x8arli", 333, 1, 9876, 7) # Unknown Mac encodingID 269 self.assertEqual([ 270 '<namerecord nameID="333" platformID="1" platEncID="9876" langID="0x7" unicode="False">', 271 ' BŠrli', 272 '</namerecord>' 273 ], self.toXML(name)) 274 275 def test_toXML_unknownPlatEncID_ASCII(self): 276 name = makeName(b"Barli", 333, 1, 9876, 7) # Unknown Mac encodingID 277 self.assertEqual([ 278 '<namerecord nameID="333" platformID="1" platEncID="9876" langID="0x7" unicode="True">', 279 ' Barli', 280 '</namerecord>' 281 ], self.toXML(name)) 282 283 def test_encoding_macroman_misc(self): 284 name = makeName('', 123, 1, 0, 17) # Mac Turkish 285 self.assertEqual(name.getEncoding(), "mac_turkish") 286 name.langID = 37 287 self.assertEqual(name.getEncoding(), "mac_romanian") 288 name.langID = 45 # Other 289 self.assertEqual(name.getEncoding(), "mac_roman") 290 291 def test_extended_mac_encodings(self): 292 name = makeName(b'\xfe', 123, 1, 1, 0) # Mac Japanese 293 self.assertEqual(name.toUnicode(), unichr(0x2122)) 294 295 def test_extended_unknown(self): 296 name = makeName(b'\xfe', 123, 10, 11, 12) 297 self.assertEqual(name.getEncoding(), "ascii") 298 self.assertEqual(name.getEncoding(None), None) 299 self.assertEqual(name.getEncoding(default=None), None) 300 301if __name__ == "__main__": 302 import sys 303 sys.exit(unittest.main()) 304