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