from fontTools.misc import sstruct from fontTools.misc.fixedTools import ( fixedToFloat as fi2fl, floatToFixed as fl2fi, floatToFixedToStr as fl2str, strToFixedToFloat as str2fl, ) from fontTools.misc.textTools import bytesjoin, safeEval from fontTools.ttLib import TTLibError from . import DefaultTable import struct from collections.abc import MutableMapping # Apple's documentation of 'trak': # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html TRAK_HEADER_FORMAT = """ > # big endian version: 16.16F format: H horizOffset: H vertOffset: H reserved: H """ TRAK_HEADER_FORMAT_SIZE = sstruct.calcsize(TRAK_HEADER_FORMAT) TRACK_DATA_FORMAT = """ > # big endian nTracks: H nSizes: H sizeTableOffset: L """ TRACK_DATA_FORMAT_SIZE = sstruct.calcsize(TRACK_DATA_FORMAT) TRACK_TABLE_ENTRY_FORMAT = """ > # big endian track: 16.16F nameIndex: H offset: H """ TRACK_TABLE_ENTRY_FORMAT_SIZE = sstruct.calcsize(TRACK_TABLE_ENTRY_FORMAT) # size values are actually '16.16F' fixed-point values, but here I do the # fixedToFloat conversion manually instead of relying on sstruct SIZE_VALUE_FORMAT = ">l" SIZE_VALUE_FORMAT_SIZE = struct.calcsize(SIZE_VALUE_FORMAT) # per-Size values are in 'FUnits', i.e. 16-bit signed integers PER_SIZE_VALUE_FORMAT = ">h" PER_SIZE_VALUE_FORMAT_SIZE = struct.calcsize(PER_SIZE_VALUE_FORMAT) class table__t_r_a_k(DefaultTable.DefaultTable): dependencies = ['name'] def compile(self, ttFont): dataList = [] offset = TRAK_HEADER_FORMAT_SIZE for direction in ('horiz', 'vert'): trackData = getattr(self, direction + 'Data', TrackData()) offsetName = direction + 'Offset' # set offset to 0 if None or empty if not trackData: setattr(self, offsetName, 0) continue # TrackData table format must be longword aligned alignedOffset = (offset + 3) & ~3 padding, offset = b"\x00"*(alignedOffset - offset), alignedOffset setattr(self, offsetName, offset) data = trackData.compile(offset) offset += len(data) dataList.append(padding + data) self.reserved = 0 tableData = bytesjoin([sstruct.pack(TRAK_HEADER_FORMAT, self)] + dataList) return tableData def decompile(self, data, ttFont): sstruct.unpack(TRAK_HEADER_FORMAT, data[:TRAK_HEADER_FORMAT_SIZE], self) for direction in ('horiz', 'vert'): trackData = TrackData() offset = getattr(self, direction + 'Offset') if offset != 0: trackData.decompile(data, offset) setattr(self, direction + 'Data', trackData) def toXML(self, writer, ttFont): writer.simpletag('version', value=self.version) writer.newline() writer.simpletag('format', value=self.format) writer.newline() for direction in ('horiz', 'vert'): dataName = direction + 'Data' writer.begintag(dataName) writer.newline() trackData = getattr(self, dataName, TrackData()) trackData.toXML(writer, ttFont) writer.endtag(dataName) writer.newline() def fromXML(self, name, attrs, content, ttFont): if name == 'version': self.version = safeEval(attrs['value']) elif name == 'format': self.format = safeEval(attrs['value']) elif name in ('horizData', 'vertData'): trackData = TrackData() setattr(self, name, trackData) for element in content: if not isinstance(element, tuple): continue name, attrs, content_ = element trackData.fromXML(name, attrs, content_, ttFont) class TrackData(MutableMapping): def __init__(self, initialdata={}): self._map = dict(initialdata) def compile(self, offset): nTracks = len(self) sizes = self.sizes() nSizes = len(sizes) # offset to the start of the size subtable offset += TRACK_DATA_FORMAT_SIZE + TRACK_TABLE_ENTRY_FORMAT_SIZE*nTracks trackDataHeader = sstruct.pack( TRACK_DATA_FORMAT, {'nTracks': nTracks, 'nSizes': nSizes, 'sizeTableOffset': offset}) entryDataList = [] perSizeDataList = [] # offset to per-size tracking values offset += SIZE_VALUE_FORMAT_SIZE*nSizes # sort track table entries by track value for track, entry in sorted(self.items()): assert entry.nameIndex is not None entry.track = track entry.offset = offset entryDataList += [sstruct.pack(TRACK_TABLE_ENTRY_FORMAT, entry)] # sort per-size values by size for size, value in sorted(entry.items()): perSizeDataList += [struct.pack(PER_SIZE_VALUE_FORMAT, value)] offset += PER_SIZE_VALUE_FORMAT_SIZE*nSizes # sort size values sizeDataList = [struct.pack(SIZE_VALUE_FORMAT, fl2fi(sv, 16)) for sv in sorted(sizes)] data = bytesjoin([trackDataHeader] + entryDataList + sizeDataList + perSizeDataList) return data def decompile(self, data, offset): # initial offset is from the start of trak table to the current TrackData trackDataHeader = data[offset:offset+TRACK_DATA_FORMAT_SIZE] if len(trackDataHeader) != TRACK_DATA_FORMAT_SIZE: raise TTLibError('not enough data to decompile TrackData header') sstruct.unpack(TRACK_DATA_FORMAT, trackDataHeader, self) offset += TRACK_DATA_FORMAT_SIZE nSizes = self.nSizes sizeTableOffset = self.sizeTableOffset sizeTable = [] for i in range(nSizes): sizeValueData = data[sizeTableOffset:sizeTableOffset+SIZE_VALUE_FORMAT_SIZE] if len(sizeValueData) < SIZE_VALUE_FORMAT_SIZE: raise TTLibError('not enough data to decompile TrackData size subtable') sizeValue, = struct.unpack(SIZE_VALUE_FORMAT, sizeValueData) sizeTable.append(fi2fl(sizeValue, 16)) sizeTableOffset += SIZE_VALUE_FORMAT_SIZE for i in range(self.nTracks): entry = TrackTableEntry() entryData = data[offset:offset+TRACK_TABLE_ENTRY_FORMAT_SIZE] if len(entryData) < TRACK_TABLE_ENTRY_FORMAT_SIZE: raise TTLibError('not enough data to decompile TrackTableEntry record') sstruct.unpack(TRACK_TABLE_ENTRY_FORMAT, entryData, entry) perSizeOffset = entry.offset for j in range(nSizes): size = sizeTable[j] perSizeValueData = data[perSizeOffset:perSizeOffset+PER_SIZE_VALUE_FORMAT_SIZE] if len(perSizeValueData) < PER_SIZE_VALUE_FORMAT_SIZE: raise TTLibError('not enough data to decompile per-size track values') perSizeValue, = struct.unpack(PER_SIZE_VALUE_FORMAT, perSizeValueData) entry[size] = perSizeValue perSizeOffset += PER_SIZE_VALUE_FORMAT_SIZE self[entry.track] = entry offset += TRACK_TABLE_ENTRY_FORMAT_SIZE def toXML(self, writer, ttFont): nTracks = len(self) nSizes = len(self.sizes()) writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes)) writer.newline() for track, entry in sorted(self.items()): assert entry.nameIndex is not None entry.track = track entry.toXML(writer, ttFont) def fromXML(self, name, attrs, content, ttFont): if name != 'trackEntry': return entry = TrackTableEntry() entry.fromXML(name, attrs, content, ttFont) self[entry.track] = entry def sizes(self): if not self: return frozenset() tracks = list(self.tracks()) sizes = self[tracks.pop(0)].sizes() for track in tracks: entrySizes = self[track].sizes() if sizes != entrySizes: raise TTLibError( "'trak' table entries must specify the same sizes: " "%s != %s" % (sorted(sizes), sorted(entrySizes))) return frozenset(sizes) def __getitem__(self, track): return self._map[track] def __delitem__(self, track): del self._map[track] def __setitem__(self, track, entry): self._map[track] = entry def __len__(self): return len(self._map) def __iter__(self): return iter(self._map) def keys(self): return self._map.keys() tracks = keys def __repr__(self): return "TrackData({})".format(self._map if self else "") class TrackTableEntry(MutableMapping): def __init__(self, values={}, nameIndex=None): self.nameIndex = nameIndex self._map = dict(values) def toXML(self, writer, ttFont): name = ttFont["name"].getDebugName(self.nameIndex) writer.begintag( "trackEntry", (('value', fl2str(self.track, 16)), ('nameIndex', self.nameIndex))) writer.newline() if name: writer.comment(name) writer.newline() for size, perSizeValue in sorted(self.items()): writer.simpletag("track", size=fl2str(size, 16), value=perSizeValue) writer.newline() writer.endtag("trackEntry") writer.newline() def fromXML(self, name, attrs, content, ttFont): self.track = str2fl(attrs['value'], 16) self.nameIndex = safeEval(attrs['nameIndex']) for element in content: if not isinstance(element, tuple): continue name, attrs, _ = element if name != 'track': continue size = str2fl(attrs['size'], 16) self[size] = safeEval(attrs['value']) def __getitem__(self, size): return self._map[size] def __delitem__(self, size): del self._map[size] def __setitem__(self, size, value): self._map[size] = value def __len__(self): return len(self._map) def __iter__(self): return iter(self._map) def keys(self): return self._map.keys() sizes = keys def __repr__(self): return "TrackTableEntry({}, nameIndex={})".format(self._map, self.nameIndex) def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented return self.nameIndex == other.nameIndex and dict(self) == dict(other) def __ne__(self, other): result = self.__eq__(other) return result if result is NotImplemented else not result