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