S_V_G_.py 7.3 KB

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