1from __future__ import print_function, division, absolute_import 2from fontTools.misc.py23 import * 3from fontTools.misc import sstruct 4from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi 5from fontTools.misc.textTools import safeEval 6from fontTools.ttLib import TTLibError 7from . import DefaultTable 8import struct 9try: 10 from collections.abc import MutableMapping 11except ImportError: 12 from UserDict import DictMixin as 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 126 def __init__(self, initialdata={}): 127 self._map = dict(initialdata) 128 129 def compile(self, offset): 130 nTracks = len(self) 131 sizes = self.sizes() 132 nSizes = len(sizes) 133 134 # offset to the start of the size subtable 135 offset += TRACK_DATA_FORMAT_SIZE + TRACK_TABLE_ENTRY_FORMAT_SIZE*nTracks 136 trackDataHeader = sstruct.pack( 137 TRACK_DATA_FORMAT, 138 {'nTracks': nTracks, 'nSizes': nSizes, 'sizeTableOffset': offset}) 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 = [struct.pack(SIZE_VALUE_FORMAT, fl2fi(sv, 16)) for sv in sorted(sizes)] 156 157 data = bytesjoin([trackDataHeader] + entryDataList + sizeDataList + perSizeDataList) 158 return data 159 160 def decompile(self, data, offset): 161 # initial offset is from the start of trak table to the current TrackData 162 trackDataHeader = data[offset:offset+TRACK_DATA_FORMAT_SIZE] 163 if len(trackDataHeader) != TRACK_DATA_FORMAT_SIZE: 164 raise TTLibError('not enough data to decompile TrackData header') 165 sstruct.unpack(TRACK_DATA_FORMAT, trackDataHeader, self) 166 offset += TRACK_DATA_FORMAT_SIZE 167 168 nSizes = self.nSizes 169 sizeTableOffset = self.sizeTableOffset 170 sizeTable = [] 171 for i in range(nSizes): 172 sizeValueData = data[sizeTableOffset:sizeTableOffset+SIZE_VALUE_FORMAT_SIZE] 173 if len(sizeValueData) < SIZE_VALUE_FORMAT_SIZE: 174 raise TTLibError('not enough data to decompile TrackData size subtable') 175 sizeValue, = struct.unpack(SIZE_VALUE_FORMAT, sizeValueData) 176 sizeTable.append(fi2fl(sizeValue, 16)) 177 sizeTableOffset += SIZE_VALUE_FORMAT_SIZE 178 179 for i in range(self.nTracks): 180 entry = TrackTableEntry() 181 entryData = data[offset:offset+TRACK_TABLE_ENTRY_FORMAT_SIZE] 182 if len(entryData) < TRACK_TABLE_ENTRY_FORMAT_SIZE: 183 raise TTLibError('not enough data to decompile TrackTableEntry record') 184 sstruct.unpack(TRACK_TABLE_ENTRY_FORMAT, entryData, entry) 185 perSizeOffset = entry.offset 186 for j in range(nSizes): 187 size = sizeTable[j] 188 perSizeValueData = data[perSizeOffset:perSizeOffset+PER_SIZE_VALUE_FORMAT_SIZE] 189 if len(perSizeValueData) < PER_SIZE_VALUE_FORMAT_SIZE: 190 raise TTLibError('not enough data to decompile per-size track values') 191 perSizeValue, = struct.unpack(PER_SIZE_VALUE_FORMAT, perSizeValueData) 192 entry[size] = perSizeValue 193 perSizeOffset += PER_SIZE_VALUE_FORMAT_SIZE 194 self[entry.track] = entry 195 offset += TRACK_TABLE_ENTRY_FORMAT_SIZE 196 197 def toXML(self, writer, ttFont): 198 nTracks = len(self) 199 nSizes = len(self.sizes()) 200 writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes)) 201 writer.newline() 202 for track, entry in sorted(self.items()): 203 assert entry.nameIndex is not None 204 entry.track = track 205 entry.toXML(writer, ttFont) 206 207 def fromXML(self, name, attrs, content, ttFont): 208 if name != 'trackEntry': 209 return 210 entry = TrackTableEntry() 211 entry.fromXML(name, attrs, content, ttFont) 212 self[entry.track] = entry 213 214 def sizes(self): 215 if not self: 216 return frozenset() 217 tracks = list(self.tracks()) 218 sizes = self[tracks.pop(0)].sizes() 219 for track in tracks: 220 entrySizes = self[track].sizes() 221 if sizes != entrySizes: 222 raise TTLibError( 223 "'trak' table entries must specify the same sizes: " 224 "%s != %s" % (sorted(sizes), sorted(entrySizes))) 225 return frozenset(sizes) 226 227 def __getitem__(self, track): 228 return self._map[track] 229 230 def __delitem__(self, track): 231 del self._map[track] 232 233 def __setitem__(self, track, entry): 234 self._map[track] = entry 235 236 def __len__(self): 237 return len(self._map) 238 239 def __iter__(self): 240 return iter(self._map) 241 242 def keys(self): 243 return self._map.keys() 244 245 tracks = keys 246 247 def __repr__(self): 248 return "TrackData({})".format(self._map if self else "") 249 250 251class TrackTableEntry(MutableMapping): 252 253 def __init__(self, values={}, nameIndex=None): 254 self.nameIndex = nameIndex 255 self._map = dict(values) 256 257 def toXML(self, writer, ttFont): 258 name = ttFont["name"].getDebugName(self.nameIndex) 259 writer.begintag( 260 "trackEntry", 261 (('value', self.track), ('nameIndex', self.nameIndex))) 262 writer.newline() 263 if name: 264 writer.comment(name) 265 writer.newline() 266 for size, perSizeValue in sorted(self.items()): 267 writer.simpletag("track", size=size, value=perSizeValue) 268 writer.newline() 269 writer.endtag("trackEntry") 270 writer.newline() 271 272 def fromXML(self, name, attrs, content, ttFont): 273 self.track = safeEval(attrs['value']) 274 self.nameIndex = safeEval(attrs['nameIndex']) 275 for element in content: 276 if not isinstance(element, tuple): 277 continue 278 name, attrs, _ = element 279 if name != 'track': 280 continue 281 size = safeEval(attrs['size']) 282 self[size] = safeEval(attrs['value']) 283 284 def __getitem__(self, size): 285 return self._map[size] 286 287 def __delitem__(self, size): 288 del self._map[size] 289 290 def __setitem__(self, size, value): 291 self._map[size] = value 292 293 def __len__(self): 294 return len(self._map) 295 296 def __iter__(self): 297 return iter(self._map) 298 299 def keys(self): 300 return self._map.keys() 301 302 sizes = keys 303 304 def __repr__(self): 305 return "TrackTableEntry({}, nameIndex={})".format(self._map, self.nameIndex) 306 307 def __eq__(self, other): 308 if not isinstance(other, self.__class__): 309 return NotImplemented 310 return self.nameIndex == other.nameIndex and dict(self) == dict(other) 311 312 def __ne__(self, other): 313 result = self.__eq__(other) 314 return result if result is NotImplemented else not result 315