[_k_e_r_n] fix compilation of AAT kern tables

- When compiling kern subtables for version=1.0 kern tables (AAT)
  the subtable header was written incorrectly: there is no version,
  the length is a uint32 and there's an additional uint16 for
  tupleIndex
- Use the 'coverage' low byte to select subtable "format", instead
  of the 'version' field, only present in OT kern subtable header.
  The getkern method was failing with AttributeError on 'unknown'
  subtable formats, as their class only has 'format' instead of
  'version' attribute. The 'version' attribute is renamed to
  'format' also to avoid confusion, but the old one is kept for
  backward compatiblity. In the only implemeted subtable class,
  'format' becomes a class attribute rather than instance's
  (it must always be 0).
- KernTable_format_0 now takes an 'apple=False' argument, used to
  know the different headers and whether to read/write tupleIndex.
- minor pep8 whitespace and indentation fixes
- A new 'tupleIndex' attribute is written out to TTX for apple
  kern subtables. Old ttx files which lack that attribute will
  still be read (with a warning) and will default to tupleIndex=0
  when recompiled or dumped with current fonttools.

Fixes #1089
This commit is contained in:
Cosimo Lupo 2017-11-03 16:11:36 +00:00
parent 5f2177a990
commit 69d3bfadcc

View File

@ -17,7 +17,7 @@ class table__k_e_r_n(DefaultTable.DefaultTable):
def getkern(self, format): def getkern(self, format):
for subtable in self.kernTables: for subtable in self.kernTables:
if subtable.version == format: if subtable.format == format:
return subtable return subtable
return None # not found return None # not found
@ -33,21 +33,23 @@ class table__k_e_r_n(DefaultTable.DefaultTable):
else: else:
self.version = version self.version = version
data = data[4:] data = data[4:]
tablesIndex = []
self.kernTables = [] self.kernTables = []
for i in range(nTables): for i in range(nTables):
if self.version == 1.0: if self.version == 1.0:
# Apple # Apple
length, coverage, tupleIndex = struct.unpack(">lHH", data[:8]) length, coverage = struct.unpack(">LH", data[:6])
version = coverage & 0xff
else: else:
version, length = struct.unpack(">HH", data[:4]) # in OpenType spec the "version" field refers to the common
length = int(length) # subtable header; the actual subtable format is stored in
if version not in kern_classes: # the last 8 mask bits of "coverage" field.
subtable = KernTable_format_unkown(version) # Since this "version" is always 0 (and is not present in the
# later AAT extensions), we simply ignore it here
_, length, coverage = struct.unpack(">HHH", data[:6])
subtableFormat = coverage & 0xff
if subtableFormat not in kern_classes:
subtable = KernTable_format_unkown(subtableFormat)
else: else:
subtable = kern_classes[version]() subtable = kern_classes[subtableFormat](apple)
subtable.apple = apple
subtable.decompile(data[:length], ttFont) subtable.decompile(data[:length], ttFont)
self.kernTables.append(subtable) self.kernTables.append(subtable)
data = data[length:] data = data[length:]
@ -59,7 +61,7 @@ class table__k_e_r_n(DefaultTable.DefaultTable):
nTables = 0 nTables = 0
if self.version == 1.0: if self.version == 1.0:
# AAT Apple's "new" format. # AAT Apple's "new" format.
data = struct.pack(">ll", fl2fi(self.version, 16), nTables) data = struct.pack(">LL", fl2fi(self.version, 16), nTables)
else: else:
data = struct.pack(">HH", self.version, nTables) data = struct.pack(">HH", self.version, nTables)
if hasattr(self, "kernTables"): if hasattr(self, "kernTables"):
@ -85,26 +87,41 @@ class table__k_e_r_n(DefaultTable.DefaultTable):
if format not in kern_classes: if format not in kern_classes:
subtable = KernTable_format_unkown(format) subtable = KernTable_format_unkown(format)
else: else:
subtable = kern_classes[format]() apple = self.version == 1.0
subtable = kern_classes[format](apple)
self.kernTables.append(subtable) self.kernTables.append(subtable)
subtable.fromXML(name, attrs, content, ttFont) subtable.fromXML(name, attrs, content, ttFont)
class KernTable_format_0(object): class KernTable_format_0(object):
# 'version' is kept for backward compatibility
version = format = 0
def __init__(self, apple=False):
self.apple = apple
def decompile(self, data, ttFont): def decompile(self, data, ttFont):
version, length, coverage = (0,0,0)
if not self.apple: if not self.apple:
version, length, coverage = struct.unpack(">HHH", data[:6]) version, length, coverage = struct.unpack(">HHH", data[:6])
if version != 0:
from fontTools.ttLib import TTLibError
raise TTLibError(
"unsupported kern subtable version: %d" % version)
tupleIndex = None
# Should we also assert length == len(data)?
data = data[6:] data = data[6:]
else: else:
version, length, coverage = struct.unpack(">LHH", data[:8]) length, coverage, tupleIndex = struct.unpack(">LHH", data[:8])
data = data[8:] data = data[8:]
self.version, self.coverage = int(version), int(coverage) assert self.format == coverage & 0xFF, "unsupported format"
self.coverage = coverage
self.tupleIndex = tupleIndex
self.kernTable = kernTable = {} self.kernTable = kernTable = {}
nPairs, searchRange, entrySelector, rangeShift = struct.unpack(">HHHH", data[:8]) nPairs, searchRange, entrySelector, rangeShift = struct.unpack(
">HHHH", data[:8])
data = data[8:] data = data[8:]
nPairs = min(nPairs, len(data) // 6) nPairs = min(nPairs, len(data) // 6)
@ -115,35 +132,69 @@ class KernTable_format_0(object):
glyphOrder = ttFont.getGlyphOrder() glyphOrder = ttFont.getGlyphOrder()
for k in range(nPairs): for k in range(nPairs):
left, right, value = next(it), next(it), next(it) left, right, value = next(it), next(it), next(it)
if value >= 32768: value -= 65536 if value >= 32768:
value -= 65536
try: try:
kernTable[(glyphOrder[left], glyphOrder[right])] = value kernTable[(glyphOrder[left], glyphOrder[right])] = value
except IndexError: except IndexError:
# Slower, but will not throw an IndexError on an invalid glyph id. # Slower, but will not throw an IndexError on an invalid
kernTable[(ttFont.getGlyphName(left), ttFont.getGlyphName(right))] = value # glyph id.
kernTable[(
ttFont.getGlyphName(left),
ttFont.getGlyphName(right))] = value
if len(data) > 6 * nPairs + 4: # Ignore up to 4 bytes excess if len(data) > 6 * nPairs + 4: # Ignore up to 4 bytes excess
log.warning("excess data in 'kern' subtable: %d bytes", len(data) - 6 * nPairs) log.warning(
"excess data in 'kern' subtable: %d bytes",
len(data) - 6 * nPairs)
def compile(self, ttFont): def compile(self, ttFont):
nPairs = len(self.kernTable) nPairs = len(self.kernTable)
searchRange, entrySelector, rangeShift = getSearchRange(nPairs, 6) searchRange, entrySelector, rangeShift = getSearchRange(nPairs, 6)
data = struct.pack(">HHHH", nPairs, searchRange, entrySelector, rangeShift) data = struct.pack(
">HHHH", nPairs, searchRange, entrySelector, rangeShift)
# yeehee! (I mean, turn names into indices) # yeehee! (I mean, turn names into indices)
try: try:
reverseOrder = ttFont.getReverseGlyphMap() reverseOrder = ttFont.getReverseGlyphMap()
kernTable = sorted((reverseOrder[left], reverseOrder[right], value) for ((left,right),value) in self.kernTable.items()) kernTable = sorted(
(reverseOrder[left], reverseOrder[right], value)
for ((left, right), value) in self.kernTable.items())
except KeyError: except KeyError:
# Slower, but will not throw KeyError on invalid glyph id. # Slower, but will not throw KeyError on invalid glyph id.
getGlyphID = ttFont.getGlyphID getGlyphID = ttFont.getGlyphID
kernTable = sorted((getGlyphID(left), getGlyphID(right), value) for ((left,right),value) in self.kernTable.items()) kernTable = sorted(
(getGlyphID(left), getGlyphID(right), value)
for ((left, right), value) in self.kernTable.items())
for left, right, value in kernTable: for left, right, value in kernTable:
data = data + struct.pack(">HHh", left, right, value) data = data + struct.pack(">HHh", left, right, value)
return struct.pack(">HHH", self.version, len(data) + 6, self.coverage) + data # ensure mask bits 8-15 (subtable format) are set to 0
self.coverage &= ~0xFF
if not self.apple:
version = 0
length = len(data) + 6
header = struct.pack(">HHH", version, length, self.coverage)
else:
if self.tupleIndex is None:
# sensible default when compiling a TTX from an old fonttools
# or when inserting a Windows-style format 0 subtable into an
# Apple version=1.0 kern table
log.warning("'tupleIndex' is None; default to 0")
self.tupleIndex = 0
length = len(data) + 8
header = struct.pack(">LHH", length, self.coverage, self.tupleIndex)
return header + data
def toXML(self, writer, ttFont): def toXML(self, writer, ttFont):
writer.begintag("kernsubtable", coverage=self.coverage, format=0) attrs = dict(coverage=self.coverage, format=self.format)
if self.apple:
if self.tupleIndex is None:
log.warning("'tupleIndex' is None; default to 0")
attrs["tupleIndex"] = 0
else:
attrs["tupleIndex"] = self.tupleIndex
writer.begintag("kernsubtable", **attrs)
writer.newline() writer.newline()
items = sorted(self.kernTable.items()) items = sorted(self.kernTable.items())
for (left, right), value in items: for (left, right), value in items:
@ -158,7 +209,18 @@ class KernTable_format_0(object):
def fromXML(self, name, attrs, content, ttFont): def fromXML(self, name, attrs, content, ttFont):
self.coverage = safeEval(attrs["coverage"]) self.coverage = safeEval(attrs["coverage"])
self.version = safeEval(attrs["format"]) subtableFormat = safeEval(attrs["format"])
if self.apple:
if "tupleIndex" in attrs:
self.tupleIndex = safeEval(attrs["tupleIndex"])
else:
# previous fontTools versions didn't export tupleIndex
log.warning(
"Apple kern subtable is missing 'tupleIndex' attribute")
self.tupleIndex = None
else:
self.tupleIndex = None
assert subtableFormat == self.format, "unsupported format"
if not hasattr(self, "kernTable"): if not hasattr(self, "kernTable"):
self.kernTable = {} self.kernTable = {}
for element in content: for element in content: