1from io import BytesIO 2import os 3import unittest 4from fontTools.ttLib import TTFont 5from fontTools.misc.textTools import strjoin 6from fontTools.misc.xmlReader import XMLReader, ProgressPrinter, BUFSIZE 7import tempfile 8 9 10class TestXMLReader(unittest.TestCase): 11 12 def test_decode_utf8(self): 13 14 class DebugXMLReader(XMLReader): 15 16 def __init__(self, fileOrPath, ttFont, progress=None): 17 super(DebugXMLReader, self).__init__( 18 fileOrPath, ttFont, progress) 19 self.contents = [] 20 21 def _endElementHandler(self, name): 22 if self.stackSize == 3: 23 name, attrs, content = self.root 24 self.contents.append(content) 25 super(DebugXMLReader, self)._endElementHandler(name) 26 27 expected = 'fôôbär' 28 data = '''\ 29<?xml version="1.0" encoding="UTF-8"?> 30<ttFont> 31 <name> 32 <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> 33 %s 34 </namerecord> 35 </name> 36</ttFont> 37''' % expected 38 39 with BytesIO(data.encode('utf-8')) as tmp: 40 reader = DebugXMLReader(tmp, TTFont()) 41 reader.read() 42 content = strjoin(reader.contents[0]).strip() 43 self.assertEqual(expected, content) 44 45 def test_normalise_newlines(self): 46 47 class DebugXMLReader(XMLReader): 48 49 def __init__(self, fileOrPath, ttFont, progress=None): 50 super(DebugXMLReader, self).__init__( 51 fileOrPath, ttFont, progress) 52 self.newlines = [] 53 54 def _characterDataHandler(self, data): 55 self.newlines.extend([c for c in data if c in ('\r', '\n')]) 56 57 # notice how when CR is escaped, it is not normalised by the XML parser 58 data = ( 59 '<ttFont>\r' # \r -> \n 60 ' <test>\r\n' # \r\n -> \n 61 ' a line of text\n' # \n 62 ' escaped CR and unix newline \n' # \n -> \r\n 63 ' escaped CR and macintosh newline \r' # \r -> \r\n 64 ' escaped CR and windows newline \r\n' # \r\n -> \r\n 65 ' </test>\n' # \n 66 '</ttFont>') 67 68 with BytesIO(data.encode('utf-8')) as tmp: 69 reader = DebugXMLReader(tmp, TTFont()) 70 reader.read() 71 expected = ['\n'] * 3 + ['\r', '\n'] * 3 + ['\n'] 72 self.assertEqual(expected, reader.newlines) 73 74 def test_progress(self): 75 76 class DummyProgressPrinter(ProgressPrinter): 77 78 def __init__(self, title, maxval=100): 79 self.label = title 80 self.maxval = maxval 81 self.pos = 0 82 83 def set(self, val, maxval=None): 84 if maxval is not None: 85 self.maxval = maxval 86 self.pos = val 87 88 def increment(self, val=1): 89 self.pos += val 90 91 def setLabel(self, text): 92 self.label = text 93 94 data = ( 95 '<ttFont>\n' 96 ' <test>\n' 97 ' %s\n' 98 ' </test>\n' 99 '</ttFont>\n' 100 % ("z" * 2 * BUFSIZE) 101 ).encode('utf-8') 102 103 dataSize = len(data) 104 progressBar = DummyProgressPrinter('test') 105 with BytesIO(data) as tmp: 106 reader = XMLReader(tmp, TTFont(), progress=progressBar) 107 self.assertEqual(progressBar.pos, 0) 108 reader.read() 109 self.assertEqual(progressBar.pos, dataSize // 100) 110 self.assertEqual(progressBar.maxval, dataSize // 100) 111 self.assertTrue('test' in progressBar.label) 112 with BytesIO(b"<ttFont></ttFont>") as tmp: 113 reader = XMLReader(tmp, TTFont(), progress=progressBar) 114 reader.read() 115 # when data size is less than 100 bytes, 'maxval' is 1 116 self.assertEqual(progressBar.maxval, 1) 117 118 def test_close_file_path(self): 119 with tempfile.NamedTemporaryFile(delete=False) as tmp: 120 tmp.write(b'<ttFont></ttFont>') 121 reader = XMLReader(tmp.name, TTFont()) 122 reader.read() 123 # when reading from path, the file is closed automatically at the end 124 self.assertTrue(reader.file.closed) 125 # this does nothing 126 reader.close() 127 self.assertTrue(reader.file.closed) 128 os.remove(tmp.name) 129 130 def test_close_file_obj(self): 131 with tempfile.NamedTemporaryFile(delete=False) as tmp: 132 tmp.write(b'<ttFont>"hello"</ttFont>') 133 with open(tmp.name, "rb") as f: 134 reader = XMLReader(f, TTFont()) 135 reader.read() 136 # when reading from a file or file-like object, the latter is kept open 137 self.assertFalse(reader.file.closed) 138 # ... until the user explicitly closes it 139 reader.close() 140 self.assertTrue(reader.file.closed) 141 os.remove(tmp.name) 142 143 def test_read_sub_file(self): 144 # Verifies that sub-file content is able to be read to a table. 145 expectedContent = 'testContent' 146 expectedNameID = '1' 147 expectedPlatform = '3' 148 expectedLangId = '0x409' 149 150 with tempfile.NamedTemporaryFile(delete=False) as tmp: 151 subFileData = ( 152 '<ttFont ttLibVersion="3.15">' 153 '<name>' 154 '<namerecord nameID="%s" platformID="%s" platEncID="1" langID="%s">' 155 '%s' 156 '</namerecord>' 157 '</name>' 158 '</ttFont>' 159 ) % (expectedNameID, expectedPlatform, expectedLangId, expectedContent) 160 tmp.write(subFileData.encode("utf-8")) 161 162 with tempfile.NamedTemporaryFile(delete=False) as tmp2: 163 fileData = ( 164 '<ttFont ttLibVersion="3.15">' 165 '<name>' 166 '<namerecord src="%s"/>' 167 '</name>' 168 '</ttFont>' 169 ) % tmp.name 170 tmp2.write(fileData.encode('utf-8')) 171 172 ttf = TTFont() 173 with open(tmp2.name, "rb") as f: 174 reader = XMLReader(f, ttf) 175 reader.read() 176 reader.close() 177 nameTable = ttf['name'] 178 self.assertTrue(int(expectedNameID) == nameTable.names[0].nameID) 179 self.assertTrue(int(expectedLangId, 16) == nameTable.names[0].langID) 180 self.assertTrue(int(expectedPlatform) == nameTable.names[0].platformID) 181 self.assertEqual(expectedContent, nameTable.names[0].string.decode(nameTable.names[0].getEncoding())) 182 183 os.remove(tmp.name) 184 os.remove(tmp2.name) 185 186if __name__ == '__main__': 187 import sys 188 sys.exit(unittest.main()) 189