Merge pull request #804 from anthrotype/t2pen-round

[t2CharStringPen] add "roundTolerance" option
diff --git a/Lib/fontTools/pens/t2CharStringPen.py b/Lib/fontTools/pens/t2CharStringPen.py
index a936d03..d9f8498 100644
--- a/Lib/fontTools/pens/t2CharStringPen.py
+++ b/Lib/fontTools/pens/t2CharStringPen.py
@@ -9,15 +9,6 @@
 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 @@
         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 @@
             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 @@
 
     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 0000000..e3cb002
--- /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())