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