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