diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py index a98bca2e0..bd6217e2e 100644 --- a/Lib/fontTools/ttLib/tables/TupleVariation.py +++ b/Lib/fontTools/ttLib/tables/TupleVariation.py @@ -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,18 +362,40 @@ 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) - while pos < numDeltas: - value = deltas[pos] - if value == 0: + + if optimizeSize: + 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) - elif -128 <= value <= 127: - pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) - elif -32768 <= value <= 32767: - pos = TupleVariation.encodeDeltaRunAsWords_(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) + 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) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 044f65f71..cdc9ef8e7 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -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"" diff --git a/Tests/ttLib/tables/TupleVariation_test.py b/Tests/ttLib/tables/TupleVariation_test.py index bfb0e453b..ffd472378 100644 --- a/Tests/ttLib/tables/TupleVariation_test.py +++ b/Tests/ttLib/tables/TupleVariation_test.py @@ -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]) )