_t_r_a_k.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. from fontTools.misc import sstruct
  2. from fontTools.misc.fixedTools import (
  3. fixedToFloat as fi2fl,
  4. floatToFixed as fl2fi,
  5. floatToFixedToStr as fl2str,
  6. strToFixedToFloat as str2fl,
  7. )
  8. from fontTools.misc.textTools import bytesjoin, safeEval
  9. from fontTools.ttLib import TTLibError
  10. from . import DefaultTable
  11. import struct
  12. from collections.abc import MutableMapping
  13. # Apple's documentation of 'trak':
  14. # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html
  15. TRAK_HEADER_FORMAT = """
  16. > # big endian
  17. version: 16.16F
  18. format: H
  19. horizOffset: H
  20. vertOffset: H
  21. reserved: H
  22. """
  23. TRAK_HEADER_FORMAT_SIZE = sstruct.calcsize(TRAK_HEADER_FORMAT)
  24. TRACK_DATA_FORMAT = """
  25. > # big endian
  26. nTracks: H
  27. nSizes: H
  28. sizeTableOffset: L
  29. """
  30. TRACK_DATA_FORMAT_SIZE = sstruct.calcsize(TRACK_DATA_FORMAT)
  31. TRACK_TABLE_ENTRY_FORMAT = """
  32. > # big endian
  33. track: 16.16F
  34. nameIndex: H
  35. offset: H
  36. """
  37. TRACK_TABLE_ENTRY_FORMAT_SIZE = sstruct.calcsize(TRACK_TABLE_ENTRY_FORMAT)
  38. # size values are actually '16.16F' fixed-point values, but here I do the
  39. # fixedToFloat conversion manually instead of relying on sstruct
  40. SIZE_VALUE_FORMAT = ">l"
  41. SIZE_VALUE_FORMAT_SIZE = struct.calcsize(SIZE_VALUE_FORMAT)
  42. # per-Size values are in 'FUnits', i.e. 16-bit signed integers
  43. PER_SIZE_VALUE_FORMAT = ">h"
  44. PER_SIZE_VALUE_FORMAT_SIZE = struct.calcsize(PER_SIZE_VALUE_FORMAT)
  45. class table__t_r_a_k(DefaultTable.DefaultTable):
  46. dependencies = ["name"]
  47. def compile(self, ttFont):
  48. dataList = []
  49. offset = TRAK_HEADER_FORMAT_SIZE
  50. for direction in ("horiz", "vert"):
  51. trackData = getattr(self, direction + "Data", TrackData())
  52. offsetName = direction + "Offset"
  53. # set offset to 0 if None or empty
  54. if not trackData:
  55. setattr(self, offsetName, 0)
  56. continue
  57. # TrackData table format must be longword aligned
  58. alignedOffset = (offset + 3) & ~3
  59. padding, offset = b"\x00" * (alignedOffset - offset), alignedOffset
  60. setattr(self, offsetName, offset)
  61. data = trackData.compile(offset)
  62. offset += len(data)
  63. dataList.append(padding + data)
  64. self.reserved = 0
  65. tableData = bytesjoin([sstruct.pack(TRAK_HEADER_FORMAT, self)] + dataList)
  66. return tableData
  67. def decompile(self, data, ttFont):
  68. sstruct.unpack(TRAK_HEADER_FORMAT, data[:TRAK_HEADER_FORMAT_SIZE], self)
  69. for direction in ("horiz", "vert"):
  70. trackData = TrackData()
  71. offset = getattr(self, direction + "Offset")
  72. if offset != 0:
  73. trackData.decompile(data, offset)
  74. setattr(self, direction + "Data", trackData)
  75. def toXML(self, writer, ttFont):
  76. writer.simpletag("version", value=self.version)
  77. writer.newline()
  78. writer.simpletag("format", value=self.format)
  79. writer.newline()
  80. for direction in ("horiz", "vert"):
  81. dataName = direction + "Data"
  82. writer.begintag(dataName)
  83. writer.newline()
  84. trackData = getattr(self, dataName, TrackData())
  85. trackData.toXML(writer, ttFont)
  86. writer.endtag(dataName)
  87. writer.newline()
  88. def fromXML(self, name, attrs, content, ttFont):
  89. if name == "version":
  90. self.version = safeEval(attrs["value"])
  91. elif name == "format":
  92. self.format = safeEval(attrs["value"])
  93. elif name in ("horizData", "vertData"):
  94. trackData = TrackData()
  95. setattr(self, name, trackData)
  96. for element in content:
  97. if not isinstance(element, tuple):
  98. continue
  99. name, attrs, content_ = element
  100. trackData.fromXML(name, attrs, content_, ttFont)
  101. class TrackData(MutableMapping):
  102. def __init__(self, initialdata={}):
  103. self._map = dict(initialdata)
  104. def compile(self, offset):
  105. nTracks = len(self)
  106. sizes = self.sizes()
  107. nSizes = len(sizes)
  108. # offset to the start of the size subtable
  109. offset += TRACK_DATA_FORMAT_SIZE + TRACK_TABLE_ENTRY_FORMAT_SIZE * nTracks
  110. trackDataHeader = sstruct.pack(
  111. TRACK_DATA_FORMAT,
  112. {"nTracks": nTracks, "nSizes": nSizes, "sizeTableOffset": offset},
  113. )
  114. entryDataList = []
  115. perSizeDataList = []
  116. # offset to per-size tracking values
  117. offset += SIZE_VALUE_FORMAT_SIZE * nSizes
  118. # sort track table entries by track value
  119. for track, entry in sorted(self.items()):
  120. assert entry.nameIndex is not None
  121. entry.track = track
  122. entry.offset = offset
  123. entryDataList += [sstruct.pack(TRACK_TABLE_ENTRY_FORMAT, entry)]
  124. # sort per-size values by size
  125. for size, value in sorted(entry.items()):
  126. perSizeDataList += [struct.pack(PER_SIZE_VALUE_FORMAT, value)]
  127. offset += PER_SIZE_VALUE_FORMAT_SIZE * nSizes
  128. # sort size values
  129. sizeDataList = [
  130. struct.pack(SIZE_VALUE_FORMAT, fl2fi(sv, 16)) for sv in sorted(sizes)
  131. ]
  132. data = bytesjoin(
  133. [trackDataHeader] + entryDataList + sizeDataList + perSizeDataList
  134. )
  135. return data
  136. def decompile(self, data, offset):
  137. # initial offset is from the start of trak table to the current TrackData
  138. trackDataHeader = data[offset : offset + TRACK_DATA_FORMAT_SIZE]
  139. if len(trackDataHeader) != TRACK_DATA_FORMAT_SIZE:
  140. raise TTLibError("not enough data to decompile TrackData header")
  141. sstruct.unpack(TRACK_DATA_FORMAT, trackDataHeader, self)
  142. offset += TRACK_DATA_FORMAT_SIZE
  143. nSizes = self.nSizes
  144. sizeTableOffset = self.sizeTableOffset
  145. sizeTable = []
  146. for i in range(nSizes):
  147. sizeValueData = data[
  148. sizeTableOffset : sizeTableOffset + SIZE_VALUE_FORMAT_SIZE
  149. ]
  150. if len(sizeValueData) < SIZE_VALUE_FORMAT_SIZE:
  151. raise TTLibError("not enough data to decompile TrackData size subtable")
  152. (sizeValue,) = struct.unpack(SIZE_VALUE_FORMAT, sizeValueData)
  153. sizeTable.append(fi2fl(sizeValue, 16))
  154. sizeTableOffset += SIZE_VALUE_FORMAT_SIZE
  155. for i in range(self.nTracks):
  156. entry = TrackTableEntry()
  157. entryData = data[offset : offset + TRACK_TABLE_ENTRY_FORMAT_SIZE]
  158. if len(entryData) < TRACK_TABLE_ENTRY_FORMAT_SIZE:
  159. raise TTLibError("not enough data to decompile TrackTableEntry record")
  160. sstruct.unpack(TRACK_TABLE_ENTRY_FORMAT, entryData, entry)
  161. perSizeOffset = entry.offset
  162. for j in range(nSizes):
  163. size = sizeTable[j]
  164. perSizeValueData = data[
  165. perSizeOffset : perSizeOffset + PER_SIZE_VALUE_FORMAT_SIZE
  166. ]
  167. if len(perSizeValueData) < PER_SIZE_VALUE_FORMAT_SIZE:
  168. raise TTLibError(
  169. "not enough data to decompile per-size track values"
  170. )
  171. (perSizeValue,) = struct.unpack(PER_SIZE_VALUE_FORMAT, perSizeValueData)
  172. entry[size] = perSizeValue
  173. perSizeOffset += PER_SIZE_VALUE_FORMAT_SIZE
  174. self[entry.track] = entry
  175. offset += TRACK_TABLE_ENTRY_FORMAT_SIZE
  176. def toXML(self, writer, ttFont):
  177. nTracks = len(self)
  178. nSizes = len(self.sizes())
  179. writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes))
  180. writer.newline()
  181. for track, entry in sorted(self.items()):
  182. assert entry.nameIndex is not None
  183. entry.track = track
  184. entry.toXML(writer, ttFont)
  185. def fromXML(self, name, attrs, content, ttFont):
  186. if name != "trackEntry":
  187. return
  188. entry = TrackTableEntry()
  189. entry.fromXML(name, attrs, content, ttFont)
  190. self[entry.track] = entry
  191. def sizes(self):
  192. if not self:
  193. return frozenset()
  194. tracks = list(self.tracks())
  195. sizes = self[tracks.pop(0)].sizes()
  196. for track in tracks:
  197. entrySizes = self[track].sizes()
  198. if sizes != entrySizes:
  199. raise TTLibError(
  200. "'trak' table entries must specify the same sizes: "
  201. "%s != %s" % (sorted(sizes), sorted(entrySizes))
  202. )
  203. return frozenset(sizes)
  204. def __getitem__(self, track):
  205. return self._map[track]
  206. def __delitem__(self, track):
  207. del self._map[track]
  208. def __setitem__(self, track, entry):
  209. self._map[track] = entry
  210. def __len__(self):
  211. return len(self._map)
  212. def __iter__(self):
  213. return iter(self._map)
  214. def keys(self):
  215. return self._map.keys()
  216. tracks = keys
  217. def __repr__(self):
  218. return "TrackData({})".format(self._map if self else "")
  219. class TrackTableEntry(MutableMapping):
  220. def __init__(self, values={}, nameIndex=None):
  221. self.nameIndex = nameIndex
  222. self._map = dict(values)
  223. def toXML(self, writer, ttFont):
  224. name = ttFont["name"].getDebugName(self.nameIndex)
  225. writer.begintag(
  226. "trackEntry",
  227. (("value", fl2str(self.track, 16)), ("nameIndex", self.nameIndex)),
  228. )
  229. writer.newline()
  230. if name:
  231. writer.comment(name)
  232. writer.newline()
  233. for size, perSizeValue in sorted(self.items()):
  234. writer.simpletag("track", size=fl2str(size, 16), value=perSizeValue)
  235. writer.newline()
  236. writer.endtag("trackEntry")
  237. writer.newline()
  238. def fromXML(self, name, attrs, content, ttFont):
  239. self.track = str2fl(attrs["value"], 16)
  240. self.nameIndex = safeEval(attrs["nameIndex"])
  241. for element in content:
  242. if not isinstance(element, tuple):
  243. continue
  244. name, attrs, _ = element
  245. if name != "track":
  246. continue
  247. size = str2fl(attrs["size"], 16)
  248. self[size] = safeEval(attrs["value"])
  249. def __getitem__(self, size):
  250. return self._map[size]
  251. def __delitem__(self, size):
  252. del self._map[size]
  253. def __setitem__(self, size, value):
  254. self._map[size] = value
  255. def __len__(self):
  256. return len(self._map)
  257. def __iter__(self):
  258. return iter(self._map)
  259. def keys(self):
  260. return self._map.keys()
  261. sizes = keys
  262. def __repr__(self):
  263. return "TrackTableEntry({}, nameIndex={})".format(self._map, self.nameIndex)
  264. def __eq__(self, other):
  265. if not isinstance(other, self.__class__):
  266. return NotImplemented
  267. return self.nameIndex == other.nameIndex and dict(self) == dict(other)
  268. def __ne__(self, other):
  269. result = self.__eq__(other)
  270. return result if result is NotImplemented else not result