Merge pull request #2285 from fonttools/varStore-32bit

Improve otBase facilities / towards 32bit VariationStore
This commit is contained in:
Just van Rossum 2021-05-08 17:10:17 +02:00 committed by GitHub
commit 31ab3aae0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 320 additions and 109 deletions

View File

@ -106,6 +106,10 @@ class BaseTTXConverter(DefaultTable):
self.table.populateDefaults()
# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
assert len(struct.pack('i', 0)) == 4
assert array.array('i').itemsize == 4, "Oops, file a bug against fonttools."
class OTTableReader(object):
"""Helper class to retrieve data from an OpenType table."""
@ -140,32 +144,43 @@ class OTTableReader(object):
value, = struct.unpack(f">{typecode}", self.data[pos:newpos])
self.pos = newpos
return value
def readUShort(self):
return self.readValue("H", staticSize=2)
def readArray(self, typecode, staticSize, count):
pos = self.pos
newpos = pos + count * staticSize
value = array.array(typecode, self.data[pos:newpos])
if sys.byteorder != "big": value.byteswap()
self.pos = newpos
return value
def readUShortArray(self, count):
return self.readArray("H", staticSize=2, count=count)
return value.tolist()
def readInt8(self):
return self.readValue("b", staticSize=1)
def readInt8Array(self, count):
return self.readArray("b", staticSize=1, count=count)
def readShort(self):
return self.readValue("h", staticSize=2)
def readShortArray(self, count):
return self.readArray("h", staticSize=2, count=count)
def readLong(self):
return self.readValue("l", staticSize=4)
return self.readValue("i", staticSize=4)
def readLongArray(self, count):
return self.readArray("i", staticSize=4, count=count)
def readUInt8(self):
return self.readValue("B", staticSize=1)
def readUInt8Array(self, count):
return self.readArray("B", staticSize=1, count=count)
def readUShort(self):
return self.readValue("H", staticSize=2)
def readUShortArray(self, count):
return self.readArray("H", staticSize=2, count=count)
def readULong(self):
return self.readValue("I", staticSize=4)
def readULongArray(self, count):
return self.readArray("I", staticSize=4, count=count)
def readUInt24(self):
pos = self.pos
@ -173,9 +188,8 @@ class OTTableReader(object):
value, = struct.unpack(">l", b'\0'+self.data[pos:newpos])
self.pos = newpos
return value
def readULong(self):
return self.readValue("L", staticSize=4)
def readUInt24Array(self, count):
return [self.readUInt24() for _ in range(count)]
def readTag(self):
pos = self.pos
@ -419,33 +433,52 @@ class OTTableWriter(object):
def writeValue(self, typecode, value):
self.items.append(struct.pack(f">{typecode}", value))
def writeUShort(self, value):
assert 0 <= value < 0x10000, value
self.items.append(struct.pack(">H", value))
def writeShort(self, value):
assert -32768 <= value < 32768, value
self.items.append(struct.pack(">h", value))
def writeUInt8(self, value):
assert 0 <= value < 256, value
self.items.append(struct.pack(">B", value))
def writeArray(self, typecode, values):
a = array.array(typecode, values)
if sys.byteorder != "big": a.byteswap()
self.items.append(a.tobytes())
def writeInt8(self, value):
assert -128 <= value < 128, value
self.items.append(struct.pack(">b", value))
def writeInt8Array(self, values):
self.writeArray('b', values)
def writeShort(self, value):
assert -32768 <= value < 32768, value
self.items.append(struct.pack(">h", value))
def writeShortArray(self, values):
self.writeArray('h', values)
def writeLong(self, value):
self.items.append(struct.pack(">i", value))
def writeLongArray(self, values):
self.writeArray('i', values)
def writeUInt8(self, value):
assert 0 <= value < 256, value
self.items.append(struct.pack(">B", value))
def writeUInt8Array(self, values):
self.writeArray('B', values)
def writeUShort(self, value):
assert 0 <= value < 0x10000, value
self.items.append(struct.pack(">H", value))
def writeUShortArray(self, values):
self.writeArray('H', values)
def writeULong(self, value):
self.items.append(struct.pack(">I", value))
def writeULongArray(self, values):
self.writeArray('I', values)
def writeUInt24(self, value):
assert 0 <= value < 0x1000000, value
b = struct.pack(">L", value)
self.items.append(b[1:])
def writeLong(self, value):
self.items.append(struct.pack(">l", value))
def writeULong(self, value):
self.items.append(struct.pack(">L", value))
def writeUInt24Array(self, values):
for value in values:
self.writeUInt24(value)
def writeTag(self, tag):
tag = Tag(tag).tobytes()
@ -532,11 +565,11 @@ def packUShort(value):
def packULong(value):
assert 0 <= value < 0x100000000, value
return struct.pack(">L", value)
return struct.pack(">I", value)
def packUInt24(value):
assert 0 <= value < 0x1000000, value
return struct.pack(">L", value)[1:]
return struct.pack(">I", value)[1:]
class BaseTable(object):
@ -698,14 +731,11 @@ class BaseTable(object):
else:
# conv.repeat is a propagated count
writer[conv.repeat].setValue(countValue)
values = value
for i, value in enumerate(values):
try:
conv.write(writer, font, table, value, i)
except Exception as e:
name = value.__class__.__name__ if value is not None else conv.name
e.args = e.args + (name+'['+str(i)+']',)
raise
try:
conv.writeArray(writer, font, table, value)
except Exception as e:
e.args = e.args + (conv.name+'[]',)
raise
elif conv.isCount:
# Special-case Count values.
# Assumption: a Count field will *always* precede

