333 lines
11 KiB
Python
333 lines
11 KiB
Python
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):
|
|
"""The AAT ``trak`` table can store per-size adjustments to each glyph's
|
|
sidebearings to make when tracking is enabled, which applications can
|
|
use to provide more visually balanced line spacing.
|
|
|
|
See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html
|
|
"""
|
|
|
|
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
|