• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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