• 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
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