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