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