1"""Compiles/decompiles SVG table. 2 3https://docs.microsoft.com/en-us/typography/opentype/spec/svg 4 5The XML format is: 6 7.. code-block:: xml 8 9 <SVG> 10 <svgDoc endGlyphID="1" startGlyphID="1"> 11 <![CDATA[ <complete SVG doc> ]] 12 </svgDoc> 13 ... 14 <svgDoc endGlyphID="n" startGlyphID="m"> 15 <![CDATA[ <complete SVG doc> ]] 16 </svgDoc> 17 </SVG> 18""" 19 20from fontTools.misc.textTools import bytesjoin, safeEval, strjoin, tobytes, tostr 21from fontTools.misc import sstruct 22from . import DefaultTable 23from collections.abc import Sequence 24from dataclasses import dataclass, astuple 25from io import BytesIO 26import struct 27import logging 28 29 30log = logging.getLogger(__name__) 31 32 33SVG_format_0 = """ 34 > # big endian 35 version: H 36 offsetToSVGDocIndex: L 37 reserved: L 38""" 39 40SVG_format_0Size = sstruct.calcsize(SVG_format_0) 41 42doc_index_entry_format_0 = """ 43 > # big endian 44 startGlyphID: H 45 endGlyphID: H 46 svgDocOffset: L 47 svgDocLength: L 48""" 49 50doc_index_entry_format_0Size = sstruct.calcsize(doc_index_entry_format_0) 51 52 53 54class table_S_V_G_(DefaultTable.DefaultTable): 55 56 def decompile(self, data, ttFont): 57 self.docList = [] 58 # Version 0 is the standardized version of the table; and current. 59 # https://www.microsoft.com/typography/otspec/svg.htm 60 sstruct.unpack(SVG_format_0, data[:SVG_format_0Size], self) 61 if self.version != 0: 62 log.warning( 63 "Unknown SVG table version '%s'. Decompiling as version 0.", self.version) 64 # read in SVG Documents Index 65 # data starts with the first entry of the entry list. 66 pos = subTableStart = self.offsetToSVGDocIndex 67 self.numEntries = struct.unpack(">H", data[pos:pos+2])[0] 68 pos += 2 69 if self.numEntries > 0: 70 data2 = data[pos:] 71 entries = [] 72 for i in range(self.numEntries): 73 docIndexEntry, data2 = sstruct.unpack2(doc_index_entry_format_0, data2, DocumentIndexEntry()) 74 entries.append(docIndexEntry) 75 76 for entry in entries: 77 start = entry.svgDocOffset + subTableStart 78 end = start + entry.svgDocLength 79 doc = data[start:end] 80 compressed = False 81 if doc.startswith(b"\x1f\x8b"): 82 import gzip 83 bytesIO = BytesIO(doc) 84 with gzip.GzipFile(None, "r", fileobj=bytesIO) as gunzipper: 85 doc = gunzipper.read() 86 del bytesIO 87 compressed = True 88 doc = tostr(doc, "utf_8") 89 self.docList.append( 90 SVGDocument(doc, entry.startGlyphID, entry.endGlyphID, compressed) 91 ) 92 93 def compile(self, ttFont): 94 version = 0 95 offsetToSVGDocIndex = SVG_format_0Size # I start the SVGDocIndex right after the header. 96 # get SGVDoc info. 97 docList = [] 98 entryList = [] 99 numEntries = len(self.docList) 100 datum = struct.pack(">H",numEntries) 101 entryList.append(datum) 102 curOffset = len(datum) + doc_index_entry_format_0Size*numEntries 103 seenDocs = {} 104 allCompressed = getattr(self, "compressed", False) 105 for i, doc in enumerate(self.docList): 106 if isinstance(doc, (list, tuple)): 107 doc = SVGDocument(*doc) 108 self.docList[i] = doc 109 docBytes = tobytes(doc.data, encoding="utf_8") 110 if (allCompressed or doc.compressed) and not docBytes.startswith(b"\x1f\x8b"): 111 import gzip 112 bytesIO = BytesIO() 113 # mtime=0 strips the useless timestamp and makes gzip output reproducible; 114 # equivalent to `gzip -n` 115 with gzip.GzipFile(None, "w", fileobj=bytesIO, mtime=0) as gzipper: 116 gzipper.write(docBytes) 117 gzipped = bytesIO.getvalue() 118 if len(gzipped) < len(docBytes): 119 docBytes = gzipped 120 del gzipped, bytesIO 121 docLength = len(docBytes) 122 if docBytes in seenDocs: 123 docOffset = seenDocs[docBytes] 124 else: 125 docOffset = curOffset 126 curOffset += docLength 127 seenDocs[docBytes] = docOffset 128 docList.append(docBytes) 129 entry = struct.pack(">HHLL", doc.startGlyphID, doc.endGlyphID, docOffset, docLength) 130 entryList.append(entry) 131 entryList.extend(docList) 132 svgDocData = bytesjoin(entryList) 133 134 reserved = 0 135 header = struct.pack(">HLL", version, offsetToSVGDocIndex, reserved) 136 data = [header, svgDocData] 137 data = bytesjoin(data) 138 return data 139 140 def toXML(self, writer, ttFont): 141 for i, doc in enumerate(self.docList): 142 if isinstance(doc, (list, tuple)): 143 doc = SVGDocument(*doc) 144 self.docList[i] = doc 145 attrs = {"startGlyphID": doc.startGlyphID, "endGlyphID": doc.endGlyphID} 146 if doc.compressed: 147 attrs["compressed"] = 1 148 writer.begintag("svgDoc", **attrs) 149 writer.newline() 150 writer.writecdata(doc.data) 151 writer.newline() 152 writer.endtag("svgDoc") 153 writer.newline() 154 155 def fromXML(self, name, attrs, content, ttFont): 156 if name == "svgDoc": 157 if not hasattr(self, "docList"): 158 self.docList = [] 159 doc = strjoin(content) 160 doc = doc.strip() 161 startGID = int(attrs["startGlyphID"]) 162 endGID = int(attrs["endGlyphID"]) 163 compressed = bool(safeEval(attrs.get("compressed", "0"))) 164 self.docList.append(SVGDocument(doc, startGID, endGID, compressed)) 165 else: 166 log.warning("Unknown %s %s", name, content) 167 168 169class DocumentIndexEntry(object): 170 def __init__(self): 171 self.startGlyphID = None # USHORT 172 self.endGlyphID = None # USHORT 173 self.svgDocOffset = None # ULONG 174 self.svgDocLength = None # ULONG 175 176 def __repr__(self): 177 return "startGlyphID: %s, endGlyphID: %s, svgDocOffset: %s, svgDocLength: %s" % (self.startGlyphID, self.endGlyphID, self.svgDocOffset, self.svgDocLength) 178 179 180@dataclass 181class SVGDocument(Sequence): 182 data: str 183 startGlyphID: int 184 endGlyphID: int 185 compressed: bool = False 186 187 # Previously, the SVG table's docList attribute contained a lists of 3 items: 188 # [doc, startGlyphID, endGlyphID]; later, we added a `compressed` attribute. 189 # For backward compatibility with code that depends of them being sequences of 190 # fixed length=3, we subclass the Sequence abstract base class and pretend only 191 # the first three items are present. 'compressed' is only accessible via named 192 # attribute lookup like regular dataclasses: i.e. `doc.compressed`, not `doc[3]` 193 def __getitem__(self, index): 194 return astuple(self)[:3][index] 195 196 def __len__(self): 197 return 3 198