_f_v_a_r.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 Tag, bytesjoin, safeEval
  9. from fontTools.ttLib import TTLibError
  10. from . import DefaultTable
  11. import struct
  12. # Apple's documentation of 'fvar':
  13. # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html
  14. FVAR_HEADER_FORMAT = """
  15. > # big endian
  16. version: L
  17. offsetToData: H
  18. countSizePairs: H
  19. axisCount: H
  20. axisSize: H
  21. instanceCount: H
  22. instanceSize: H
  23. """
  24. FVAR_AXIS_FORMAT = """
  25. > # big endian
  26. axisTag: 4s
  27. minValue: 16.16F
  28. defaultValue: 16.16F
  29. maxValue: 16.16F
  30. flags: H
  31. axisNameID: H
  32. """
  33. FVAR_INSTANCE_FORMAT = """
  34. > # big endian
  35. subfamilyNameID: H
  36. flags: H
  37. """
  38. class table__f_v_a_r(DefaultTable.DefaultTable):
  39. dependencies = ["name"]
  40. def __init__(self, tag=None):
  41. DefaultTable.DefaultTable.__init__(self, tag)
  42. self.axes = []
  43. self.instances = []
  44. def compile(self, ttFont):
  45. instanceSize = sstruct.calcsize(FVAR_INSTANCE_FORMAT) + (len(self.axes) * 4)
  46. includePostScriptNames = any(
  47. instance.postscriptNameID != 0xFFFF for instance in self.instances
  48. )
  49. if includePostScriptNames:
  50. instanceSize += 2
  51. header = {
  52. "version": 0x00010000,
  53. "offsetToData": sstruct.calcsize(FVAR_HEADER_FORMAT),
  54. "countSizePairs": 2,
  55. "axisCount": len(self.axes),
  56. "axisSize": sstruct.calcsize(FVAR_AXIS_FORMAT),
  57. "instanceCount": len(self.instances),
  58. "instanceSize": instanceSize,
  59. }
  60. result = [sstruct.pack(FVAR_HEADER_FORMAT, header)]
  61. result.extend([axis.compile() for axis in self.axes])
  62. axisTags = [axis.axisTag for axis in self.axes]
  63. for instance in self.instances:
  64. result.append(instance.compile(axisTags, includePostScriptNames))
  65. return bytesjoin(result)
  66. def decompile(self, data, ttFont):
  67. header = {}
  68. headerSize = sstruct.calcsize(FVAR_HEADER_FORMAT)
  69. header = sstruct.unpack(FVAR_HEADER_FORMAT, data[0:headerSize])
  70. if header["version"] != 0x00010000:
  71. raise TTLibError("unsupported 'fvar' version %04x" % header["version"])
  72. pos = header["offsetToData"]
  73. axisSize = header["axisSize"]
  74. for _ in range(header["axisCount"]):
  75. axis = Axis()
  76. axis.decompile(data[pos : pos + axisSize])
  77. self.axes.append(axis)
  78. pos += axisSize
  79. instanceSize = header["instanceSize"]
  80. axisTags = [axis.axisTag for axis in self.axes]
  81. for _ in range(header["instanceCount"]):
  82. instance = NamedInstance()
  83. instance.decompile(data[pos : pos + instanceSize], axisTags)
  84. self.instances.append(instance)
  85. pos += instanceSize
  86. def toXML(self, writer, ttFont):
  87. for axis in self.axes:
  88. axis.toXML(writer, ttFont)
  89. for instance in self.instances:
  90. instance.toXML(writer, ttFont)
  91. def fromXML(self, name, attrs, content, ttFont):
  92. if name == "Axis":
  93. axis = Axis()
  94. axis.fromXML(name, attrs, content, ttFont)
  95. self.axes.append(axis)
  96. elif name == "NamedInstance":
  97. instance = NamedInstance()
  98. instance.fromXML(name, attrs, content, ttFont)
  99. self.instances.append(instance)
  100. class Axis(object):
  101. def __init__(self):
  102. self.axisTag = None
  103. self.axisNameID = 0
  104. self.flags = 0
  105. self.minValue = -1.0
  106. self.defaultValue = 0.0
  107. self.maxValue = 1.0
  108. def compile(self):
  109. return sstruct.pack(FVAR_AXIS_FORMAT, self)
  110. def decompile(self, data):
  111. sstruct.unpack2(FVAR_AXIS_FORMAT, data, self)
  112. def toXML(self, writer, ttFont):
  113. name = (
  114. ttFont["name"].getDebugName(self.axisNameID) if "name" in ttFont else None
  115. )
  116. if name is not None:
  117. writer.newline()
  118. writer.comment(name)
  119. writer.newline()
  120. writer.begintag("Axis")
  121. writer.newline()
  122. for tag, value in [
  123. ("AxisTag", self.axisTag),
  124. ("Flags", "0x%X" % self.flags),
  125. ("MinValue", fl2str(self.minValue, 16)),
  126. ("DefaultValue", fl2str(self.defaultValue, 16)),
  127. ("MaxValue", fl2str(self.maxValue, 16)),
  128. ("AxisNameID", str(self.axisNameID)),
  129. ]:
  130. writer.begintag(tag)
  131. writer.write(value)
  132. writer.endtag(tag)
  133. writer.newline()
  134. writer.endtag("Axis")
  135. writer.newline()
  136. def fromXML(self, name, _attrs, content, ttFont):
  137. assert name == "Axis"
  138. for tag, _, value in filter(lambda t: type(t) is tuple, content):
  139. value = "".join(value)
  140. if tag == "AxisTag":
  141. self.axisTag = Tag(value)
  142. elif tag in {"Flags", "MinValue", "DefaultValue", "MaxValue", "AxisNameID"}:
  143. setattr(
  144. self,
  145. tag[0].lower() + tag[1:],
  146. str2fl(value, 16) if tag.endswith("Value") else safeEval(value),
  147. )
  148. class NamedInstance(object):
  149. def __init__(self):
  150. self.subfamilyNameID = 0
  151. self.postscriptNameID = 0xFFFF
  152. self.flags = 0
  153. self.coordinates = {}
  154. def compile(self, axisTags, includePostScriptName):
  155. result = [sstruct.pack(FVAR_INSTANCE_FORMAT, self)]
  156. for axis in axisTags:
  157. fixedCoord = fl2fi(self.coordinates[axis], 16)
  158. result.append(struct.pack(">l", fixedCoord))
  159. if includePostScriptName:
  160. result.append(struct.pack(">H", self.postscriptNameID))
  161. return bytesjoin(result)
  162. def decompile(self, data, axisTags):
  163. sstruct.unpack2(FVAR_INSTANCE_FORMAT, data, self)
  164. pos = sstruct.calcsize(FVAR_INSTANCE_FORMAT)
  165. for axis in axisTags:
  166. value = struct.unpack(">l", data[pos : pos + 4])[0]
  167. self.coordinates[axis] = fi2fl(value, 16)
  168. pos += 4
  169. if pos + 2 <= len(data):
  170. self.postscriptNameID = struct.unpack(">H", data[pos : pos + 2])[0]
  171. else:
  172. self.postscriptNameID = 0xFFFF
  173. def toXML(self, writer, ttFont):
  174. name = (
  175. ttFont["name"].getDebugName(self.subfamilyNameID)
  176. if "name" in ttFont
  177. else None
  178. )
  179. if name is not None:
  180. writer.newline()
  181. writer.comment(name)
  182. writer.newline()
  183. psname = (
  184. ttFont["name"].getDebugName(self.postscriptNameID)
  185. if "name" in ttFont
  186. else None
  187. )
  188. if psname is not None:
  189. writer.comment("PostScript: " + psname)
  190. writer.newline()
  191. if self.postscriptNameID == 0xFFFF:
  192. writer.begintag(
  193. "NamedInstance",
  194. flags=("0x%X" % self.flags),
  195. subfamilyNameID=self.subfamilyNameID,
  196. )
  197. else:
  198. writer.begintag(
  199. "NamedInstance",
  200. flags=("0x%X" % self.flags),
  201. subfamilyNameID=self.subfamilyNameID,
  202. postscriptNameID=self.postscriptNameID,
  203. )
  204. writer.newline()
  205. for axis in ttFont["fvar"].axes:
  206. writer.simpletag(
  207. "coord",
  208. axis=axis.axisTag,
  209. value=fl2str(self.coordinates[axis.axisTag], 16),
  210. )
  211. writer.newline()
  212. writer.endtag("NamedInstance")
  213. writer.newline()
  214. def fromXML(self, name, attrs, content, ttFont):
  215. assert name == "NamedInstance"
  216. self.subfamilyNameID = safeEval(attrs["subfamilyNameID"])
  217. self.flags = safeEval(attrs.get("flags", "0"))
  218. if "postscriptNameID" in attrs:
  219. self.postscriptNameID = safeEval(attrs["postscriptNameID"])
  220. else:
  221. self.postscriptNameID = 0xFFFF
  222. for tag, elementAttrs, _ in filter(lambda t: type(t) is tuple, content):
  223. if tag == "coord":
  224. value = str2fl(elementAttrs["value"], 16)
  225. self.coordinates[elementAttrs["axis"]] = value