Home | History | Annotate | Download | only in tables
      1 from __future__ import print_function, division, absolute_import
      2 from fontTools.misc.py23 import *
      3 from fontTools.misc import sstruct
      4 from . import DefaultTable
      5 try:
      6 	import xml.etree.cElementTree as ET
      7 except ImportError:
      8 	import xml.etree.ElementTree as ET
      9 import struct
     10 import re
     11 import logging
     12 
     13 
     14 log = logging.getLogger(__name__)
     15 
     16 
     17 __doc__="""
     18 Compiles/decompiles version 0 and 1 SVG tables from/to XML.
     19 
     20 Version 1 is the first SVG definition, implemented in Mozilla before Aug 2013, now deprecated.
     21 This module will decompile this correctly, but will compile a version 1 table
     22 only if you add the secret element "<version1/>" to the SVG element in the TTF file.
     23 
     24 Version 0 is the joint Adobe-Mozilla proposal, which supports color palettes.
     25 
     26 The 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 
     54 Color values must be less than 256.
     55 
     56 The number of color records in each </colorPalette> must be the same as
     57 the number of <colorParamUINameID> elements.
     58 
     59 """
     60 
     61 XML = ET.XML
     62 XMLElement = ET.Element
     63 xmlToString = ET.tostring
     64 
     65 SVG_format_0 = """
     66 	>   # big endian
     67 	version:                  H
     68 	offsetToSVGDocIndex:      L
     69 	offsetToColorPalettes:    L
     70 """
     71 
     72 SVG_format_0Size = sstruct.calcsize(SVG_format_0)
     73 
     74 SVG_format_1 = """
     75 	>   # big endian
     76 	version:                  H
     77 	numIndicies:              H
     78 """
     79 
     80 SVG_format_1Size = sstruct.calcsize(SVG_format_1)
     81 
     82 doc_index_entry_format_0 = """
     83 	>   # big endian
     84 	startGlyphID:             H
     85 	endGlyphID:               H
     86 	svgDocOffset:             L
     87 	svgDocLength:             L
     88 """
     89 
     90 doc_index_entry_format_0Size = sstruct.calcsize(doc_index_entry_format_0)
     91 
     92 colorRecord_format_0 = """
     93 	red:                      B
     94 	green:                    B
     95 	blue:                     B
     96 	alpha:                    B
     97 """
     98 
     99 
    100 class 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 
    327 class 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 
    337 class 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 
    363 class 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 
    382 class 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