From 6ea7f5e2a689b2749aa570c455d95cf52d41249e Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2025 11:06:28 +0000 Subject: [PATCH 1/2] [ttGlyphPen_test] add repro for googlefonts/fontc#1206 the test makes sure that the bounding box of composite glyph with non trivial transform pointing to base glyphs with un-rounded floating-point coordinates, still get their bounding box computed on the rounded-off integer coordinates. see https://github.com/googlefonts/fontc/issues/1206 --- Tests/pens/ttGlyphPen_test.py | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/Tests/pens/ttGlyphPen_test.py b/Tests/pens/ttGlyphPen_test.py index 6a74cb25a..e508b2bfd 100644 --- a/Tests/pens/ttGlyphPen_test.py +++ b/Tests/pens/ttGlyphPen_test.py @@ -3,6 +3,7 @@ import pytest import struct from fontTools import ttLib +from fontTools.misc.roundTools import noRound from fontTools.pens.basePen import PenError from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen from fontTools.pens.ttGlyphPen import TTGlyphPen, TTGlyphPointPen, MAX_F2DOT14 @@ -564,6 +565,55 @@ class TTGlyphPointPenTest(TTGlyphPenTestBase): uni0302_uni0300.recalcBounds(glyphSet) self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025)) + def test_unrounded_coords_rounded_bounds(self): + # The following test case is taken from Faustina-Italic.glyphs. + # It reproduces a bug whereby the bounding boxes of composite glyphs that + # contain components with non trivial transformations, and in which the base + # glyphs of the transformed components still has un-rounded, floating-point + # coordinates, may be off-by-one compared to the correct bounds that should + # be computed from the rounded coordinates. + # https://github.com/googlefonts/fontc/issues/1206 + glyphSet = {} + pen = TTGlyphPointPen(glyphSet) + + pen.beginPath() + pen.addPoint((235, 680), "line") + pen.addPoint((292, 729), "line") + pen.addPoint((314.5, 748.5), None) + pen.addPoint((342.0, 780.75), None) + pen.addPoint((342.0, 792.0), "qcurve") + pen.addPoint((342.0, 800.0), None) + pen.addPoint((336.66666666666663, 815.9166666666666), None) + pen.addPoint((318.0, 829.5), None) + pen.addPoint((298.0, 834.0), "qcurve") + pen.addPoint((209, 702), "line") + pen.endPath() + pen.beginPath() + pen.addPoint((90, 680), "line") + pen.addPoint((147, 729), "line") + pen.addPoint((169.5, 748.5), None) + pen.addPoint((197.0, 780.75), None) + pen.addPoint((197.0, 792.0), "qcurve") + pen.addPoint((197.0, 800.0), None) + pen.addPoint((191.66666666666663, 815.9166666666666), None) + pen.addPoint((173.0, 829.5), None) + pen.addPoint((153.0, 834.0), "qcurve") + pen.addPoint((64, 702), "line") + pen.endPath() + # round=noRound forces floating-point coordinates to be kept un-rounded + glyphSet["hungarumlaut.case"] = pen.glyph( + dropImpliedOnCurves=True, round=noRound + ) + + # component has non-trivial transform (shear + reflection) + pen.addComponent("hungarumlaut.case", (-1, 0, 0.28108, 1, 196, 0)) + compositeGlyph = pen.glyph(round=noRound) + glyphSet["dblgravecomb.case"] = compositeGlyph + + compositeGlyph.recalcBounds(glyphSet) + # these are the expected bounds as computed from the rounded coordinates + self.assertGlyphBoundsEqual(compositeGlyph, (74, 680, 329, 834)) + def test_open_path_starting_with_move(self): # when a contour starts with a 'move' point, it signifies the beginnig # of an open contour. From ec5d41929f451115810cfb712c773e3390186060 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2025 11:19:30 +0000 Subject: [PATCH 2/2] [glyf] fix recalcBounds of transformed components with unrounded coordinates Fixes https://github.com/googlefonts/fontc/issues/1206 --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 92b69e70b..50f771ab6 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1187,7 +1187,7 @@ class Glyph(object): ): return try: - coords, endPts, flags = self.getCoordinates(glyfTable) + coords, endPts, flags = self.getCoordinates(glyfTable, round=otRound) self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds() except NotImplementedError: pass @@ -1241,7 +1241,7 @@ class Glyph(object): else: return self.numberOfContours == -1 - def getCoordinates(self, glyfTable): + def getCoordinates(self, glyfTable, round=noRound): """Return the coordinates, end points and flags This method returns three values: A :py:class:`GlyphCoordinates` object, @@ -1267,13 +1267,16 @@ class Glyph(object): for compo in self.components: g = glyfTable[compo.glyphName] try: - coordinates, endPts, flags = g.getCoordinates(glyfTable) + coordinates, endPts, flags = g.getCoordinates( + glyfTable, round=round + ) except RecursionError: raise ttLib.TTLibError( "glyph '%s' contains a recursive component reference" % compo.glyphName ) coordinates = GlyphCoordinates(coordinates) + coordinates.toInt(round=round) if hasattr(compo, "firstPt"): # component uses two reference points: we apply the transform _before_ # computing the offset between the points