From e8146a6d0725d398cfa110cba683946ee762f8e2 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 9 Oct 2024 11:04:25 -0600 Subject: [PATCH] [glyf] Add optimizeSize option Set to True by default. Can be turned to False on the table, or at Glyph() compile time. Also fixes Glyph's draw() to expand the glyph first. Otherwise it was failing. --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 77 ++++++++++++++++++++++++-- Tests/ttLib/tables/_g_l_y_f_test.py | 31 +++++++++++ 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index fa11cf8f4..bc7d4bf1e 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -713,7 +713,9 @@ class Glyph(object): else: self.decompileCoordinates(data) - def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): + def compile( + self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=None + ): if hasattr(self, "data"): if recalcBBoxes: # must unpack glyph in order to recalculate bounding box @@ -730,7 +732,9 @@ class Glyph(object): if self.isComposite(): data = data + self.compileComponents(glyfTable) else: - data = data + self.compileCoordinates() + if optimizeSize is None: + optimizeSize = getattr(glyfTable, "optimizeSize", True) + data = data + self.compileCoordinates(optimizeSize=optimizeSize) return data def toXML(self, writer, ttFont): @@ -976,7 +980,7 @@ class Glyph(object): data = data + struct.pack(">h", len(instructions)) + instructions return data - def compileCoordinates(self): + def compileCoordinates(self, *, optimizeSize=True): assert len(self.coordinates) == len(self.flags) data = [] endPtsOfContours = array.array("H", self.endPtsOfContours) @@ -991,9 +995,12 @@ class Glyph(object): deltas.toInt() deltas.absoluteToRelative() - # TODO(behdad): Add a configuration option for this? - deltas = self.compileDeltasGreedy(self.flags, deltas) - # deltas = self.compileDeltasOptimal(self.flags, deltas) + if optimizeSize: + # TODO(behdad): Add a configuration option for this? + deltas = self.compileDeltasGreedy(self.flags, deltas) + # deltas = self.compileDeltasOptimal(self.flags, deltas) + else: + deltas = self.compileDeltasForSpeed(self.flags, deltas) data.extend(deltas) return b"".join(data) @@ -1110,6 +1117,63 @@ class Glyph(object): return (compressedFlags, compressedXs, compressedYs) + def compileDeltasForSpeed(self, flags, deltas): + # uses widest representation needed, for all deltas. + compressedFlags = bytearray() + compressedXs = bytearray() + compressedYs = bytearray() + + # Compute the necessary width for each axis + xs = [d[0] for d in deltas] + ys = [d[1] for d in deltas] + minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys) + xZero = minX == 0 and maxX == 0 + yZero = minY == 0 and maxY == 0 + xShort = -255 <= minX <= maxX <= 255 + yShort = -255 <= minY <= maxY <= 255 + + lastflag = None + repeat = 0 + for flag, (x, y) in zip(flags, deltas): + # Oh, the horrors of TrueType + # do x + if xZero: + flag = flag | flagXsame + elif xShort: + flag = flag | flagXShort + if x > 0: + flag = flag | flagXsame + else: + x = -x + compressedXs.append(x) + else: + compressedXs.extend(struct.pack(">h", x)) + # do y + if yZero: + flag = flag | flagYsame + elif yShort: + flag = flag | flagYShort + if y > 0: + flag = flag | flagYsame + else: + y = -y + compressedYs.append(y) + else: + compressedYs.extend(struct.pack(">h", y)) + # handle repeating flags + if flag == lastflag and repeat != 255: + repeat = repeat + 1 + if repeat == 1: + compressedFlags.append(flag) + else: + compressedFlags[-2] = flag | flagRepeat + compressedFlags[-1] = repeat + else: + repeat = 0 + compressedFlags.append(flag) + lastflag = flag + return (compressedFlags, compressedXs, compressedYs) + def recalcBounds(self, glyfTable, *, boundsDone=None): """Recalculates the bounds of the glyph. @@ -1404,6 +1468,7 @@ class Glyph(object): pen.addComponent(glyphName, transform) return + self.expand(glyfTable) coordinates, endPts, flags = self.getCoordinates(glyfTable) if offset: coordinates = coordinates.copy() diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 9a3fd2eaf..10e053c4d 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -707,6 +707,37 @@ class GlyphComponentTest: '' ] + def test_compile_for_speed(self): + glyph = Glyph() + glyph.numberOfContours = 1 + glyph.coordinates = GlyphCoordinates( + [(0, 0), (1, 0), (1, 0), (1, 1), (1, 1), (0, 1), (0, 1)] + ) + glyph.flags = array.array("B", [flagOnCurve] + [flagCubic] * 6) + glyph.endPtsOfContours = [6] + glyph.program = ttProgram.Program() + + glyph.expand(None) + sizeBytes = glyph.compile(None, optimizeSize=True) + glyph.expand(None) + speedBytes = glyph.compile(None, optimizeSize=False) + + assert len(sizeBytes) < len(speedBytes) + + for data in sizeBytes, speedBytes: + glyph = Glyph(data) + + pen = RecordingPen() + glyph.draw(pen, None) + + assert pen.value == [ + ("moveTo", ((0, 0),)), + ("curveTo", ((1, 0), (1, 0), (1.0, 0.5))), + ("curveTo", ((1, 1), (1, 1), (0.5, 1.0))), + ("curveTo", ((0, 1), (0, 1), (0, 0))), + ("closePath", ()), + ] + def test_fromXML_reference_points(self): comp = GlyphComponent() for name, attrs, content in parseXML(