diff --git a/Lib/cu2qu/test/pens_test.py b/Lib/cu2qu/test/pens_test.py new file mode 100644 index 000000000..95e6130b0 --- /dev/null +++ b/Lib/cu2qu/test/pens_test.py @@ -0,0 +1,336 @@ +from __future__ import print_function, division, absolute_import +import unittest + +from cu2qu.pens import Cu2QuPen, Cu2QuPointPen +from cu2qu.test import CUBIC_GLYPHS, QUAD_GLYPHS +from cu2qu.test.utils import DummyGlyph, DummyPointGlyph +from cu2qu.test.utils import DummyPen, DummyPointPen + +MAX_ERR = 1.0 + + +class _TestPenMixin(object): + """Collection of tests that are shared by both the SegmentPen and the + PointPen test cases, plus some helper methods. + """ + + def diff(self, expected, actual): + import difflib + expected = str(self.Glyph(expected)).splitlines(True) + actual = str(self.Glyph(actual)).splitlines(True) + diff = difflib.unified_diff( + expected, actual, fromfile='expected', tofile='actual') + return "".join(diff) + + def convert_glyph(self, glyph, **kwargs): + # draw source glyph onto a new glyph using a Cu2Qu pen and return it + converted = self.Glyph() + pen = getattr(converted, self.pen_getter_name)() + quadpen = self.Cu2QuPen(pen, MAX_ERR, **kwargs) + getattr(glyph, self.draw_method_name)(quadpen) + return converted + + def expect_glyph(self, source, expected): + converted = self.convert_glyph(source) + self.assertNotEqual(converted, source) + if converted != expected: + print(self.diff(expected, converted)) + self.fail("converted glyph is different from expected") + + def test_convert_simple_glyph(self): + self.expect_glyph(CUBIC_GLYPHS['a'], QUAD_GLYPHS['a']) + self.expect_glyph(CUBIC_GLYPHS['A'], QUAD_GLYPHS['A']) + + def test_convert_composite_glyph(self): + source = CUBIC_GLYPHS['Aacute'] + converted = self.convert_glyph(source) + # components don't change after quadratic conversion + self.assertEqual(converted, source) + + def test_convert_mixed_glyph(self): + # this contains a mix of contours and components + self.expect_glyph(CUBIC_GLYPHS['Eacute'], QUAD_GLYPHS['Eacute']) + + def test_reverse_direction(self): + for name in ('a', 'A', 'Eacute'): + source = CUBIC_GLYPHS[name] + normal_glyph = self.convert_glyph(source) + reversed_glyph = self.convert_glyph(source, reverse_direction=True) + + # the number of commands is the same, just their order is iverted + self.assertTrue( + len(normal_glyph.outline), len(reversed_glyph.outline)) + self.assertNotEqual(normal_glyph, reversed_glyph) + + def test_stats(self): + stats = {} + for name in CUBIC_GLYPHS.keys(): + source = CUBIC_GLYPHS[name] + self.convert_glyph(source, stats=stats) + + self.assertTrue(stats) + self.assertTrue('4' in stats) + self.assertEqual(type(stats['4']), int) + + def test_addComponent(self): + pen = self.Pen() + quadpen = self.Cu2QuPen(pen, MAX_ERR) + quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0)) + + # components are passed through without changes + self.assertEqual(str(pen).splitlines(), [ + "pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))", + ]) + + +class TestCu2QuPen(unittest.TestCase, _TestPenMixin): + + def __init__(self, *args, **kwargs): + super(TestCu2QuPen, self).__init__(*args, **kwargs) + self.Glyph = DummyGlyph + self.Pen = DummyPen + self.Cu2QuPen = Cu2QuPen + self.pen_getter_name = 'getPen' + self.draw_method_name = 'draw' + + def test__check_contour_is_open(self): + msg = "moveTo is required" + quadpen = Cu2QuPen(DummyPen(), MAX_ERR) + + with self.assertRaisesRegex(AssertionError, msg): + quadpen.lineTo((0, 0)) + with self.assertRaisesRegex(AssertionError, msg): + quadpen.qCurveTo((0, 0), (1, 1)) + with self.assertRaisesRegex(AssertionError, msg): + quadpen.curveTo((0, 0), (1, 1), (2, 2)) + with self.assertRaisesRegex(AssertionError, msg): + quadpen.closePath() + with self.assertRaisesRegex(AssertionError, msg): + quadpen.endPath() + + quadpen.moveTo((0, 0)) # now it works + quadpen.lineTo((1, 1)) + quadpen.qCurveTo((2, 2), (3, 3)) + quadpen.curveTo((4, 4), (5, 5), (6, 6)) + quadpen.closePath() + + def test__check_contour_closed(self): + msg = "closePath or endPath is required" + quadpen = Cu2QuPen(DummyPen(), MAX_ERR) + quadpen.moveTo((0, 0)) + + with self.assertRaisesRegex(AssertionError, msg): + quadpen.moveTo((1, 1)) + with self.assertRaisesRegex(AssertionError, msg): + quadpen.addComponent("a", (1, 0, 0, 1, 0, 0)) + + # it works if contour is closed + quadpen.closePath() + quadpen.moveTo((1, 1)) + quadpen.endPath() + quadpen.addComponent("a", (1, 0, 0, 1, 0, 0)) + + def test_qCurveTo_no_points(self): + quadpen = Cu2QuPen(DummyPen(), MAX_ERR) + quadpen.moveTo((0, 0)) + + with self.assertRaisesRegex( + AssertionError, "illegal qcurve segment point count: 0"): + quadpen.qCurveTo() + + def test_qCurveTo_1_point(self): + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR) + quadpen.moveTo((0, 0)) + quadpen.qCurveTo((1, 1)) + + self.assertEqual(str(pen).splitlines(), [ + "pen.moveTo((0, 0))", + "pen.lineTo((1, 1))", + ]) + + def test_qCurveTo_more_than_1_point(self): + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR) + quadpen.moveTo((0, 0)) + quadpen.qCurveTo((1, 1), (2, 2)) + + self.assertEqual(str(pen).splitlines(), [ + "pen.moveTo((0, 0))", + "pen.qCurveTo((1, 1), (2, 2))", + ]) + + def test_curveTo_no_points(self): + quadpen = Cu2QuPen(DummyPen(), MAX_ERR) + quadpen.moveTo((0, 0)) + + with self.assertRaisesRegex( + AssertionError, "illegal curve segment point count: 0"): + quadpen.curveTo() + + def test_curveTo_1_point(self): + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR) + quadpen.moveTo((0, 0)) + quadpen.curveTo((1, 1)) + + self.assertEqual(str(pen).splitlines(), [ + "pen.moveTo((0, 0))", + "pen.lineTo((1, 1))", + ]) + + def test_curveTo_2_points(self): + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR) + quadpen.moveTo((0, 0)) + quadpen.curveTo((1, 1), (2, 2)) + + self.assertEqual(str(pen).splitlines(), [ + "pen.moveTo((0, 0))", + "pen.qCurveTo((1, 1), (2, 2))", + ]) + + def test_curveTo_3_points(self): + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR) + quadpen.moveTo((0, 0)) + quadpen.curveTo((1, 1), (2, 2), (3, 3)) + + self.assertEqual(str(pen).splitlines(), [ + "pen.moveTo((0, 0))", + "pen.qCurveTo((0.75, 0.75), (2.25, 2.25), (3, 3))", + ]) + + def test_curveTo_more_than_3_points(self): + # a 'SuperBezier' as described in fontTools.basePen.AbstractPen + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR) + quadpen.moveTo((0, 0)) + quadpen.curveTo((1, 1), (2, 2), (3, 3), (4, 4)) + + self.assertEqual(str(pen).splitlines(), [ + "pen.moveTo((0, 0))", + "pen.qCurveTo((0.75, 0.75), (1.625, 1.625), (2, 2))", + "pen.qCurveTo((2.375, 2.375), (3.25, 3.25), (4, 4))", + ]) + + def test_addComponent(self): + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR) + quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0)) + + # components are passed through without changes + self.assertEqual(str(pen).splitlines(), [ + "pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))", + ]) + + def test_ignore_single_points(self): + pen = DummyPen() + quadpen = Cu2QuPen(pen, MAX_ERR, ignore_single_points=True) + quadpen.moveTo((0, 0)) + quadpen.endPath() + quadpen.moveTo((1, 1)) + quadpen.closePath() + + # single-point contours were ignored, so the pen commands are empty + self.assertFalse(pen.commands) + + # redraw without ignoring single points + quadpen.ignore_single_points = False + quadpen.moveTo((0, 0)) + quadpen.endPath() + quadpen.moveTo((1, 1)) + quadpen.closePath() + + self.assertTrue(pen.commands) + self.assertEqual(str(pen).splitlines(), [ + "pen.moveTo((0, 0))", + "pen.endPath()", + "pen.moveTo((1, 1))", + "pen.closePath()" + ]) + + +class TestCu2QuPointPen(unittest.TestCase, _TestPenMixin): + + def __init__(self, *args, **kwargs): + super(TestCu2QuPointPen, self).__init__(*args, **kwargs) + self.Glyph = DummyPointGlyph + self.Pen = DummyPointPen + self.Cu2QuPen = Cu2QuPointPen + self.pen_getter_name = 'getPointPen' + self.draw_method_name = 'drawPoints' + + def test_super_bezier_curve(self): + pen = DummyPointPen() + quadpen = Cu2QuPointPen(pen, MAX_ERR) + quadpen.beginPath() + quadpen.addPoint((0, 0), segmentType="move") + quadpen.addPoint((1, 1)) + quadpen.addPoint((2, 2)) + quadpen.addPoint((3, 3)) + quadpen.addPoint( + (4, 4), segmentType="curve", smooth=False, name="up", selected=1) + quadpen.endPath() + + self.assertEqual(str(pen).splitlines(), """\ +pen.beginPath() +pen.addPoint((0, 0), name=None, segmentType='move', smooth=False) +pen.addPoint((0.75, 0.75), name=None, segmentType=None, smooth=False) +pen.addPoint((1.625, 1.625), name=None, segmentType=None, smooth=False) +pen.addPoint((2, 2), name=None, segmentType='qcurve', smooth=True) +pen.addPoint((2.375, 2.375), name=None, segmentType=None, smooth=False) +pen.addPoint((3.25, 3.25), name=None, segmentType=None, smooth=False) +pen.addPoint((4, 4), name='up', segmentType='qcurve', selected=1, smooth=False) +pen.endPath()""".splitlines()) + + def test__flushContour_restore_starting_point(self): + pen = DummyPointPen() + quadpen = Cu2QuPointPen(pen, MAX_ERR) + + # collect the output of _flushContour before it's sent to _drawPoints + new_segments = [] + def _drawPoints(segments): + new_segments.extend(segments) + Cu2QuPointPen._drawPoints(quadpen, segments) + quadpen._drawPoints = _drawPoints + + # a closed path (ie. no "move" segmentType) + quadpen._flushContour([ + ("curve", [ + ((2, 2), False, None, {}), + ((1, 1), False, None, {}), + ((0, 0), False, None, {}), + ]), + ("curve", [ + ((1, 1), False, None, {}), + ((2, 2), False, None, {}), + ((3, 3), False, None, {}), + ]), + ]) + + # the original starting point is restored: the last segment has become + # the first + self.assertEqual(new_segments[0][1][-1][0], (3, 3)) + self.assertEqual(new_segments[-1][1][-1][0], (0, 0)) + + new_segments = [] + # an open path (ie. starting with "move") + quadpen._flushContour([ + ("move", [ + ((0, 0), False, None, {}), + ]), + ("curve", [ + ((1, 1), False, None, {}), + ((2, 2), False, None, {}), + ((3, 3), False, None, {}), + ]), + ]) + + # the segment order stays the same before and after _flushContour + self.assertEqual(new_segments[0][1][-1][0], (0, 0)) + self.assertEqual(new_segments[-1][1][-1][0], (3, 3)) + + +if __name__ == "__main__": + unittest.main()