Our footprint in the Python module namespace is all under fontTools now. User code importing sstruct should be updated to say "from fontTools.misc import sstruct".
372 lines
12 KiB
Python
372 lines
12 KiB
Python
__doc__="""
|
|
Compiles/decompiles version 0 and 1 SVG tables from/to XML.
|
|
|
|
Version 1 is the first SVG definition, implemented in Mozilla before Aug 2013, now deprecated.
|
|
This module will decompile this correctly, but will compile a version 1 table
|
|
only if you add the secret element "<version1/>" to the SVG element in the TTF file.
|
|
|
|
Version 0 is the joint Adobe-Mozilla proposal, which supports color palettes.
|
|
|
|
The XML format is:
|
|
<SVG>
|
|
<svgDoc endGlyphID="1" startGlyphID="1">
|
|
<![CDATA[ <complete SVG doc> ]]
|
|
</svgDoc>
|
|
...
|
|
<svgDoc endGlyphID="n" startGlyphID="m">
|
|
<![CDATA[ <complete SVG doc> ]]
|
|
</svgDoc>
|
|
|
|
<colorPalettes>
|
|
<colorParamUINameID>n</colorParamUINameID>
|
|
...
|
|
<colorParamUINameID>m</colorParamUINameID>
|
|
<colorPalette uiNameID="n">
|
|
<colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" />
|
|
...
|
|
<colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" />
|
|
</colorPalette>
|
|
...
|
|
<colorPalette uiNameID="m">
|
|
<colorRecord red="<int> green="<int>" blue="<int>" alpha="<int>" />
|
|
...
|
|
<colorRecord red=<int>" green="<int>" blue="<int>" alpha="<int>" />
|
|
</colorPalette>
|
|
</colorPalettes>
|
|
</SVG>
|
|
|
|
Color values must be less than 256.
|
|
|
|
The number of color records in each </colorPalette> must be the same as
|
|
the number of <colorParamUINameID> elements.
|
|
|
|
"""
|
|
|
|
import DefaultTable
|
|
import struct
|
|
from fontTools.misc import sstruct
|
|
from fontTools.misc.textTools import safeEval
|
|
try:
|
|
import xml.etree.cElementTree as ET
|
|
except ImportError:
|
|
import xml.etree.ElementTree as ET
|
|
import string
|
|
import types
|
|
import re
|
|
|
|
XML = ET.XML
|
|
XMLElement = ET.Element
|
|
xmlToString = ET.tostring
|
|
|
|
SVG_format_0 = """
|
|
> # big endian
|
|
version: H
|
|
offsetToSVGDocIndex: L
|
|
offsetToColorPalettes: L
|
|
"""
|
|
|
|
SVG_format_0Size = sstruct.calcsize(SVG_format_0)
|
|
|
|
SVG_format_1 = """
|
|
> # big endian
|
|
version: H
|
|
numIndicies: H
|
|
"""
|
|
|
|
SVG_format_1Size = sstruct.calcsize(SVG_format_1)
|
|
|
|
doc_index_entry_format_0 = """
|
|
> # big endian
|
|
startGlyphID: H
|
|
endGlyphID: H
|
|
svgDocOffset: L
|
|
svgDocLength: L
|
|
"""
|
|
|
|
doc_index_entry_format_0Size = sstruct.calcsize(doc_index_entry_format_0)
|
|
|
|
colorRecord_format_0 = """
|
|
red: B
|
|
green: B
|
|
blue: B
|
|
alpha: B
|
|
"""
|
|
|
|
|
|
class table_S_V_G_(DefaultTable.DefaultTable):
|
|
|
|
def decompile(self, data, ttFont):
|
|
self.docList = None
|
|
self.colorPalettes = None
|
|
pos = 0
|
|
self.version = struct.unpack(">H", data[pos:pos+2])[0]
|
|
|
|
if self.version == 1:
|
|
self.decompile_format_1(data, ttFont)
|
|
else:
|
|
if self.version != 0:
|
|
print "Unknown SVG table version '%s'. Decompiling as version 0." % (self.version)
|
|
self.decompile_format_0(data, ttFont)
|
|
|
|
|
|
def decompile_format_0(self, data, ttFont):
|
|
dummy, data2 = sstruct.unpack2(SVG_format_0, data, self)
|
|
# read in SVG Documents Index
|
|
pos = self.offsetToSVGDocIndex
|
|
self.numEntries = numEntries = struct.unpack(">H", data[pos:pos+2])[0]
|
|
self.decompileEntryList(data, pos+2)
|
|
|
|
# read in colorPalettes table.
|
|
self.colorPalettes = colorPalettes = ColorPalettes()
|
|
pos = self.offsetToColorPalettes
|
|
if pos > 0:
|
|
colorPalettes.numColorParams = numColorParams = struct.unpack(">H", data[pos:pos+2])[0]
|
|
if numColorParams > 0:
|
|
colorPalettes.colorParamUINameIDs = colorParamUINameIDs = []
|
|
pos = pos + 2
|
|
i = 0
|
|
while i < numColorParams:
|
|
nameID = struct.unpack(">H", data[pos:pos+2])[0]
|
|
colorParamUINameIDs.append(nameID)
|
|
pos = pos + 2
|
|
i += 1
|
|
|
|
colorPalettes.numColorPalettes = numColorPalettes = struct.unpack(">H", data[pos:pos+2])[0]
|
|
pos = pos + 2
|
|
if numColorPalettes > 0:
|
|
colorPalettes.colorPaletteList = colorPaletteList = []
|
|
i = 0
|
|
while i < numColorPalettes:
|
|
colorPalette = ColorPalette()
|
|
colorPaletteList.append(colorPalette)
|
|
colorPalette.uiNameID = struct.unpack(">H", data[pos:pos+2])[0]
|
|
pos = pos + 2
|
|
colorPalette.paletteColors = paletteColors = []
|
|
j = 0
|
|
while j < numColorParams:
|
|
colorRecord, colorPaletteData = sstruct.unpack2(colorRecord_format_0, data[pos:], ColorRecord())
|
|
paletteColors.append(colorRecord)
|
|
j += 1
|
|
pos += 4
|
|
i += 1
|
|
|
|
def decompile_format_1(self, data, ttFont):
|
|
pos = 2
|
|
self.numEntries = struct.unpack(">H", data[pos:pos+2])[0]
|
|
pos += 2
|
|
self.decompileEntryList(data, pos)
|
|
|
|
def decompileEntryList(self, data, pos):
|
|
# data starts with the first entry of the entry list.
|
|
if self.numEntries > 0:
|
|
data2 = data[pos:]
|
|
self.docList = []
|
|
self.entries = entries = []
|
|
i = 0
|
|
while i < self.numEntries:
|
|
docIndexEntry, data2 = sstruct.unpack2(doc_index_entry_format_0, data2, DocumentIndexEntry())
|
|
entries.append(docIndexEntry)
|
|
i += 1
|
|
|
|
for entry in entries:
|
|
start = entry.svgDocOffset
|
|
end = start + entry.svgDocLength
|
|
doc = data[start:end]
|
|
self.docList.append( [doc, entry.startGlyphID, entry.endGlyphID] )
|
|
|
|
def compile(self, ttFont):
|
|
if hasattr(self, "version1"):
|
|
data = self.compileFormat1(ttFont)
|
|
else:
|
|
data = self.compileFormat0(ttFont)
|
|
return data
|
|
|
|
def compileFormat0(self, ttFont):
|
|
version = 0
|
|
offsetToSVGDocIndex = SVG_format_0Size # I start the SVGDocIndex right after the header.
|
|
|
|
# get SGVDoc info.
|
|
docList = []
|
|
entryList = []
|
|
numEntries = len(self.docList)
|
|
datum = struct.pack(">H",numEntries)
|
|
entryList.append(datum)
|
|
curOffset = offsetToSVGDocIndex + len(datum) + doc_index_entry_format_0Size*numEntries
|
|
for doc, startGlyphID, endGlyphID in self.docList:
|
|
docOffset = curOffset
|
|
docLength = len(doc)
|
|
curOffset += docLength
|
|
entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength)
|
|
entryList.append(entry)
|
|
docList.append(doc)
|
|
entryList.extend(docList)
|
|
svgDocData = "".join(entryList)
|
|
|
|
# get colorpalette info.
|
|
if self.colorPalettes == None:
|
|
offsetToColorPalettes = 0
|
|
palettesData = ""
|
|
else:
|
|
offsetToColorPalettes = SVG_format_0Size + len(svgDocData)
|
|
dataList = []
|
|
numColorParams = len(self.colorPalettes.colorParamUINameIDs)
|
|
datum = struct.pack(">H", numColorParams)
|
|
dataList.append(datum)
|
|
for uiNameId in self.colorPalettes.colorParamUINameIDs:
|
|
datum = struct.pack(">H", uiNameId)
|
|
dataList.append(datum)
|
|
numColorPalettes = len(self.colorPalettes.colorPaletteList)
|
|
datum = struct.pack(">H", numColorPalettes)
|
|
dataList.append(datum)
|
|
for colorPalette in self.colorPalettes.colorPaletteList:
|
|
datum = struct.pack(">H", colorPalette.uiNameID)
|
|
dataList.append(datum)
|
|
for colorRecord in colorPalette.paletteColors:
|
|
data = struct.pack(">BBBB", colorRecord.red, colorRecord.green, colorRecord.blue, colorRecord.alpha)
|
|
dataList.append(data)
|
|
palettesData = "".join(dataList)
|
|
|
|
header = struct.pack(">HLL", version, offsetToSVGDocIndex, offsetToColorPalettes)
|
|
data = [header, svgDocData, palettesData]
|
|
data = "".join(data)
|
|
return data
|
|
|
|
def compileFormat1(self, ttFont):
|
|
version = 1
|
|
numEntries = len(self.docList)
|
|
header = struct.pack(">HH", version, numEntries)
|
|
dataList = [header]
|
|
docList = []
|
|
curOffset = SVG_format_1Size + doc_index_entry_format_0Size*numEntries
|
|
for doc, startGlyphID, endGlyphID in self.docList:
|
|
docOffset = curOffset
|
|
docLength = len(doc)
|
|
curOffset += docLength
|
|
entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength)
|
|
dataList.append(entry)
|
|
docList.append(doc)
|
|
dataList.extend(docList)
|
|
data = "".join(dataList)
|
|
return data
|
|
|
|
def toXML(self, writer, ttFont):
|
|
writer.newline()
|
|
for doc, startGID, endGID in self.docList:
|
|
writer.begintag("svgDoc", startGlyphID=startGID, endGlyphID=endGID)
|
|
writer.newline()
|
|
writer.writeraw("<![CDATA["+ doc + "]]>")
|
|
writer.newline()
|
|
writer.endtag("svgDoc")
|
|
writer.newline()
|
|
|
|
if self.colorPalettes.numColorParams != None:
|
|
writer.begintag("colorPalettes")
|
|
writer.newline()
|
|
for uiNameID in self.colorPalettes.colorParamUINameIDs:
|
|
writer.begintag("colorParamUINameID")
|
|
writer.writeraw(str(uiNameID))
|
|
writer.endtag("colorParamUINameID")
|
|
writer.newline()
|
|
for colorPalette in self.colorPalettes.colorPaletteList:
|
|
writer.begintag("colorPalette", [("uiNameID", str(colorPalette.uiNameID))])
|
|
writer.newline()
|
|
for colorRecord in colorPalette.paletteColors:
|
|
colorAttributes = [
|
|
("red", hex(colorRecord.red)),
|
|
("green", hex(colorRecord.green)),
|
|
("blue", hex(colorRecord.blue)),
|
|
("alpha", hex(colorRecord.alpha)),
|
|
]
|
|
writer.begintag("colorRecord", colorAttributes)
|
|
writer.endtag("colorRecord")
|
|
writer.newline()
|
|
writer.endtag("colorPalette")
|
|
writer.newline()
|
|
|
|
writer.endtag("colorPalettes")
|
|
writer.newline()
|
|
else:
|
|
writer.begintag("colorPalettes")
|
|
writer.endtag("colorPalettes")
|
|
writer.newline()
|
|
|
|
def fromXML(self, (name, attrs, content), ttFont):
|
|
import re
|
|
if name == "svgDoc":
|
|
if not hasattr(self, "docList"):
|
|
self.docList = []
|
|
doc = "".join(content)
|
|
doc = doc.strip()
|
|
startGID = int(attrs["startGlyphID"])
|
|
endGID = int(attrs["endGlyphID"])
|
|
self.docList.append( [doc, startGID, endGID] )
|
|
elif name == "colorPalettes":
|
|
self.colorPalettes = ColorPalettes()
|
|
self.colorPalettes.fromXML((name, attrs, content), ttFont)
|
|
if self.colorPalettes.numColorParams == 0:
|
|
self.colorPalettes = None
|
|
else:
|
|
print "Unknown", name, content
|
|
|
|
class DocumentIndexEntry:
|
|
def __init__(self):
|
|
self.startGlyphID = None # USHORT
|
|
self.endGlyphID = None # USHORT
|
|
self.svgDocOffset = None # ULONG
|
|
self.svgDocLength = None # ULONG
|
|
|
|
def __repr__(self):
|
|
return "startGlyphID: %s, endGlyphID: %s, svgDocOffset: %s, svgDocLength: %s" % (self.startGlyphID, self.endGlyphID, self.svgDocOffset, self.svgDocLength)
|
|
|
|
class ColorPalettes:
|
|
def __init__(self):
|
|
self.numColorParams = None # USHORT
|
|
self.colorParamUINameIDs = [] # list of name table name ID values that provide UI description of each color palette.
|
|
self.numColorPalettes = None # USHORT
|
|
self.colorPaletteList = [] # list of ColorPalette records
|
|
|
|
def fromXML(self, (name, attrs, content), ttFont):
|
|
for element in content:
|
|
if type(element) == type(""):
|
|
continue
|
|
name, attrib, content = element
|
|
if name == "colorParamUINameID":
|
|
uiNameID = int(content[0])
|
|
self.colorParamUINameIDs.append(uiNameID)
|
|
elif name == "colorPalette":
|
|
colorPalette = ColorPalette()
|
|
self.colorPaletteList.append(colorPalette)
|
|
colorPalette.fromXML((name, attrib, content), ttFont)
|
|
|
|
self.numColorParams = len(self.colorParamUINameIDs)
|
|
self.numColorPalettes = len(self.colorPaletteList)
|
|
for colorPalette in self.colorPaletteList:
|
|
if len(colorPalette.paletteColors) != self.numColorParams:
|
|
raise ValueError("Number of color records in a colorPalette ('%s') does not match the number of colorParamUINameIDs elements ('%s')." % (len(colorPalette.paletteColors), self.numColorParams))
|
|
|
|
class ColorPalette:
|
|
def __init__(self):
|
|
self.uiNameID = None # USHORT. name table ID that describes user interface strings associated with this color palette.
|
|
self.paletteColors = [] # list of ColorRecords
|
|
|
|
def fromXML(self, (name, attrs, content), ttFont):
|
|
self.uiNameID = int(attrs["uiNameID"])
|
|
for element in content:
|
|
if type(element) == type(""):
|
|
continue
|
|
name, attrib, content = element
|
|
if name == "colorRecord":
|
|
colorRecord = ColorRecord()
|
|
self.paletteColors.append(colorRecord)
|
|
colorRecord.red = eval(attrib["red"])
|
|
colorRecord.green = eval(attrib["green"])
|
|
colorRecord.blue = eval(attrib["blue"])
|
|
colorRecord.alpha = eval(attrib["alpha"])
|
|
|
|
class ColorRecord:
|
|
def __init__(self):
|
|
self.red = 255 # all are one byte values.
|
|
self.green = 255
|
|
self.blue = 255
|
|
self.alpha = 255
|