Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/_f_v_a_r.py: 19%
150 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
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
14# Apple's documentation of 'fvar':
15# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html
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"""
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"""
38FVAR_INSTANCE_FORMAT = """
39 > # big endian
40 subfamilyNameID: H
41 flags: H
42"""
45class table__f_v_a_r(DefaultTable.DefaultTable):
46 dependencies = ["name"]
48 def __init__(self, tag=None):
49 DefaultTable.DefaultTable.__init__(self, tag)
50 self.axes = []
51 self.instances = []
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)
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
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)
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)
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
123 def compile(self):
124 return sstruct.pack(FVAR_AXIS_FORMAT, self)
126 def decompile(self, data):
127 sstruct.unpack2(FVAR_AXIS_FORMAT, data, self)
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()
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 )
168class NamedInstance(object):
169 def __init__(self):
170 self.subfamilyNameID = 0
171 self.postscriptNameID = 0xFFFF
172 self.flags = 0
173 self.coordinates = {}
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)
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
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()
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
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