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)
[docs]
class table__t_r_a_k(DefaultTable.DefaultTable):
dependencies = ["name"]
[docs]
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
[docs]
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)
[docs]
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()
[docs]
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)
[docs]
class TrackData(MutableMapping):
def __init__(self, initialdata={}):
self._map = dict(initialdata)
[docs]
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
[docs]
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
[docs]
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)
[docs]
def fromXML(self, name, attrs, content, ttFont):
if name != "trackEntry":
return
entry = TrackTableEntry()
entry.fromXML(name, attrs, content, ttFont)
self[entry.track] = entry
[docs]
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)
[docs]
def keys(self):
return self._map.keys()
tracks = keys
def __repr__(self):
return "TrackData({})".format(self._map if self else "")
[docs]
class TrackTableEntry(MutableMapping):
def __init__(self, values={}, nameIndex=None):
self.nameIndex = nameIndex
self._map = dict(values)
[docs]
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()
[docs]
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)
[docs]
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