Repository: fonttools/fonttools

Issue:
`name` table decoding error
While using the following code:
```
for record in font['name'].names:
    if record.nameID == 1:
        print(record)
```
It gave an error for some of the Chinese fonts I have:
```
Traceback (most recent call last):
  File "C:\Python39\lib\site-packages\fontTools\encodings\codecs.py", line 54, in decode
    assert errors == 'strict'
AssertionError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "d:\Desktop\coding\fontname\main.py", line 75, in <module>
    print(record)
  File "C:\Python39\lib\site-packages\fontTools\ttLib\tables\_n_a_m_e.py", line 414, in __str__
    return self.toStr(errors='backslashreplace')
  File "C:\Python39\lib\site-packages\fontTools\ttLib\tables\_n_a_m_e.py", line 460, in toUnicode
    string = tounicode(string, encoding=encoding, errors=errors)
  File "C:\Python39\lib\site-packages\fontTools\misc\py23.py", line 82, in tounicode
    return s.decode(encoding, errors)
AssertionError: decoding with 'x_mac_simp_chinese_ttx' codec failed (AssertionError: )
```
It seems to be stuck on this section (based on `ttx`):
```
    <namerecord nameID="1" platformID="3" platEncID="1" langID="0x804">
      汉仪彩云体简
    </namerecord>
```


Discussion:
Try `print(record.toUnicode())` as a workaround.
@justvanrossum  Ok that worked out, thanks. Should i close this issue?
No, let's keep it open, because `print(record)` should work, it just doesn't with this specific encoding (and possibly a few others).
Thanks for reporting!
Ok, thanks, hope to see it sloved soon.
I can't reproduce, @NightFurySL2001 could you please upload or send me a font that exhibits this issue?
I'm surprised this happens. Calling print() on a NameRecord should be equivalent to calling its `__str__` magic method and then printing the result; the `__str__` method in turn calls `record.toStr` which is an alias to `record.toUnicode` method (from the py2 days when str == bytes). Basically, it is the same as doing `print(record.toUnicode(errors="backslashreplace"))`.
This is the assert:
https://github.com/fonttools/fonttools/blob/70958dca86008aaa52086618876755968f3e82be/Lib/fontTools/encodings/codecs.py#L53-L55
The custom encodings do not support `"backslashreplace"`.

I had a reproducing snippet, but I lost it. Will try to recreate.
```python
from fontTools.ttLib.tables._n_a_m_e import NameRecord

nr = NameRecord()
nr.nameID = 0
nr.platformID = 1
nr.platEncID = 25
nr.langID = 0x804
nr.string = "汉仪彩云体简".encode("x_mac_simp_chinese_ttx")

print(nr)
```

```
Traceback (most recent call last):
  File "/Users/just/code/git/fonttools_just/Lib/fontTools/encodings/codecs.py", line 54, in decode
    assert errors == 'strict'
AssertionError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<untitled>", line 10, in <module>
  File "/Users/just/code/git/fonttools_just/Lib/fontTools/ttLib/tables/_n_a_m_e.py", line 414, in __str__
    return self.toStr(errors='backslashreplace')
  File "/Users/just/code/git/fonttools_just/Lib/fontTools/ttLib/tables/_n_a_m_e.py", line 460, in toUnicode
    string = tounicode(string, encoding=encoding, errors=errors)
  File "/Users/just/code/git/fonttools_just/Lib/fontTools/misc/py23.py", line 82, in tounicode
    return s.decode(encoding, errors)
AssertionError: decoding with 'x_mac_simp_chinese_ttx' codec failed (AssertionError: )

```

Resolving patch:
diff --git a/Lib/fontTools/encodings/codecs.py b/Lib/fontTools/encodings/codecs.py
--- a/Lib/fontTools/encodings/codecs.py
+++ b/Lib/fontTools/encodings/codecs.py
@@ -16,43 +16,29 @@ def __init__(self, name, base_encoding, mapping):
 		self.info = codecs.CodecInfo(name=self.name, encode=self.encode, decode=self.decode)
 		codecs.register_error(name, self.error)
 
-	def encode(self, input, errors='strict'):
-		assert errors == 'strict'
-		#return codecs.encode(input, self.base_encoding, self.name), len(input)
-
-		# The above line could totally be all we needed, relying on the error
-		# handling to replace the unencodable Unicode characters with our extended
-		# byte sequences.
-		#
-		# However, there seems to be a design bug in Python (probably intentional):
-		# the error handler for encoding is supposed to return a **Unicode** character,
-		# that then needs to be encodable itself...  Ugh.
-		#
-		# So we implement what codecs.encode() should have been doing: which is expect
-		# error handler to return bytes() to be added to the output.
-		#
-		# This seems to have been fixed in Python 3.3.  We should try using that and
-		# use fallback only if that failed.
-		# https://docs.python.org/3.3/library/codecs.html#codecs.register_error
-
+	def _map(self, mapper, output_type, exc_type, input, errors):
+		base_error_handler = codecs.lookup_error(errors)
 		length = len(input)
-		out = b''
+		out = output_type()
 		while input:
+			# first try to use self.error as the error handler
 			try:
-				part = codecs.encode(input, self.base_encoding)
+				part = mapper(input, self.base_encoding, errors=self.name)
 				out += part
-				input = '' # All converted
-			except UnicodeEncodeError as e:
-				# Convert the correct part
-				out += codecs.encode(input[:e.start], self.base_encoding)
-				replacement, pos = self.error(e)
+				break  # All converted
+			except exc_type as e:
+				# else convert the correct part, handle error as requested and continue
+				out += mapper(input[:e.start], self.base_encoding, self.name)
+				replacement, pos = base_error_handler(e)
 				out += replacement
 				input = input[pos:]
 		return out, length
 
+	def encode(self, input, errors='strict'):
+		return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors)
+
 	def decode(self, input, errors='strict'):
-		assert errors == 'strict'
-		return codecs.decode(input, self.base_encoding, self.name), len(input)
+		return self._map(codecs.decode, str, UnicodeDecodeError, input, errors)
 
 	def error(self, e):
 		if isinstance(e, UnicodeDecodeError):


Test patch:
diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py
--- a/Tests/ttLib/tables/_n_a_m_e_test.py
+++ b/Tests/ttLib/tables/_n_a_m_e_test.py
@@ -432,6 +432,18 @@ def test_extended_mac_encodings(self):
 		name = makeName(b'\xfe', 123, 1, 1, 0) # Mac Japanese
 		self.assertEqual(name.toUnicode(), unichr(0x2122))
 
+	def test_extended_mac_encodings_errors(self):
+		s = "汉仪彩云体简"
+		name = makeName(s.encode("x_mac_simp_chinese_ttx"), 123, 1, 25, 0)
+		# first check we round-trip with 'strict'
+		self.assertEqual(name.toUnicode(errors="strict"), s)
+
+		# append an incomplete invalid sequence and check that we handle
+		# errors with the requested error handler
+		name.string += b"\xba"
+		self.assertEqual(name.toUnicode(errors="backslashreplace"), s + "\\xba")
+		self.assertEqual(name.toUnicode(errors="replace"), s + "�")
+
 	def test_extended_unknown(self):
 		name = makeName(b'\xfe', 123, 10, 11, 12)
 		self.assertEqual(name.getEncoding(), "ascii")