1from fontTools.misc.py23 import bytesjoin, strjoin, tobytes, tostr 2from fontTools.misc import sstruct 3from . import DefaultTable 4try: 5 import xml.etree.cElementTree as ET 6except ImportError: 7 import xml.etree.ElementTree as ET 8from io import BytesIO 9import struct 10import logging 11 12 13log = logging.getLogger(__name__) 14 15 16__doc__=""" 17Compiles/decompiles version 0 and 1 SVG tables from/to XML. 18 19Version 1 is the first SVG definition, implemented in Mozilla before Aug 2013, now deprecated. 20This module will decompile this correctly, but will compile a version 1 table 21only if you add the secret element "<version1/>" to the SVG element in the TTF file. 22 23Version 0 is the joint Adobe-Mozilla proposal, which supports color palettes. 24 25The XML format is: 26<SVG> 27 <svgDoc endGlyphID="1" startGlyphID="1"> 28 <![CDATA[ <complete SVG doc> ]] 29 </svgDoc> 30... 31 <svgDoc endGlyphID="n" startGlyphID="m"> 32 <![CDATA[ <complete SVG doc> ]] 33 </svgDoc> 34 35 <colorPalettes> 36 <colorParamUINameID>n</colorParamUINameID> 37 ... 38 <colorParamUINameID>m</colorParamUINameID> 39 <colorPalette uiNameID="n"> 40 <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" /> 41 ... 42 <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" /> 43 </colorPalette> 44 ... 45 <colorPalette uiNameID="m"> 46 <colorRecord red="<int> green="<int>" blue="<int>" alpha="<int>" /> 47 ... 48 <colorRecord red=<int>" green="<int>" blue="<int>" alpha="<int>" /> 49 </colorPalette> 50 </colorPalettes> 51</SVG> 52 53Color values must be less than 256. 54 55The number of color records in each </colorPalette> must be the same as 56the number of <colorParamUINameID> elements. 57 58""" 59 60XML = ET.XML 61XMLElement = ET.Element 62xmlToString = ET.tostring 63 64SVG_format_0 = """ 65 > # big endian 66 version: H 67 offsetToSVGDocIndex: L 68 offsetToColorPalettes: L 69""" 70 71SVG_format_0Size = sstruct.calcsize(SVG_format_0) 72 73SVG_format_1 = """ 74 > # big endian 75 version: H 76 numIndicies: H 77""" 78 79SVG_format_1Size = sstruct.calcsize(SVG_format_1) 80 81doc_index_entry_format_0 = """ 82 > # big endian 83 startGlyphID: H 84 endGlyphID: H 85 svgDocOffset: L 86 svgDocLength: L 87""" 88 89doc_index_entry_format_0Size = sstruct.calcsize(doc_index_entry_format_0) 90 91colorRecord_format_0 = """ 92 red: B 93 green: B 94 blue: B 95 alpha: B 96""" 97 98 99class table_S_V_G_(DefaultTable.DefaultTable): 100 101 def __init__(self, tag=None): 102 DefaultTable.DefaultTable.__init__(self, tag) 103 self.colorPalettes = None 104 105 def decompile(self, data, ttFont): 106 self.docList = None 107 self.colorPalettes = None 108 pos = 0 109 self.version = struct.unpack(">H", data[pos:pos+2])[0] 110 111 if self.version == 1: 112 # This is pre-standardization version of the table; and obsolete. But we decompile it for now. 113 # https://wiki.mozilla.org/SVGOpenTypeFonts 114 self.decompile_format_1(data, ttFont) 115 else: 116 if self.version != 0: 117 log.warning( 118 "Unknown SVG table version '%s'. Decompiling as version 0.", self.version) 119 # This is the standardized version of the table; and current. 120 # https://www.microsoft.com/typography/otspec/svg.htm 121 self.decompile_format_0(data, ttFont) 122 123 def decompile_format_0(self, data, ttFont): 124 dummy, data2 = sstruct.unpack2(SVG_format_0, data, self) 125 # read in SVG Documents Index 126 self.decompileEntryList(data) 127 128 # read in colorPalettes table. 129 self.colorPalettes = colorPalettes = ColorPalettes() 130 pos = self.offsetToColorPalettes 131 if pos > 0: 132 colorPalettes.numColorParams = numColorParams = struct.unpack(">H", data[pos:pos+2])[0] 133 if numColorParams > 0: 134 colorPalettes.colorParamUINameIDs = colorParamUINameIDs = [] 135 pos = pos + 2 136 for i in range(numColorParams): 137 nameID = struct.unpack(">H", data[pos:pos+2])[0] 138 colorParamUINameIDs.append(nameID) 139 pos = pos + 2 140 141 colorPalettes.numColorPalettes = numColorPalettes = struct.unpack(">H", data[pos:pos+2])[0] 142 pos = pos + 2 143 if numColorPalettes > 0: 144 colorPalettes.colorPaletteList = colorPaletteList = [] 145 for i in range(numColorPalettes): 146 colorPalette = ColorPalette() 147 colorPaletteList.append(colorPalette) 148 colorPalette.uiNameID = struct.unpack(">H", data[pos:pos+2])[0] 149 pos = pos + 2 150 colorPalette.paletteColors = paletteColors = [] 151 for j in range(numColorParams): 152 colorRecord, colorPaletteData = sstruct.unpack2(colorRecord_format_0, data[pos:], ColorRecord()) 153 paletteColors.append(colorRecord) 154 pos += 4 155 156 def decompile_format_1(self, data, ttFont): 157 self.offsetToSVGDocIndex = 2 158 self.decompileEntryList(data) 159 160 def decompileEntryList(self, data): 161 # data starts with the first entry of the entry list. 162 pos = subTableStart = self.offsetToSVGDocIndex 163 self.numEntries = numEntries = struct.unpack(">H", data[pos:pos+2])[0] 164 pos += 2 165 if self.numEntries > 0: 166 data2 = data[pos:] 167 self.docList = [] 168 self.entries = entries = [] 169 for i in range(self.numEntries): 170 docIndexEntry, data2 = sstruct.unpack2(doc_index_entry_format_0, data2, DocumentIndexEntry()) 171 entries.append(docIndexEntry) 172 173 for entry in entries: 174 start = entry.svgDocOffset + subTableStart 175 end = start + entry.svgDocLength 176 doc = data[start:end] 177 if doc.startswith(b"\x1f\x8b"): 178 import gzip 179 bytesIO = BytesIO(doc) 180 with gzip.GzipFile(None, "r", fileobj=bytesIO) as gunzipper: 181 doc = gunzipper.read() 182 self.compressed = True 183 del bytesIO 184 doc = tostr(doc, "utf_8") 185 self.docList.append( [doc, entry.startGlyphID, entry.endGlyphID] ) 186 187 def compile(self, ttFont): 188 if hasattr(self, "version1"): 189 data = self.compileFormat1(ttFont) 190 else: 191 data = self.compileFormat0(ttFont) 192 return data 193 194 def compileFormat0(self, ttFont): 195 version = 0 196 offsetToSVGDocIndex = SVG_format_0Size # I start the SVGDocIndex right after the header. 197 # get SGVDoc info. 198 docList = [] 199 entryList = [] 200 numEntries = len(self.docList) 201 datum = struct.pack(">H",numEntries) 202 entryList.append(datum) 203 curOffset = len(datum) + doc_index_entry_format_0Size*numEntries 204 for doc, startGlyphID, endGlyphID in self.docList: 205 docOffset = curOffset 206 docBytes = tobytes(doc, encoding="utf_8") 207 if getattr(self, "compressed", False) and not docBytes.startswith(b"\x1f\x8b"): 208 import gzip 209 bytesIO = BytesIO() 210 with gzip.GzipFile(None, "w", fileobj=bytesIO) as gzipper: 211 gzipper.write(docBytes) 212 gzipped = bytesIO.getvalue() 213 if len(gzipped) < len(docBytes): 214 docBytes = gzipped 215 del gzipped, bytesIO 216 docLength = len(docBytes) 217 curOffset += docLength 218 entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength) 219 entryList.append(entry) 220 docList.append(docBytes) 221 entryList.extend(docList) 222 svgDocData = bytesjoin(entryList) 223 224 # get colorpalette info. 225 if self.colorPalettes is None: 226 offsetToColorPalettes = 0 227 palettesData = "" 228 else: 229 offsetToColorPalettes = SVG_format_0Size + len(svgDocData) 230 dataList = [] 231 numColorParams = len(self.colorPalettes.colorParamUINameIDs) 232 datum = struct.pack(">H", numColorParams) 233 dataList.append(datum) 234 for uiNameId in self.colorPalettes.colorParamUINameIDs: 235 datum = struct.pack(">H", uiNameId) 236 dataList.append(datum) 237 numColorPalettes = len(self.colorPalettes.colorPaletteList) 238 datum = struct.pack(">H", numColorPalettes) 239 dataList.append(datum) 240 for colorPalette in self.colorPalettes.colorPaletteList: 241 datum = struct.pack(">H", colorPalette.uiNameID) 242 dataList.append(datum) 243 for colorRecord in colorPalette.paletteColors: 244 data = struct.pack(">BBBB", colorRecord.red, colorRecord.green, colorRecord.blue, colorRecord.alpha) 245 dataList.append(data) 246 palettesData = bytesjoin(dataList) 247 248 header = struct.pack(">HLL", version, offsetToSVGDocIndex, offsetToColorPalettes) 249 data = [header, svgDocData, palettesData] 250 data = bytesjoin(data) 251 return data 252 253 def compileFormat1(self, ttFont): 254 version = 1 255 numEntries = len(self.docList) 256 header = struct.pack(">HH", version, numEntries) 257 dataList = [header] 258 docList = [] 259 curOffset = SVG_format_1Size + doc_index_entry_format_0Size*numEntries 260 for doc, startGlyphID, endGlyphID in self.docList: 261 docOffset = curOffset 262 docBytes = tobytes(doc, encoding="utf_8") 263 docLength = len(docBytes) 264 curOffset += docLength 265 entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength) 266 dataList.append(entry) 267 docList.append(docBytes) 268 dataList.extend(docList) 269 data = bytesjoin(dataList) 270 return data 271 272 def toXML(self, writer, ttFont): 273 writer.newline() 274 for doc, startGID, endGID in self.docList: 275 writer.begintag("svgDoc", startGlyphID=startGID, endGlyphID=endGID) 276 writer.newline() 277 writer.writecdata(doc) 278 writer.newline() 279 writer.endtag("svgDoc") 280 writer.newline() 281 282 if (self.colorPalettes is not None) and (self.colorPalettes.numColorParams is not None): 283 writer.begintag("colorPalettes") 284 writer.newline() 285 for uiNameID in self.colorPalettes.colorParamUINameIDs: 286 writer.begintag("colorParamUINameID") 287 writer._writeraw(str(uiNameID)) 288 writer.endtag("colorParamUINameID") 289 writer.newline() 290 for colorPalette in self.colorPalettes.colorPaletteList: 291 writer.begintag("colorPalette", [("uiNameID", str(colorPalette.uiNameID))]) 292 writer.newline() 293 for colorRecord in colorPalette.paletteColors: 294 colorAttributes = [ 295 ("red", hex(colorRecord.red)), 296 ("green", hex(colorRecord.green)), 297 ("blue", hex(colorRecord.blue)), 298 ("alpha", hex(colorRecord.alpha)), 299 ] 300 writer.begintag("colorRecord", colorAttributes) 301 writer.endtag("colorRecord") 302 writer.newline() 303 writer.endtag("colorPalette") 304 writer.newline() 305 306 writer.endtag("colorPalettes") 307 writer.newline() 308 309 def fromXML(self, name, attrs, content, ttFont): 310 if name == "svgDoc": 311 if not hasattr(self, "docList"): 312 self.docList = [] 313 doc = strjoin(content) 314 doc = doc.strip() 315 startGID = int(attrs["startGlyphID"]) 316 endGID = int(attrs["endGlyphID"]) 317 self.docList.append( [doc, startGID, endGID] ) 318 elif name == "colorPalettes": 319 self.colorPalettes = ColorPalettes() 320 self.colorPalettes.fromXML(name, attrs, content, ttFont) 321 if self.colorPalettes.numColorParams == 0: 322 self.colorPalettes = None 323 else: 324 log.warning("Unknown %s %s", name, content) 325 326class DocumentIndexEntry(object): 327 def __init__(self): 328 self.startGlyphID = None # USHORT 329 self.endGlyphID = None # USHORT 330 self.svgDocOffset = None # ULONG 331 self.svgDocLength = None # ULONG 332 333 def __repr__(self): 334 return "startGlyphID: %s, endGlyphID: %s, svgDocOffset: %s, svgDocLength: %s" % (self.startGlyphID, self.endGlyphID, self.svgDocOffset, self.svgDocLength) 335 336class ColorPalettes(object): 337 def __init__(self): 338 self.numColorParams = None # USHORT 339 self.colorParamUINameIDs = [] # list of name table name ID values that provide UI description of each color palette. 340 self.numColorPalettes = None # USHORT 341 self.colorPaletteList = [] # list of ColorPalette records 342 343 def fromXML(self, name, attrs, content, ttFont): 344 for element in content: 345 if not isinstance(element, tuple): 346 continue 347 name, attrib, content = element 348 if name == "colorParamUINameID": 349 uiNameID = int(content[0]) 350 self.colorParamUINameIDs.append(uiNameID) 351 elif name == "colorPalette": 352 colorPalette = ColorPalette() 353 self.colorPaletteList.append(colorPalette) 354 colorPalette.fromXML(name, attrib, content, ttFont) 355 356 self.numColorParams = len(self.colorParamUINameIDs) 357 self.numColorPalettes = len(self.colorPaletteList) 358 for colorPalette in self.colorPaletteList: 359 if len(colorPalette.paletteColors) != self.numColorParams: 360 raise ValueError("Number of color records in a colorPalette ('%s') does not match the number of colorParamUINameIDs elements ('%s')." % (len(colorPalette.paletteColors), self.numColorParams)) 361 362class ColorPalette(object): 363 def __init__(self): 364 self.uiNameID = None # USHORT. name table ID that describes user interface strings associated with this color palette. 365 self.paletteColors = [] # list of ColorRecords 366 367 def fromXML(self, name, attrs, content, ttFont): 368 self.uiNameID = int(attrs["uiNameID"]) 369 for element in content: 370 if isinstance(element, type("")): 371 continue 372 name, attrib, content = element 373 if name == "colorRecord": 374 colorRecord = ColorRecord() 375 self.paletteColors.append(colorRecord) 376 colorRecord.red = eval(attrib["red"]) 377 colorRecord.green = eval(attrib["green"]) 378 colorRecord.blue = eval(attrib["blue"]) 379 colorRecord.alpha = eval(attrib["alpha"]) 380 381class ColorRecord(object): 382 def __init__(self): 383 self.red = 255 # all are one byte values. 384 self.green = 255 385 self.blue = 255 386 self.alpha = 255 387