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