• 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 bytesjoin, safeEval
9from fontTools.ttLib import TTLibError
10from . import DefaultTable
11import struct
12from collections.abc import MutableMapping
13
14
15# Apple's documentation of 'trak':
16# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html
17
18TRAK_HEADER_FORMAT = """
19	> # big endian
20	version:     16.16F
21	format:      H
22	horizOffset: H
23	vertOffset:  H
24	reserved:    H
25"""
26
27TRAK_HEADER_FORMAT_SIZE = sstruct.calcsize(TRAK_HEADER_FORMAT)
28
29
30TRACK_DATA_FORMAT = """
31	> # big endian
32	nTracks:         H
33	nSizes:          H
34	sizeTableOffset: L
35"""
36
37TRACK_DATA_FORMAT_SIZE = sstruct.calcsize(TRACK_DATA_FORMAT)
38
39
40TRACK_TABLE_ENTRY_FORMAT = """
41	> # big endian
42	track:      16.16F
43	nameIndex:       H
44	offset:          H
45"""
46
47TRACK_TABLE_ENTRY_FORMAT_SIZE = sstruct.calcsize(TRACK_TABLE_ENTRY_FORMAT)
48
49
50# size values are actually '16.16F' fixed-point values, but here I do the
51# fixedToFloat conversion manually instead of relying on sstruct
52SIZE_VALUE_FORMAT = ">l"
53SIZE_VALUE_FORMAT_SIZE = struct.calcsize(SIZE_VALUE_FORMAT)
54
55# per-Size values are in 'FUnits', i.e. 16-bit signed integers
56PER_SIZE_VALUE_FORMAT = ">h"
57PER_SIZE_VALUE_FORMAT_SIZE = struct.calcsize(PER_SIZE_VALUE_FORMAT)
58
59
60class table__t_r_a_k(DefaultTable.DefaultTable):
61    dependencies = ["name"]
62
63    def compile(self, ttFont):
64        dataList = []
65        offset = TRAK_HEADER_FORMAT_SIZE
66        for direction in ("horiz", "vert"):
67            trackData = getattr(self, direction + "Data", TrackData())
68            offsetName = direction + "Offset"
69            # set offset to 0 if None or empty
70            if not trackData:
71                setattr(self, offsetName, 0)
72                continue
73            # TrackData table format must be longword aligned
74            alignedOffset = (offset + 3) & ~3
75            padding, offset = b"\x00" * (alignedOffset - offset), alignedOffset
76            setattr(self, offsetName, offset)
77
78            data = trackData.compile(offset)
79            offset += len(data)
80            dataList.append(padding + data)
81
82        self.reserved = 0
83        tableData = bytesjoin([sstruct.pack(TRAK_HEADER_FORMAT, self)] + dataList)
84        return tableData
85
86    def decompile(self, data, ttFont):
87        sstruct.unpack(TRAK_HEADER_FORMAT, data[:TRAK_HEADER_FORMAT_SIZE], self)
88        for direction in ("horiz", "vert"):
89            trackData = TrackData()
90            offset = getattr(self, direction + "Offset")
91            if offset != 0:
92                trackData.decompile(data, offset)
93            setattr(self, direction + "Data", trackData)
94
95    def toXML(self, writer, ttFont):
96        writer.simpletag("version", value=self.version)
97        writer.newline()
98        writer.simpletag("format", value=self.format)
99        writer.newline()
100        for direction in ("horiz", "vert"):
101            dataName = direction + "Data"
102            writer.begintag(dataName)
103            writer.newline()
104            trackData = getattr(self, dataName, TrackData())
105            trackData.toXML(writer, ttFont)
106            writer.endtag(dataName)
107            writer.newline()
108
109    def fromXML(self, name, attrs, content, ttFont):
110        if name == "version":
111            self.version = safeEval(attrs["value"])
112        elif name == "format":
113            self.format = safeEval(attrs["value"])
114        elif name in ("horizData", "vertData"):
115            trackData = TrackData()
116            setattr(self, name, trackData)
117            for element in content:
118                if not isinstance(element, tuple):
119                    continue
120                name, attrs, content_ = element
121                trackData.fromXML(name, attrs, content_, ttFont)
122
123
124class TrackData(MutableMapping):
125    def __init__(self, initialdata={}):
126        self._map = dict(initialdata)
127
128    def compile(self, offset):
129        nTracks = len(self)
130        sizes = self.sizes()
131        nSizes = len(sizes)
132
133        # offset to the start of the size subtable
134        offset += TRACK_DATA_FORMAT_SIZE + TRACK_TABLE_ENTRY_FORMAT_SIZE * nTracks
135        trackDataHeader = sstruct.pack(
136            TRACK_DATA_FORMAT,
137            {"nTracks": nTracks, "nSizes": nSizes, "sizeTableOffset": offset},
138        )
139
140        entryDataList = []
141        perSizeDataList = []
142        # offset to per-size tracking values
143        offset += SIZE_VALUE_FORMAT_SIZE * nSizes
144        # sort track table entries by track value
145        for track, entry in sorted(self.items()):
146            assert entry.nameIndex is not None
147            entry.track = track
148            entry.offset = offset
149            entryDataList += [sstruct.pack(TRACK_TABLE_ENTRY_FORMAT, entry)]
150            # sort per-size values by size
151            for size, value in sorted(entry.items()):
152                perSizeDataList += [struct.pack(PER_SIZE_VALUE_FORMAT, value)]
153            offset += PER_SIZE_VALUE_FORMAT_SIZE * nSizes
154        # sort size values
155        sizeDataList = [
156            struct.pack(SIZE_VALUE_FORMAT, fl2fi(sv, 16)) for sv in sorted(sizes)
157        ]
158
159        data = bytesjoin(
160            [trackDataHeader] + entryDataList + sizeDataList + perSizeDataList
161        )
162        return data
163
164    def decompile(self, data, offset):
165        # initial offset is from the start of trak table to the current TrackData
166        trackDataHeader = data[offset : offset + TRACK_DATA_FORMAT_SIZE]
167        if len(trackDataHeader) != TRACK_DATA_FORMAT_SIZE:
168            raise TTLibError("not enough data to decompile TrackData header")
169        sstruct.unpack(TRACK_DATA_FORMAT, trackDataHeader, self)
170        offset += TRACK_DATA_FORMAT_SIZE
171
172        nSizes = self.nSizes
173        sizeTableOffset = self.sizeTableOffset
174        sizeTable = []
175        for i in range(nSizes):
176            sizeValueData = data[
177                sizeTableOffset : sizeTableOffset + SIZE_VALUE_FORMAT_SIZE
178            ]
179            if len(sizeValueData) < SIZE_VALUE_FORMAT_SIZE:
180                raise TTLibError("not enough data to decompile TrackData size subtable")
181            (sizeValue,) = struct.unpack(SIZE_VALUE_FORMAT, sizeValueData)
182            sizeTable.append(fi2fl(sizeValue, 16))
183            sizeTableOffset += SIZE_VALUE_FORMAT_SIZE
184
185        for i in range(self.nTracks):
186            entry = TrackTableEntry()
187            entryData = data[offset : offset + TRACK_TABLE_ENTRY_FORMAT_SIZE]
188            if len(entryData) < TRACK_TABLE_ENTRY_FORMAT_SIZE:
189                raise TTLibError("not enough data to decompile TrackTableEntry record")
190            sstruct.unpack(TRACK_TABLE_ENTRY_FORMAT, entryData, entry)
191            perSizeOffset = entry.offset
192            for j in range(nSizes):
193                size = sizeTable[j]
194                perSizeValueData = data[
195                    perSizeOffset : perSizeOffset + PER_SIZE_VALUE_FORMAT_SIZE
196                ]
197                if len(perSizeValueData) < PER_SIZE_VALUE_FORMAT_SIZE:
198                    raise TTLibError(
199                        "not enough data to decompile per-size track values"
200                    )
201                (perSizeValue,) = struct.unpack(PER_SIZE_VALUE_FORMAT, perSizeValueData)
202                entry[size] = perSizeValue
203                perSizeOffset += PER_SIZE_VALUE_FORMAT_SIZE
204            self[entry.track] = entry
205            offset += TRACK_TABLE_ENTRY_FORMAT_SIZE
206
207    def toXML(self, writer, ttFont):
208        nTracks = len(self)
209        nSizes = len(self.sizes())
210        writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes))
211        writer.newline()
212        for track, entry in sorted(self.items()):
213            assert entry.nameIndex is not None
214            entry.track = track
215            entry.toXML(writer, ttFont)
216
217    def fromXML(self, name, attrs, content, ttFont):
218        if name != "trackEntry":
219            return
220        entry = TrackTableEntry()
221        entry.fromXML(name, attrs, content, ttFont)
222        self[entry.track] = entry
223
224    def sizes(self):
225        if not self:
226            return frozenset()
227        tracks = list(self.tracks())
228        sizes = self[tracks.pop(0)].sizes()
229        for track in tracks:
230            entrySizes = self[track].sizes()
231            if sizes != entrySizes:
232                raise TTLibError(
233                    "'trak' table entries must specify the same sizes: "
234                    "%s != %s" % (sorted(sizes), sorted(entrySizes))
235                )
236        return frozenset(sizes)
237
238    def __getitem__(self, track):
239        return self._map[track]
240
241    def __delitem__(self, track):
242        del self._map[track]
243
244    def __setitem__(self, track, entry):
245        self._map[track] = entry
246
247    def __len__(self):
248        return len(self._map)
249
250    def __iter__(self):
251        return iter(self._map)
252
253    def keys(self):
254        return self._map.keys()
255
256    tracks = keys
257
258    def __repr__(self):
259        return "TrackData({})".format(self._map if self else "")
260
261
262class TrackTableEntry(MutableMapping):
263    def __init__(self, values={}, nameIndex=None):
264        self.nameIndex = nameIndex
265        self._map = dict(values)
266
267    def toXML(self, writer, ttFont):
268        name = ttFont["name"].getDebugName(self.nameIndex)
269        writer.begintag(
270            "trackEntry",
271            (("value", fl2str(self.track, 16)), ("nameIndex", self.nameIndex)),
272        )
273        writer.newline()
274        if name:
275            writer.comment(name)
276            writer.newline()
277        for size, perSizeValue in sorted(self.items()):
278            writer.simpletag("track", size=fl2str(size, 16), value=perSizeValue)
279            writer.newline()
280        writer.endtag("trackEntry")
281        writer.newline()
282
283    def fromXML(self, name, attrs, content, ttFont):
284        self.track = str2fl(attrs["value"], 16)
285        self.nameIndex = safeEval(attrs["nameIndex"])
286        for element in content:
287            if not isinstance(element, tuple):
288                continue
289            name, attrs, _ = element
290            if name != "track":
291                continue
292            size = str2fl(attrs["size"], 16)
293            self[size] = safeEval(attrs["value"])
294
295    def __getitem__(self, size):
296        return self._map[size]
297
298    def __delitem__(self, size):
299        del self._map[size]
300
301    def __setitem__(self, size, value):
302        self._map[size] = value
303
304    def __len__(self):
305        return len(self._map)
306
307    def __iter__(self):
308        return iter(self._map)
309
310    def keys(self):
311        return self._map.keys()
312
313    sizes = keys
314
315    def __repr__(self):
316        return "TrackTableEntry({}, nameIndex={})".format(self._map, self.nameIndex)
317
318    def __eq__(self, other):
319        if not isinstance(other, self.__class__):
320            return NotImplemented
321        return self.nameIndex == other.nameIndex and dict(self) == dict(other)
322
323    def __ne__(self, other):
324        result = self.__eq__(other)
325        return result if result is NotImplemented else not result
326