[TupleVariation] Option to optimize for loading speed, not size

This commit is contained in:
Behdad Esfahbod 2024-09-27 13:26:41 -06:00
parent 18ca57cad4
commit 0213bea88e
3 changed files with 89 additions and 28 deletions

View File

@ -129,7 +129,9 @@ class TupleVariation(object):
else:
log.warning("bad delta format: %s" % ", ".join(sorted(attrs.keys())))
def compile(self, axisTags, sharedCoordIndices={}, pointData=None):
def compile(
self, axisTags, sharedCoordIndices={}, pointData=None, *, optimizeSize=True
):
assert set(self.axes.keys()) <= set(axisTags), (
"Unknown axis tag found.",
self.axes.keys(),
@ -161,7 +163,7 @@ class TupleVariation(object):
flags |= PRIVATE_POINT_NUMBERS
auxData.append(pointData)
auxData.append(self.compileDeltas())
auxData.append(self.compileDeltas(optimizeSize=optimizeSize))
auxData = b"".join(auxData)
tupleData.insert(0, struct.pack(">HH", len(auxData), flags))
@ -322,7 +324,7 @@ class TupleVariation(object):
)
return (result, pos)
def compileDeltas(self):
def compileDeltas(self, optimizeSize=True):
deltaX = []
deltaY = []
if self.getCoordWidth() == 2:
@ -337,12 +339,12 @@ class TupleVariation(object):
continue
deltaX.append(c)
bytearr = bytearray()
self.compileDeltaValues_(deltaX, bytearr)
self.compileDeltaValues_(deltaY, bytearr)
self.compileDeltaValues_(deltaX, bytearr, optimizeSize=optimizeSize)
self.compileDeltaValues_(deltaY, bytearr, optimizeSize=optimizeSize)
return bytearr
@staticmethod
def compileDeltaValues_(deltas, bytearr=None):
def compileDeltaValues_(deltas, bytearr=None, *, optimizeSize=True):
"""[value1, value2, value3, ...] --> bytearray
Emits a sequence of runs. Each run starts with a
@ -360,8 +362,11 @@ class TupleVariation(object):
""" # Explaining the format because the 'gvar' spec is hard to understand.
if bytearr is None:
bytearr = bytearray()
pos = 0
numDeltas = len(deltas)
if optimizeSize:
while pos < numDeltas:
value = deltas[pos]
if value == 0:
@ -372,6 +377,25 @@ class TupleVariation(object):
pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr)
else:
pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr)
else:
minVal, maxVal = min(deltas), max(deltas)
if minVal == 0 == maxVal:
pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr)
elif -128 <= minVal <= maxVal <= 127:
pos = TupleVariation.encodeDeltaRunAsBytes_(
deltas, pos, bytearr, optimizeSize=False
)
elif -32768 <= minVal <= maxVal <= 32767:
pos = TupleVariation.encodeDeltaRunAsWords_(
deltas, pos, bytearr, optimizeSize=False
)
else:
pos = TupleVariation.encodeDeltaRunAsLongs_(
deltas, pos, bytearr, optimizeSize=False
)
assert pos == numDeltas, (pos, numDeltas)
return bytearr
@staticmethod
@ -389,7 +413,7 @@ class TupleVariation(object):
return pos
@staticmethod
def encodeDeltaRunAsBytes_(deltas, offset, bytearr):
def encodeDeltaRunAsBytes_(deltas, offset, bytearr, optimizeSize=True):
pos = offset
numDeltas = len(deltas)
while pos < numDeltas:
@ -404,7 +428,12 @@ class TupleVariation(object):
# (04 0F 0F 00 0F 0F) when storing the zero value
# literally, but 7 bytes (01 0F 0F 80 01 0F 0F)
# when starting a new run.
if value == 0 and pos + 1 < numDeltas and deltas[pos + 1] == 0:
if (
optimizeSize
and value == 0
and pos + 1 < numDeltas
and deltas[pos + 1] == 0
):
break
pos += 1
runLength = pos - offset
@ -419,7 +448,7 @@ class TupleVariation(object):
return pos
@staticmethod
def encodeDeltaRunAsWords_(deltas, offset, bytearr):
def encodeDeltaRunAsWords_(deltas, offset, bytearr, optimizeSize=True):
pos = offset
numDeltas = len(deltas)
while pos < numDeltas:
@ -432,7 +461,7 @@ class TupleVariation(object):
# storing the zero literally (42 66 66 00 00 77 77),
# and equally 7 bytes when starting a new run
# (40 66 66 80 40 77 77).
if value == 0:
if optimizeSize and value == 0:
break
# Within a word-encoded run of deltas, a single value
@ -442,7 +471,8 @@ class TupleVariation(object):
# the value literally (42 66 66 00 02 77 77), but 8 bytes
# when starting a new run (40 66 66 00 02 40 77 77).
if (
(-128 <= value <= 127)
optimizeSize
and (-128 <= value <= 127)
and pos + 1 < numDeltas
and (-128 <= deltas[pos + 1] <= 127)
):
@ -470,12 +500,12 @@ class TupleVariation(object):
return pos
@staticmethod
def encodeDeltaRunAsLongs_(deltas, offset, bytearr):
def encodeDeltaRunAsLongs_(deltas, offset, bytearr, optimizeSize=True):
pos = offset
numDeltas = len(deltas)
while pos < numDeltas:
value = deltas[pos]
if -32768 <= value <= 32767:
if optimizeSize and -32768 <= value <= 32767:
break
pos += 1
runLength = pos - offset
@ -677,7 +707,13 @@ def compileSharedTuples(
def compileTupleVariationStore(
variations, pointCount, axisTags, sharedTupleIndices, useSharedPoints=True
variations,
pointCount,
axisTags,
sharedTupleIndices,
useSharedPoints=True,
*,
optimizeSize=True,
):
# pointCount is actually unused. Keeping for API compat.
del pointCount
@ -733,7 +769,9 @@ def compileTupleVariationStore(
]
for v, p in zip(variations, pointDatas):
thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p)
thisTuple, thisData = v.compile(
axisTags, sharedTupleIndices, pointData=p, optimizeSize=optimizeSize
)
tuples.append(thisTuple)
data.append(thisData)

