S_V_G_.py 7.5 KB

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