Merge pull request #3750 from fonttools/recalcBounds-round-coords

[glyf] fix bbox of transformed components with un-rounded coordinates
This commit is contained in:
Cosimo Lupo 2025-01-23 11:26:50 +00:00 committed by GitHub
commit cf20a14453
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 3 deletions

View File

@ -1187,7 +1187,7 @@ class Glyph(object):
): ):
return return
try: 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() self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds()
except NotImplementedError: except NotImplementedError:
pass pass
@ -1241,7 +1241,7 @@ class Glyph(object):
else: else:
return self.numberOfContours == -1 return self.numberOfContours == -1
def getCoordinates(self, glyfTable): def getCoordinates(self, glyfTable, round=noRound):
"""Return the coordinates, end points and flags """Return the coordinates, end points and flags
This method returns three values: A :py:class:`GlyphCoordinates` object, This method returns three values: A :py:class:`GlyphCoordinates` object,
@ -1267,13 +1267,16 @@ class Glyph(object):
for compo in self.components: for compo in self.components:
g = glyfTable[compo.glyphName] g = glyfTable[compo.glyphName]
try: try:
coordinates, endPts, flags = g.getCoordinates(glyfTable) coordinates, endPts, flags = g.getCoordinates(
glyfTable, round=round
)
except RecursionError: except RecursionError:
raise ttLib.TTLibError( raise ttLib.TTLibError(
"glyph '%s' contains a recursive component reference" "glyph '%s' contains a recursive component reference"
% compo.glyphName % compo.glyphName
) )
coordinates = GlyphCoordinates(coordinates) coordinates = GlyphCoordinates(coordinates)
coordinates.toInt(round=round)
if hasattr(compo, "firstPt"): if hasattr(compo, "firstPt"):
# component uses two reference points: we apply the transform _before_ # component uses two reference points: we apply the transform _before_
# computing the offset between the points # computing the offset between the points

View File

@ -3,6 +3,7 @@ import pytest
import struct import struct
from fontTools import ttLib from fontTools import ttLib
from fontTools.misc.roundTools import noRound
from fontTools.pens.basePen import PenError from fontTools.pens.basePen import PenError
from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen
from fontTools.pens.ttGlyphPen import TTGlyphPen, TTGlyphPointPen, MAX_F2DOT14 from fontTools.pens.ttGlyphPen import TTGlyphPen, TTGlyphPointPen, MAX_F2DOT14
@ -564,6 +565,55 @@ class TTGlyphPointPenTest(TTGlyphPenTestBase):
uni0302_uni0300.recalcBounds(glyphSet) uni0302_uni0300.recalcBounds(glyphSet)
self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025)) 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): def test_open_path_starting_with_move(self):
# when a contour starts with a 'move' point, it signifies the beginnig # when a contour starts with a 'move' point, it signifies the beginnig
# of an open contour. # of an open contour.