View File

@ -85,6 +85,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices):
result = []
glyf = ttFont["glyf"]
optimizeSize = getattr(self, "optimizeSize", True)
for glyphName in ttFont.getGlyphOrder():
variations = self.variations.get(glyphName, [])
if not variations:
@ -93,7 +94,11 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
pointCountUnused = 0 # pointCount is actually unused by compileGlyph
result.append(
compileGlyph_(
variations, pointCountUnused, axisTags, sharedCoordIndices
variations,
pointCountUnused,
axisTags,
sharedCoordIndices,
optimizeSize=optimizeSize,
)
)
return result
@ -248,9 +253,11 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS
def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices):
def compileGlyph_(
variations, pointCount, axisTags, sharedCoordIndices, *, optimizeSize=True
):
tupleVariationCount, tuples, data = tv.compileTupleVariationStore(
variations, pointCount, axisTags, sharedCoordIndices
variations, pointCount, axisTags, sharedCoordIndices, optimizeSize=optimizeSize
)
if tupleVariationCount == 0:
return b""

View File

@ -594,8 +594,8 @@ class TupleVariationTest(unittest.TestCase):
self.assertEqual("02 01 02 04", hexencode(var.compileDeltas()))
def test_compileDeltaValues(self):
compileDeltaValues = lambda values: hexencode(
TupleVariation.compileDeltaValues_(values)
compileDeltaValues = lambda values, optimizeSize=True: hexencode(
TupleVariation.compileDeltaValues_(values, optimizeSize=optimizeSize)
)
# zeroes
self.assertEqual("80", compileDeltaValues([0]))
@ -635,18 +635,34 @@ class TupleVariationTest(unittest.TestCase):
)
# bytes, zeroes
self.assertEqual("01 01 00", compileDeltaValues([1, 0]))
self.assertEqual("01 01 00", compileDeltaValues([1, 0], optimizeSize=False))
self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0]))
self.assertEqual(
"02 01 00 00", compileDeltaValues([1, 0, 0], optimizeSize=False)
)
# words, bytes, words: a single byte is more compact when encoded as part of the words run
self.assertEqual(
"42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777])
)
self.assertEqual(
"42 66 66 00 02 77 77",
compileDeltaValues([0x6666, 2, 0x7777], optimizeSize=False),
)
self.assertEqual(
"40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777])
)
self.assertEqual(
"43 66 66 00 02 00 02 77 77",
compileDeltaValues([0x6666, 2, 2, 0x7777], optimizeSize=False),
)
# words, zeroes, words
self.assertEqual(
"40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777])
)
self.assertEqual(
"42 66 66 00 00 77 77",
compileDeltaValues([0x6666, 0, 0x7777], optimizeSize=False),
)
self.assertEqual(
"40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777])
)