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

View File

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

View File

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