1from fontTools import ttLib 2from fontTools.misc.textTools import safeEval 3from fontTools.ttLib.tables.DefaultTable import DefaultTable 4import sys 5import os 6import logging 7 8 9log = logging.getLogger(__name__) 10 11 12class TTXParseError(Exception): 13 pass 14 15 16BUFSIZE = 0x4000 17 18 19class XMLReader(object): 20 def __init__( 21 self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False 22 ): 23 if fileOrPath == "-": 24 fileOrPath = sys.stdin 25 if not hasattr(fileOrPath, "read"): 26 self.file = open(fileOrPath, "rb") 27 self._closeStream = True 28 else: 29 # assume readable file object 30 self.file = fileOrPath 31 self._closeStream = False 32 self.ttFont = ttFont 33 self.progress = progress 34 if quiet is not None: 35 from fontTools.misc.loggingTools import deprecateArgument 36 37 deprecateArgument("quiet", "configure logging instead") 38 self.quiet = quiet 39 self.root = None 40 self.contentStack = [] 41 self.contentOnly = contentOnly 42 self.stackSize = 0 43 44 def read(self, rootless=False): 45 if rootless: 46 self.stackSize += 1 47 if self.progress: 48 self.file.seek(0, 2) 49 fileSize = self.file.tell() 50 self.progress.set(0, fileSize // 100 or 1) 51 self.file.seek(0) 52 self._parseFile(self.file) 53 if self._closeStream: 54 self.close() 55 if rootless: 56 self.stackSize -= 1 57 58 def close(self): 59 self.file.close() 60 61 def _parseFile(self, file): 62 from xml.parsers.expat import ParserCreate 63 64 parser = ParserCreate() 65 parser.StartElementHandler = self._startElementHandler 66 parser.EndElementHandler = self._endElementHandler 67 parser.CharacterDataHandler = self._characterDataHandler 68 69 pos = 0 70 while True: 71 chunk = file.read(BUFSIZE) 72 if not chunk: 73 parser.Parse(chunk, 1) 74 break 75 pos = pos + len(chunk) 76 if self.progress: 77 self.progress.set(pos // 100) 78 parser.Parse(chunk, 0) 79 80 def _startElementHandler(self, name, attrs): 81 if self.stackSize == 1 and self.contentOnly: 82 # We already know the table we're parsing, skip 83 # parsing the table tag and continue to 84 # stack '2' which begins parsing content 85 self.contentStack.append([]) 86 self.stackSize = 2 87 return 88 stackSize = self.stackSize 89 self.stackSize = stackSize + 1 90 subFile = attrs.get("src") 91 if subFile is not None: 92 if hasattr(self.file, "name"): 93 # if file has a name, get its parent directory 94 dirname = os.path.dirname(self.file.name) 95 else: 96 # else fall back to using the current working directory 97 dirname = os.getcwd() 98 subFile = os.path.join(dirname, subFile) 99 if not stackSize: 100 if name != "ttFont": 101 raise TTXParseError("illegal root tag: %s" % name) 102 if self.ttFont.reader is None and not self.ttFont.tables: 103 sfntVersion = attrs.get("sfntVersion") 104 if sfntVersion is not None: 105 if len(sfntVersion) != 4: 106 sfntVersion = safeEval('"' + sfntVersion + '"') 107 self.ttFont.sfntVersion = sfntVersion 108 self.contentStack.append([]) 109 elif stackSize == 1: 110 if subFile is not None: 111 subReader = XMLReader(subFile, self.ttFont, self.progress) 112 subReader.read() 113 self.contentStack.append([]) 114 return 115 tag = ttLib.xmlToTag(name) 116 msg = "Parsing '%s' table..." % tag 117 if self.progress: 118 self.progress.setLabel(msg) 119 log.info(msg) 120 if tag == "GlyphOrder": 121 tableClass = ttLib.GlyphOrder 122 elif "ERROR" in attrs or ("raw" in attrs and safeEval(attrs["raw"])): 123 tableClass = DefaultTable 124 else: 125 tableClass = ttLib.getTableClass(tag) 126 if tableClass is None: 127 tableClass = DefaultTable 128 if tag == "loca" and tag in self.ttFont: 129 # Special-case the 'loca' table as we need the 130 # original if the 'glyf' table isn't recompiled. 131 self.currentTable = self.ttFont[tag] 132 else: 133 self.currentTable = tableClass(tag) 134 self.ttFont[tag] = self.currentTable 135 self.contentStack.append([]) 136 elif stackSize == 2 and subFile is not None: 137 subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True) 138 subReader.read() 139 self.contentStack.append([]) 140 self.root = subReader.root 141 elif stackSize == 2: 142 self.contentStack.append([]) 143 self.root = (name, attrs, self.contentStack[-1]) 144 else: 145 l = [] 146 self.contentStack[-1].append((name, attrs, l)) 147 self.contentStack.append(l) 148 149 def _characterDataHandler(self, data): 150 if self.stackSize > 1: 151 # parser parses in chunks, so we may get multiple calls 152 # for the same text node; thus we need to append the data 153 # to the last item in the content stack: 154 # https://github.com/fonttools/fonttools/issues/2614 155 if ( 156 data != "\n" 157 and self.contentStack[-1] 158 and isinstance(self.contentStack[-1][-1], str) 159 and self.contentStack[-1][-1] != "\n" 160 ): 161 self.contentStack[-1][-1] += data 162 else: 163 self.contentStack[-1].append(data) 164 165 def _endElementHandler(self, name): 166 self.stackSize = self.stackSize - 1 167 del self.contentStack[-1] 168 if not self.contentOnly: 169 if self.stackSize == 1: 170 self.root = None 171 elif self.stackSize == 2: 172 name, attrs, content = self.root 173 self.currentTable.fromXML(name, attrs, content, self.ttFont) 174 self.root = None 175 176 177class ProgressPrinter(object): 178 def __init__(self, title, maxval=100): 179 print(title) 180 181 def set(self, val, maxval=None): 182 pass 183 184 def increment(self, val=1): 185 pass 186 187 def setLabel(self, text): 188 print(text) 189