• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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