diff --git a/Lib/fontTools/pens/t2CharStringPen.py b/Lib/fontTools/pens/t2CharStringPen.py index a936d03a0..d9f8498fe 100644 --- a/Lib/fontTools/pens/t2CharStringPen.py +++ b/Lib/fontTools/pens/t2CharStringPen.py @@ -9,15 +9,6 @@ from fontTools.misc.psCharStrings import T2CharString from fontTools.pens.basePen import BasePen -def roundInt(v): - return int(round(v)) - - -def roundIntPoint(point): - x, y = point - return roundInt(x), roundInt(y) - - class RelativeCoordinatePen(BasePen): def __init__(self, glyphSet): @@ -75,20 +66,45 @@ class RelativeCoordinatePen(BasePen): raise NotImplementedError +def makeRoundFunc(tolerance): + if tolerance < 0: + raise ValueError("Rounding tolerance must be positive") + + def _round(number): + if tolerance == 0: + return number # no-op + rounded = round(number) + # return rounded integer if the tolerance >= 0.5, or if the absolute + # difference between the original float and the rounded integer is + # within the tolerance + if tolerance >= .5 or abs(rounded - number) <= tolerance: + return rounded + else: + # else return the value un-rounded + return number + + def roundPoint(point): + x, y = point + return _round(x), _round(y) + + return roundPoint + + class T2CharStringPen(RelativeCoordinatePen): - def __init__(self, width, glyphSet): + def __init__(self, width, glyphSet, roundTolerance=0.5): RelativeCoordinatePen.__init__(self, glyphSet) + self.roundPoint = makeRoundFunc(roundTolerance) self._heldMove = None self._program = [] if width is not None: - self._program.append(roundInt(width)) + self._program.append(round(width)) def _moveTo(self, pt): - RelativeCoordinatePen._moveTo(self, roundIntPoint(pt)) + RelativeCoordinatePen._moveTo(self, self.roundPoint(pt)) def _relativeMoveTo(self, pt): - pt = roundIntPoint(pt) + pt = self.roundPoint(pt) x, y = pt self._heldMove = [x, y, "rmoveto"] @@ -98,22 +114,25 @@ class T2CharStringPen(RelativeCoordinatePen): self._heldMove = None def _lineTo(self, pt): - RelativeCoordinatePen._lineTo(self, roundIntPoint(pt)) + RelativeCoordinatePen._lineTo(self, self.roundPoint(pt)) def _relativeLineTo(self, pt): self._storeHeldMove() - pt = roundIntPoint(pt) + pt = self.roundPoint(pt) x, y = pt self._program.extend([x, y, "rlineto"]) def _curveToOne(self, pt1, pt2, pt3): - RelativeCoordinatePen._curveToOne(self, roundIntPoint(pt1), roundIntPoint(pt2), roundIntPoint(pt3)) + RelativeCoordinatePen._curveToOne(self, + self.roundPoint(pt1), + self.roundPoint(pt2), + self.roundPoint(pt3)) def _relativeCurveToOne(self, pt1, pt2, pt3): self._storeHeldMove() - pt1 = roundIntPoint(pt1) - pt2 = roundIntPoint(pt2) - pt3 = roundIntPoint(pt3) + pt1 = self.roundPoint(pt1) + pt2 = self.roundPoint(pt2) + pt3 = self.roundPoint(pt3) x1, y1 = pt1 x2, y2 = pt2 x3, y3 = pt3 @@ -127,5 +146,6 @@ class T2CharStringPen(RelativeCoordinatePen): def getCharString(self, private=None, globalSubrs=None): program = self._program + ["endchar"] - charString = T2CharString(program=program, private=private, globalSubrs=globalSubrs) + charString = T2CharString( + program=program, private=private, globalSubrs=globalSubrs) return charString diff --git a/Lib/fontTools/pens/t2CharStringPen_test.py b/Lib/fontTools/pens/t2CharStringPen_test.py new file mode 100644 index 000000000..e3cb002b2 --- /dev/null +++ b/Lib/fontTools/pens/t2CharStringPen_test.py @@ -0,0 +1,125 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.t2CharStringPen import T2CharStringPen +import unittest + + +class T2CharStringPenTest(unittest.TestCase): + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def assertAlmostEqualProgram(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for i1, i2 in zip(expected, actual): + if isinstance(i1, basestring): + self.assertIsInstance(i2, basestring) + self.assertEqual(i1, i2) + else: + self.assertAlmostEqual(i1, i2) + + def test_draw_lines(self): + pen = T2CharStringPen(100, {}) + pen.moveTo((0, 0)) + pen.lineTo((10, 0)) + pen.lineTo((10, 10)) + pen.lineTo((0, 10)) + pen.closePath() # no-op + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 0, 0, 'rmoveto', + 10, 0, 'rlineto', + 0, 10, 'rlineto', + -10, 0, 'rlineto', + 'endchar'], + charstring.program) + + def test_draw_curves(self): + pen = T2CharStringPen(100, {}) + pen.moveTo((0, 0)) + pen.curveTo((10, 0), (20, 10), (20, 20)) + pen.curveTo((20, 30), (10, 40), (0, 40)) + pen.endPath() # no-op + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 0, 0, 'rmoveto', + 10, 0, 10, 10, 0, 10, 'rrcurveto', + 0, 10, -10, 10, -10, 0, 'rrcurveto', + 'endchar'], + charstring.program) + + def test_default_width(self): + pen = T2CharStringPen(None, {}) + charstring = pen.getCharString(None, None) + self.assertEqual(['endchar'], charstring.program) + + def test_no_round(self): + pen = T2CharStringPen(100.1, {}, roundTolerance=0.0) + pen.moveTo((0, 0)) + pen.curveTo((10.1, 0.1), (19.9, 9.9), (20.49, 20.49)) + pen.curveTo((20.49, 30.49), (9.9, 39.9), (0.1, 40.1)) + pen.closePath() + charstring = pen.getCharString(None, None) + + self.assertAlmostEqualProgram( + [100, # we always round the advance width + 0, 0, 'rmoveto', + 10.1, 0.1, 9.8, 9.8, 0.59, 10.59, 'rrcurveto', + 0, 10, -10.59, 9.41, -9.8, 0.2, 'rrcurveto', + 'endchar'], + charstring.program) + + def test_round_all(self): + pen = T2CharStringPen(100.1, {}, roundTolerance=0.5) + pen.moveTo((0, 0)) + pen.curveTo((10.1, 0.1), (19.9, 9.9), (20.49, 20.49)) + pen.curveTo((20.49, 30.49), (9.9, 39.9), (0.1, 40.1)) + pen.closePath() + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 0, 0, 'rmoveto', + 10, 0, 10, 10, 0, 10, 'rrcurveto', + 0, 10, -10, 10, -10, 0, 'rrcurveto', + 'endchar'], + charstring.program) + + def test_round_some(self): + pen = T2CharStringPen(100, {}, roundTolerance=0.2) + pen.moveTo((0, 0)) + # the following two are rounded as within the tolerance + pen.lineTo((10.1, 0.1)) + pen.lineTo((19.9, 9.9)) + # this one is not rounded as it exceeds the tolerance + pen.lineTo((20.49, 20.49)) + pen.closePath() + charstring = pen.getCharString(None, None) + + self.assertAlmostEqualProgram( + [100, + 0, 0, 'rmoveto', + 10, 0, 'rlineto', + 10, 10, 'rlineto', + 0.49, 10.49, 'rlineto', + 'endchar'], + charstring.program) + + def test_invalid_tolerance(self): + self.assertRaisesRegex( + ValueError, + "Rounding tolerance must be positive", + T2CharStringPen, None, {}, roundTolerance=-0.1) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main())