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