View File

@ -192,8 +192,12 @@ class BaseConverter(object):
raise NotImplementedError(self)
def writeArray(self, writer, font, tableDict, values):
for i, value in enumerate(values):
self.write(writer, font, tableDict, value, i)
try:
for i, value in enumerate(values):
self.write(writer, font, tableDict, value, i)
except Exception as e:
e.args = e.args + (i,)
raise
def write(self, writer, font, tableDict, value, repeatIndex=None):
"""Write a value to the writer."""
@ -230,15 +234,23 @@ class Long(IntValue):
staticSize = 4
def read(self, reader, font, tableDict):
return reader.readLong()
def readArray(self, reader, font, tableDict, count):
return reader.readLongArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeLong(value)
def writeArray(self, writer, font, tableDict, values):
writer.writeLongArray(values)
class ULong(IntValue):
staticSize = 4
def read(self, reader, font, tableDict):
return reader.readULong()
def readArray(self, reader, font, tableDict, count):
return reader.readULongArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeULong(value)
def writeArray(self, writer, font, tableDict, values):
writer.writeULongArray(values)
class Flags32(ULong):
@staticmethod
@ -249,29 +261,45 @@ class Short(IntValue):
staticSize = 2
def read(self, reader, font, tableDict):
return reader.readShort()
def readArray(self, reader, font, tableDict, count):
return reader.readShortArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeShort(value)
def writeArray(self, writer, font, tableDict, values):
writer.writeShortArray(values)
class UShort(IntValue):
staticSize = 2
def read(self, reader, font, tableDict):
return reader.readUShort()
def readArray(self, reader, font, tableDict, count):
return reader.readUShortArray(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUShort(value)
def writeArray(self, writer, font, tableDict, values):
writer.writeUShortArray(values)
class Int8(IntValue):
staticSize = 1
def read(self, reader, font, tableDict):
return reader.readInt8()
def readArray(self, reader, font, tableDict, count):
return reader.readInt8Array(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeInt8(value)
def writeArray(self, writer, font, tableDict, values):
writer.writeInt8Array(values)
class UInt8(IntValue):
staticSize = 1
def read(self, reader, font, tableDict):
return reader.readUInt8()
def readArray(self, reader, font, tableDict, count):
return reader.readUInt8Array(count)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUInt8(value)
def writeArray(self, writer, font, tableDict, values):
writer.writeUInt8Array(values)
class UInt24(IntValue):
staticSize = 3
@ -314,6 +342,14 @@ class GlyphID(SimpleValue):
return l
def read(self, reader, font, tableDict):
return font.getGlyphName(reader.readValue(self.typecode, self.staticSize))
def writeArray(self, writer, font, tableDict, values):
glyphMap = font.getReverseGlyphMap()
try:
values = [glyphMap[glyphname] for glyphname in values]
except KeyError:
# Slower, but will not throw a KeyError on an out-of-range glyph name.
values = [font.getGlyphID(glyphname) for glyphname in values]
writer.writeArray(self.typecode, values)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeValue(self.typecode, font.getGlyphID(value))
@ -1551,20 +1587,15 @@ class VarIdxMapValue(BaseConverter):
outerShift = 16 - innerBits
entrySize = 1 + ((fmt & 0x0030) >> 4)
read = {
1: reader.readUInt8,
2: reader.readUShort,
3: reader.readUInt24,
4: reader.readULong,
readArray = {
1: reader.readUInt8Array,
2: reader.readUShortArray,
3: reader.readUInt24Array,
4: reader.readULongArray,
}[entrySize]
mapping = []
for i in range(nItems):
raw = read()
idx = ((raw & outerMask) << outerShift) | (raw & innerMask)
mapping.append(idx)
return mapping
return [(((raw & outerMask) << outerShift) | (raw & innerMask))
for raw in readArray(nItems)]
def write(self, writer, font, tableDict, value, repeatIndex=None):
fmt = tableDict['EntryFormat']
@ -1576,16 +1607,15 @@ class VarIdxMapValue(BaseConverter):
outerShift = 16 - innerBits
entrySize = 1 + ((fmt & 0x0030) >> 4)
write = {
1: writer.writeUInt8,
2: writer.writeUShort,
3: writer.writeUInt24,
4: writer.writeULong,
writeArray = {
1: writer.writeUInt8Array,
2: writer.writeUShortArray,
3: writer.writeUInt24Array,
4: writer.writeULongArray,
}[entrySize]
for idx in mapping:
raw = ((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask)
write(raw)
writeArray([(((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask))
for idx in mapping])
class VarDataValue(BaseConverter):
@ -1594,27 +1624,43 @@ class VarDataValue(BaseConverter):
values = []
regionCount = tableDict["VarRegionCount"]
shortCount = tableDict["NumShorts"]
wordCount = tableDict["NumShorts"]
for i in range(min(regionCount, shortCount)):
values.append(reader.readShort())
for i in range(min(regionCount, shortCount), regionCount):
values.append(reader.readInt8())
for i in range(regionCount, shortCount):
reader.readInt8()
# https://github.com/fonttools/fonttools/issues/2279
longWords = bool(wordCount & 0x8000)
wordCount = wordCount & 0x7FFF
if longWords:
readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray
else:
readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array
n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
values.extend(readBigArray(n1))
values.extend(readSmallArray(n2 - n1))
if n2 > regionCount: # Padding
del values[regionCount:]
return values
def write(self, writer, font, tableDict, value, repeatIndex=None):
def write(self, writer, font, tableDict, values, repeatIndex=None):
regionCount = tableDict["VarRegionCount"]
shortCount = tableDict["NumShorts"]
wordCount = tableDict["NumShorts"]
for i in range(min(regionCount, shortCount)):
writer.writeShort(value[i])
for i in range(min(regionCount, shortCount), regionCount):
writer.writeInt8(value[i])
for i in range(regionCount, shortCount):
writer.writeInt8(0)
# https://github.com/fonttools/fonttools/issues/2279
longWords = bool(wordCount & 0x8000)
wordCount = wordCount & 0x7FFF
(writeBigArray, writeSmallArray) = {
False: (writer.writeShortArray, writer.writeInt8Array),
True: (writer.writeLongArray, writer.writeShortArray),
}[longWords]
n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
writeBigArray(values[:n1])
writeSmallArray(values[n1:regionCount])
if n2 > regionCount: # Padding
writer.writeSmallArray([0] * (n2 - regionCount))
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)])

View File

@ -26,39 +26,54 @@ def buildVarRegionList(supports, axisTags):
return self
def _reorderItem(lst, narrows, zeroes):
out = []
count = len(lst)
for i in range(count):
if i not in narrows:
out.append(lst[i])
for i in range(count):
if i in narrows and i not in zeroes:
out.append(lst[i])
return out
def _reorderItem(lst, mapping):
return [lst[i] for i in mapping]
def VarData_calculateNumShorts(self, optimize=False):
count = self.VarRegionCount
items = self.Item
narrows = set(range(count))
zeroes = set(range(count))
bit_lengths = [0] * count
for item in items:
wides = [i for i in narrows if not (-128 <= item[i] <= 127)]
narrows.difference_update(wides)
nonzeroes = [i for i in zeroes if item[i]]
zeroes.difference_update(nonzeroes)
if not narrows and not zeroes:
break
# The "+ (i < -1)" magic is to handle two's-compliment.
# That is, we want to get back 7 for -128, whereas
# bit_length() returns 8. Similarly for -65536.
# The reason "i < -1" is used instead of "i < 0" is that
# the latter would make it return 0 for "-1" instead of 1.
bl = [(i + (i < -1)).bit_length() for i in item]
bit_lengths = [max(*pair) for pair in zip(bl, bit_lengths)]
# The addition of 8, instead of seven, is to account for the sign bit.
# This "((b + 8) >> 3) if b else 0" when combined with the above
# "(i + (i < -1)).bit_length()" is a faster way to compute byte-lengths
# conforming to:
#
# byte_length = (0 if i == 0 else
# 1 if -128 <= i < 128 else
# 2 if -65536 <= i < 65536 else
# ...)
byte_lengths = [((b + 8) >> 3) if b else 0 for b in bit_lengths]
# https://github.com/fonttools/fonttools/issues/2279
longWords = any(b > 2 for b in byte_lengths)
if optimize:
# Reorder columns such that all SHORT columns come before UINT8
self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows, zeroes)
# Reorder columns such that wider columns come before narrower columns
mapping = []
mapping.extend(i for i,b in enumerate(byte_lengths) if b > 2)
mapping.extend(i for i,b in enumerate(byte_lengths) if b == 2)
mapping.extend(i for i,b in enumerate(byte_lengths) if b == 1)
byte_lengths = _reorderItem(byte_lengths, mapping)
self.VarRegionIndex = _reorderItem(self.VarRegionIndex, mapping)
self.VarRegionCount = len(self.VarRegionIndex)
for i in range(len(items)):
items[i] = _reorderItem(items[i], narrows, zeroes)
self.NumShorts = count - len(narrows)
items[i] = _reorderItem(items[i], mapping)
if longWords:
self.NumShorts = max((i for i,b in enumerate(byte_lengths) if b > 2), default=-1) + 1
self.NumShorts |= 0x8000
else:
wides = set(range(count)) - narrows
self.NumShorts = 1+max(wides) if wides else 0
self.NumShorts = max((i for i,b in enumerate(byte_lengths) if b > 1), default=-1) + 1
self.VarRegionCount = len(self.VarRegionIndex)
return self

View File

@ -5,7 +5,6 @@ from fontTools.varLib.builder import (buildVarRegionList, buildVarStore,
buildVarRegion, buildVarData)
from functools import partial
from collections import defaultdict
from array import array
def _getLocationKey(loc):
@ -375,12 +374,11 @@ class _Encoding(object):
as a VarData."""
c = 6
while chars:
if chars & 3:
if chars & 0b1111:
c += 2
chars >>= 2
chars >>= 4
return c
def _find_yourself_best_new_encoding(self, done_by_width):
self.best_new_encoding = None
for new_width in range(self.width+1, self.width+self.room+1):
@ -405,14 +403,31 @@ class _EncodingDict(dict):
@staticmethod
def _row_characteristics(row):
"""Returns encoding characteristics for a row."""
longWords = False
chars = 0
i = 1
for v in row:
if v:
chars += i
if not (-128 <= v <= 127):
chars += i * 2
i <<= 2
chars += i * 0b0010
if not (-32768 <= v <= 32767):
longWords = True
break
i <<= 4
if longWords:
# Redo; only allow 2byte/4byte encoding
chars = 0
i = 1
for v in row:
if v:
chars += i * 0b0011
if not (-32768 <= v <= 32767):
chars += i * 0b1100
i <<= 4
return chars
@ -423,7 +438,7 @@ def VarStore_optimize(self):
# Check that no two VarRegions are the same; if they are, fold them.
n = len(self.VarRegionList.Region) # Number of columns
zeroes = array('h', [0]*n)
zeroes = [0] * n
front_mapping = {} # Map from old VarIdxes to full row tuples
@ -435,7 +450,7 @@ def VarStore_optimize(self):
for minor,item in enumerate(data.Item):
row = array('h', zeroes)
row = list(zeroes)
for regionIdx,v in zip(regionIndices, item):
row[regionIdx] += v
row = tuple(row)

View File

@ -8,12 +8,20 @@ import pytest
([0], [[128]], 1),
([0, 1, 2], [[128, 1, 2], [3, -129, 5], [6, 7, 8]], 2),
([0, 1, 2], [[0, 128, 2], [3, 4, 5], [6, 7, -129]], 3),
([0], [[32768]], 0x8001),
([0, 1, 2], [[32768, 1, 2], [3, -129, 5], [6, 7, 8]], 0x8001),
([0, 1, 2], [[32768, 1, 2], [3, -32769, 5], [6, 7, 8]], 0x8002),
([0, 1, 2], [[0, 32768, 2], [3, 4, 5], [6, 7, -32769]], 0x8003),
], ids=[
"0_regions_0_deltas",
"1_region_1_uint8",
"1_region_1_short",
"3_regions_2_shorts_ordered",
"3_regions_2_shorts_unordered",
"1_region_1_long",
"3_regions_1_long_ordered",
"3_regions_2_longs_ordered",
"3_regions_2_longs_unordered",
])
def test_buildVarData_no_optimize(region_indices, items, expected_num_shorts):
data = buildVarData(region_indices, items, optimize=False)
@ -41,6 +49,16 @@ def test_buildVarData_no_optimize(region_indices, items, expected_num_shorts):
[0, 1, 2], [[0, 1, 128], [3, -129, 5], [256, 7, 8]]),
([0, 1, 2], [[0, 128, 2], [0, 4, 5], [0, 7, 8]], 1,
[1, 2], [[128, 2], [4, 5], [7, 8]]),
([0, 1, 2], [[0, 32768, 2], [3, 4, 5], [6, 7, 8]], 0x8001,
[1, 0, 2], [[32768, 0, 2], [4, 3, 5], [7, 6, 8]]),
([0, 1, 2], [[0, 1, 32768], [3, 4, 5], [6, -32769, 8]], 0x8002,
[1, 2, 0], [[1, 32768, 0], [4, 5, 3], [-32769, 8, 6]]),
([0, 1, 2], [[32768, 1, -32769], [3, 4, 5], [6, 7, 8]], 0x8002,
[0, 2, 1], [[32768, -32769, 1], [3, 5, 4], [6, 8, 7]]),
([0, 1, 2], [[0, 1, 32768], [3, -32769, 5], [65536, 7, 8]], 0x8003,
[0, 1, 2], [[0, 1, 32768], [3, -32769, 5], [65536, 7, 8]]),
([0, 1, 2], [[0, 32768, 2], [0, 4, 5], [0, 7, 8]], 0x8001,
[1, 2], [[32768, 2], [4, 5], [7, 8]]),
], ids=[
"0/3_shorts_no_reorder",
"1/3_shorts_reorder",
@ -48,6 +66,11 @@ def test_buildVarData_no_optimize(region_indices, items, expected_num_shorts):
"2/3_shorts_same_row_reorder",
"3/3_shorts_no_reorder",
"1/3_shorts_1/3_zeroes",
"1/3_longs_reorder",
"2/3_longs_reorder",
"2/3_longs_same_row_reorder",
"3/3_longs_no_reorder",
"1/3_longs_1/3_zeroes",
])
def test_buildVarData_optimize(
region_indices, items, expected_num_shorts, expected_regions,

View File

@ -0,0 +1,82 @@
import pytest
from fontTools.varLib.models import VariationModel
from fontTools.varLib.varStore import OnlineVarStoreBuilder, VarStoreInstancer
from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables._f_v_a_r import Axis
from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter
from fontTools.ttLib.tables.otTables import VarStore
@pytest.mark.parametrize(
"locations, masterValues",
[
(
[{}, {"a": 1}],
[
[10, 20],
[100, 2000],
[100, 22000],
],
),
(
[{}, {"a": 1}, {"b": 1}, {"a": 1, "b": 1}],
[
[10, 20, 40, 60],
[100, 2000, 400, 6000],
[7100, 22000, 4000, 30000],
],
),
(
[{}, {"a": 1}],
[
[10, 20],
[42000, 100],
[100, 52000],
],
),
(
[{}, {"a": 1}, {"b": 1}, {"a": 1, "b": 1}],
[
[10, 20, 40, 60],
[40000, 42000, 400, 6000],
[100, 22000, 4000, 173000],
],
),
],
)
def test_onlineVarStoreBuilder(locations, masterValues):
axisTags = sorted({k for loc in locations for k in loc})
model = VariationModel(locations)
builder = OnlineVarStoreBuilder(axisTags)
builder.setModel(model)
varIdxs = []
for masters in masterValues:
_, varIdx = builder.storeMasters(masters)
varIdxs.append(varIdx)
varStore = builder.finish()
mapping = varStore.optimize()
varIdxs = [mapping[varIdx] for varIdx in varIdxs]
dummyFont = TTFont()
writer = OTTableWriter()
varStore.compile(writer, dummyFont)
data = writer.getAllData()
reader = OTTableReader(data)
varStore = VarStore()
varStore.decompile(reader, dummyFont)
fvarAxes = [buildAxis(axisTag) for axisTag in axisTags]
instancer = VarStoreInstancer(varStore, fvarAxes)
for masters, varIdx in zip(masterValues, varIdxs):
base, *rest = masters
for expectedValue, loc in zip(masters, locations):
instancer.setLocation(loc)
value = base + instancer[varIdx]
assert expectedValue == value
def buildAxis(axisTag):
axis = Axis()
axis.axisTag = axisTag
return axis