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&#138;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