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