1# Copyright 2016 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import unittest
16
17from fontTools.pens.cu2quPen import Cu2QuPen, Cu2QuPointPen
18from . import CUBIC_GLYPHS, QUAD_GLYPHS
19from .utils import DummyGlyph, DummyPointGlyph
20from .utils import DummyPen, DummyPointPen
21from fontTools.misc.loggingTools import CapturingLogHandler
22from textwrap import dedent
23import logging
24
25
26MAX_ERR = 1.0
27
28
29class _TestPenMixin(object):
30    """Collection of tests that are shared by both the SegmentPen and the
31    PointPen test cases, plus some helper methods.
32    """
33
34    maxDiff = None
35
36    def diff(self, expected, actual):
37        import difflib
38        expected = str(self.Glyph(expected)).splitlines(True)
39        actual = str(self.Glyph(actual)).splitlines(True)
40        diff = difflib.unified_diff(
41            expected, actual, fromfile='expected', tofile='actual')
42        return "".join(diff)
43
44    def convert_glyph(self, glyph, **kwargs):
45        # draw source glyph onto a new glyph using a Cu2Qu pen and return it
46        converted = self.Glyph()
47        pen = getattr(converted, self.pen_getter_name)()
48        quadpen = self.Cu2QuPen(pen, MAX_ERR, **kwargs)
49        getattr(glyph, self.draw_method_name)(quadpen)
50        return converted
51
52    def expect_glyph(self, source, expected):
53        converted = self.convert_glyph(source)
54        self.assertNotEqual(converted, source)
55        if not converted.approx(expected):
56            print(self.diff(expected, converted))
57            self.fail("converted glyph is different from expected")
58
59    def test_convert_simple_glyph(self):
60        self.expect_glyph(CUBIC_GLYPHS['a'], QUAD_GLYPHS['a'])
61        self.expect_glyph(CUBIC_GLYPHS['A'], QUAD_GLYPHS['A'])
62
63    def test_convert_composite_glyph(self):
64        source = CUBIC_GLYPHS['Aacute']
65        converted = self.convert_glyph(source)
66        # components don't change after quadratic conversion
67        self.assertEqual(converted, source)
68
69    def test_convert_mixed_glyph(self):
70        # this contains a mix of contours and components
71        self.expect_glyph(CUBIC_GLYPHS['Eacute'], QUAD_GLYPHS['Eacute'])
72
73    def test_reverse_direction(self):
74        for name in ('a', 'A', 'Eacute'):
75            source = CUBIC_GLYPHS[name]
76            normal_glyph = self.convert_glyph(source)
77            reversed_glyph = self.convert_glyph(source, reverse_direction=True)
78
79            # the number of commands is the same, just their order is iverted
80            self.assertTrue(
81                len(normal_glyph.outline), len(reversed_glyph.outline))
82            self.assertNotEqual(normal_glyph, reversed_glyph)
83
84    def test_stats(self):
85        stats = {}
86        for name in CUBIC_GLYPHS.keys():
87            source = CUBIC_GLYPHS[name]
88            self.convert_glyph(source, stats=stats)
89
90        self.assertTrue(stats)
91        self.assertTrue('1' in stats)
92        self.assertEqual(type(stats['1']), int)
93
94    def test_addComponent(self):
95        pen = self.Pen()
96        quadpen = self.Cu2QuPen(pen, MAX_ERR)
97        quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0))
98
99        # components are passed through without changes
100        self.assertEqual(str(pen).splitlines(), [
101            "pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))",
102        ])
103
104
105class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
106
107    def __init__(self, *args, **kwargs):
108        super(TestCu2QuPen, self).__init__(*args, **kwargs)
109        self.Glyph = DummyGlyph
110        self.Pen = DummyPen
111        self.Cu2QuPen = Cu2QuPen
112        self.pen_getter_name = 'getPen'
113        self.draw_method_name = 'draw'
114
115    def test__check_contour_is_open(self):
116        msg = "moveTo is required"
117        quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
118
119        with self.assertRaisesRegex(AssertionError, msg):
120            quadpen.lineTo((0, 0))
121        with self.assertRaisesRegex(AssertionError, msg):
122            quadpen.qCurveTo((0, 0), (1, 1))
123        with self.assertRaisesRegex(AssertionError, msg):
124            quadpen.curveTo((0, 0), (1, 1), (2, 2))
125        with self.assertRaisesRegex(AssertionError, msg):
126            quadpen.closePath()
127        with self.assertRaisesRegex(AssertionError, msg):
128            quadpen.endPath()
129
130        quadpen.moveTo((0, 0))  # now it works
131        quadpen.lineTo((1, 1))
132        quadpen.qCurveTo((2, 2), (3, 3))
133        quadpen.curveTo((4, 4), (5, 5), (6, 6))
134        quadpen.closePath()
135
136    def test__check_contour_closed(self):
137        msg = "closePath or endPath is required"
138        quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
139        quadpen.moveTo((0, 0))
140
141        with self.assertRaisesRegex(AssertionError, msg):
142            quadpen.moveTo((1, 1))
143        with self.assertRaisesRegex(AssertionError, msg):
144            quadpen.addComponent("a", (1, 0, 0, 1, 0, 0))
145
146        # it works if contour is closed
147        quadpen.closePath()
148        quadpen.moveTo((1, 1))
149        quadpen.endPath()
150        quadpen.addComponent("a", (1, 0, 0, 1, 0, 0))
151
152    def test_qCurveTo_no_points(self):
153        quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
154        quadpen.moveTo((0, 0))
155
156        with self.assertRaisesRegex(
157                AssertionError, "illegal qcurve segment point count: 0"):
158            quadpen.qCurveTo()
159
160    def test_qCurveTo_1_point(self):
161        pen = DummyPen()
162        quadpen = Cu2QuPen(pen, MAX_ERR)
163        quadpen.moveTo((0, 0))
164        quadpen.qCurveTo((1, 1))
165
166        self.assertEqual(str(pen).splitlines(), [
167            "pen.moveTo((0, 0))",
168            "pen.lineTo((1, 1))",
169        ])
170
171    def test_qCurveTo_more_than_1_point(self):
172        pen = DummyPen()
173        quadpen = Cu2QuPen(pen, MAX_ERR)
174        quadpen.moveTo((0, 0))
175        quadpen.qCurveTo((1, 1), (2, 2))
176
177        self.assertEqual(str(pen).splitlines(), [
178            "pen.moveTo((0, 0))",
179            "pen.qCurveTo((1, 1), (2, 2))",
180        ])
181
182    def test_curveTo_no_points(self):
183        quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
184        quadpen.moveTo((0, 0))
185
186        with self.assertRaisesRegex(
187                AssertionError, "illegal curve segment point count: 0"):
188            quadpen.curveTo()
189
190    def test_curveTo_1_point(self):
191        pen = DummyPen()
192        quadpen = Cu2QuPen(pen, MAX_ERR)
193        quadpen.moveTo((0, 0))
194        quadpen.curveTo((1, 1))
195
196        self.assertEqual(str(pen).splitlines(), [
197            "pen.moveTo((0, 0))",
198            "pen.lineTo((1, 1))",
199        ])
200
201    def test_curveTo_2_points(self):
202        pen = DummyPen()
203        quadpen = Cu2QuPen(pen, MAX_ERR)
204        quadpen.moveTo((0, 0))
205        quadpen.curveTo((1, 1), (2, 2))
206
207        self.assertEqual(str(pen).splitlines(), [
208            "pen.moveTo((0, 0))",
209            "pen.qCurveTo((1, 1), (2, 2))",
210        ])
211
212    def test_curveTo_3_points(self):
213        pen = DummyPen()
214        quadpen = Cu2QuPen(pen, MAX_ERR)
215        quadpen.moveTo((0, 0))
216        quadpen.curveTo((1, 1), (2, 2), (3, 3))
217
218        self.assertEqual(str(pen).splitlines(), [
219            "pen.moveTo((0, 0))",
220            "pen.qCurveTo((0.75, 0.75), (2.25, 2.25), (3, 3))",
221        ])
222
223    def test_curveTo_more_than_3_points(self):
224        # a 'SuperBezier' as described in fontTools.basePen.AbstractPen
225        pen = DummyPen()
226        quadpen = Cu2QuPen(pen, MAX_ERR)
227        quadpen.moveTo((0, 0))
228        quadpen.curveTo((1, 1), (2, 2), (3, 3), (4, 4))
229
230        self.assertEqual(str(pen).splitlines(), [
231            "pen.moveTo((0, 0))",
232            "pen.qCurveTo((0.75, 0.75), (1.625, 1.625), (2, 2))",
233            "pen.qCurveTo((2.375, 2.375), (3.25, 3.25), (4, 4))",
234        ])
235
236    def test_addComponent(self):
237        pen = DummyPen()
238        quadpen = Cu2QuPen(pen, MAX_ERR)
239        quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0))
240
241        # components are passed through without changes
242        self.assertEqual(str(pen).splitlines(), [
243            "pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))",
244        ])
245
246    def test_ignore_single_points(self):
247        pen = DummyPen()
248        try:
249            logging.captureWarnings(True)
250            with CapturingLogHandler("py.warnings", level="WARNING") as log:
251                quadpen = Cu2QuPen(pen, MAX_ERR, ignore_single_points=True)
252        finally:
253            logging.captureWarnings(False)
254        quadpen.moveTo((0, 0))
255        quadpen.endPath()
256        quadpen.moveTo((1, 1))
257        quadpen.closePath()
258
259        self.assertGreaterEqual(len(log.records), 1)
260        self.assertIn("ignore_single_points is deprecated",
261                      log.records[0].args[0])
262
263        # single-point contours were ignored, so the pen commands are empty
264        self.assertFalse(pen.commands)
265
266        # redraw without ignoring single points
267        quadpen.ignore_single_points = False
268        quadpen.moveTo((0, 0))
269        quadpen.endPath()
270        quadpen.moveTo((1, 1))
271        quadpen.closePath()
272
273        self.assertTrue(pen.commands)
274        self.assertEqual(str(pen).splitlines(), [
275            "pen.moveTo((0, 0))",
276            "pen.endPath()",
277            "pen.moveTo((1, 1))",
278            "pen.closePath()"
279        ])
280
281
282class TestCu2QuPointPen(unittest.TestCase, _TestPenMixin):
283
284    def __init__(self, *args, **kwargs):
285        super(TestCu2QuPointPen, self).__init__(*args, **kwargs)
286        self.Glyph = DummyPointGlyph
287        self.Pen = DummyPointPen
288        self.Cu2QuPen = Cu2QuPointPen
289        self.pen_getter_name = 'getPointPen'
290        self.draw_method_name = 'drawPoints'
291
292    def test_super_bezier_curve(self):
293        pen = DummyPointPen()
294        quadpen = Cu2QuPointPen(pen, MAX_ERR)
295        quadpen.beginPath()
296        quadpen.addPoint((0, 0), segmentType="move")
297        quadpen.addPoint((1, 1))
298        quadpen.addPoint((2, 2))
299        quadpen.addPoint((3, 3))
300        quadpen.addPoint(
301            (4, 4), segmentType="curve", smooth=False, name="up", selected=1)
302        quadpen.endPath()
303
304        self.assertEqual(str(pen).splitlines(), """\
305pen.beginPath()
306pen.addPoint((0, 0), name=None, segmentType='move', smooth=False)
307pen.addPoint((0.75, 0.75), name=None, segmentType=None, smooth=False)
308pen.addPoint((1.625, 1.625), name=None, segmentType=None, smooth=False)
309pen.addPoint((2, 2), name=None, segmentType='qcurve', smooth=True)
310pen.addPoint((2.375, 2.375), name=None, segmentType=None, smooth=False)
311pen.addPoint((3.25, 3.25), name=None, segmentType=None, smooth=False)
312pen.addPoint((4, 4), name='up', segmentType='qcurve', selected=1, smooth=False)
313pen.endPath()""".splitlines())
314
315    def test__flushContour_restore_starting_point(self):
316        pen = DummyPointPen()
317        quadpen = Cu2QuPointPen(pen, MAX_ERR)
318
319        # collect the output of _flushContour before it's sent to _drawPoints
320        new_segments = []
321        def _drawPoints(segments):
322            new_segments.extend(segments)
323            Cu2QuPointPen._drawPoints(quadpen, segments)
324        quadpen._drawPoints = _drawPoints
325
326        # a closed path (ie. no "move" segmentType)
327        quadpen._flushContour([
328            ("curve", [
329                ((2, 2), False, None, {}),
330                ((1, 1), False, None, {}),
331                ((0, 0), False, None, {}),
332            ]),
333            ("curve", [
334                ((1, 1), False, None, {}),
335                ((2, 2), False, None, {}),
336                ((3, 3), False, None, {}),
337            ]),
338        ])
339
340        # the original starting point is restored: the last segment has become
341        # the first
342        self.assertEqual(new_segments[0][1][-1][0], (3, 3))
343        self.assertEqual(new_segments[-1][1][-1][0], (0, 0))
344
345        new_segments = []
346        # an open path (ie. starting with "move")
347        quadpen._flushContour([
348            ("move", [
349                ((0, 0), False, None, {}),
350            ]),
351            ("curve", [
352                ((1, 1), False, None, {}),
353                ((2, 2), False, None, {}),
354                ((3, 3), False, None, {}),
355            ]),
356        ])
357
358        # the segment order stays the same before and after _flushContour
359        self.assertEqual(new_segments[0][1][-1][0], (0, 0))
360        self.assertEqual(new_segments[-1][1][-1][0], (3, 3))
361
362    def test_quad_no_oncurve(self):
363        """When passed a contour which has no on-curve points, the
364        Cu2QuPointPen will treat it as a special quadratic contour whose
365        first point has 'None' coordinates.
366        """
367        self.maxDiff = None
368        pen = DummyPointPen()
369        quadpen = Cu2QuPointPen(pen, MAX_ERR)
370        quadpen.beginPath()
371        quadpen.addPoint((1, 1))
372        quadpen.addPoint((2, 2))
373        quadpen.addPoint((3, 3))
374        quadpen.endPath()
375
376        self.assertEqual(
377            str(pen),
378            dedent(
379                """\
380                pen.beginPath()
381                pen.addPoint((1, 1), name=None, segmentType=None, smooth=False)
382                pen.addPoint((2, 2), name=None, segmentType=None, smooth=False)
383                pen.addPoint((3, 3), name=None, segmentType=None, smooth=False)
384                pen.endPath()"""
385            )
386        )
387
388
389if __name__ == "__main__":
390    unittest.main()
391