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 sys 16import unittest 17 18from fontTools.pens.cu2quPen import Cu2QuPen, Cu2QuPointPen, Cu2QuMultiPen 19from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen 20from fontTools.misc.loggingTools import CapturingLogHandler 21from textwrap import dedent 22import logging 23import pytest 24 25try: 26 from .utils import CUBIC_GLYPHS, QUAD_GLYPHS 27 from .utils import DummyGlyph, DummyPointGlyph 28 from .utils import DummyPen, DummyPointPen 29except ImportError as e: 30 pytest.skip(str(e), allow_module_level=True) 31 32 33MAX_ERR = 1.0 34 35 36class _TestPenMixin(object): 37 """Collection of tests that are shared by both the SegmentPen and the 38 PointPen test cases, plus some helper methods. 39 """ 40 41 maxDiff = None 42 43 def diff(self, expected, actual): 44 import difflib 45 46 expected = str(self.Glyph(expected)).splitlines(True) 47 actual = str(self.Glyph(actual)).splitlines(True) 48 diff = difflib.unified_diff( 49 expected, actual, fromfile="expected", tofile="actual" 50 ) 51 return "".join(diff) 52 53 def convert_glyph(self, glyph, **kwargs): 54 # draw source glyph onto a new glyph using a Cu2Qu pen and return it 55 converted = self.Glyph() 56 pen = getattr(converted, self.pen_getter_name)() 57 quadpen = self.Cu2QuPen(pen, MAX_ERR, **kwargs) 58 getattr(glyph, self.draw_method_name)(quadpen) 59 return converted 60 61 def expect_glyph(self, source, expected): 62 converted = self.convert_glyph(source) 63 self.assertNotEqual(converted, source) 64 if not converted.approx(expected): 65 print(self.diff(expected, converted)) 66 self.fail("converted glyph is different from expected") 67 68 def test_convert_simple_glyph(self): 69 self.expect_glyph(CUBIC_GLYPHS["a"], QUAD_GLYPHS["a"]) 70 self.expect_glyph(CUBIC_GLYPHS["A"], QUAD_GLYPHS["A"]) 71 72 def test_convert_composite_glyph(self): 73 source = CUBIC_GLYPHS["Aacute"] 74 converted = self.convert_glyph(source) 75 # components don't change after quadratic conversion 76 self.assertEqual(converted, source) 77 78 def test_convert_mixed_glyph(self): 79 # this contains a mix of contours and components 80 self.expect_glyph(CUBIC_GLYPHS["Eacute"], QUAD_GLYPHS["Eacute"]) 81 82 def test_reverse_direction(self): 83 for name in ("a", "A", "Eacute"): 84 source = CUBIC_GLYPHS[name] 85 normal_glyph = self.convert_glyph(source) 86 reversed_glyph = self.convert_glyph(source, reverse_direction=True) 87 88 # the number of commands is the same, just their order is iverted 89 self.assertTrue(len(normal_glyph.outline), len(reversed_glyph.outline)) 90 self.assertNotEqual(normal_glyph, reversed_glyph) 91 92 def test_stats(self): 93 stats = {} 94 for name in CUBIC_GLYPHS.keys(): 95 source = CUBIC_GLYPHS[name] 96 self.convert_glyph(source, stats=stats) 97 98 self.assertTrue(stats) 99 self.assertTrue("1" in stats) 100 self.assertEqual(type(stats["1"]), int) 101 102 def test_addComponent(self): 103 pen = self.Pen() 104 quadpen = self.Cu2QuPen(pen, MAX_ERR) 105 quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0)) 106 107 # components are passed through without changes 108 self.assertEqual( 109 str(pen).splitlines(), 110 [ 111 "pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))", 112 ], 113 ) 114 115 116class TestCu2QuPen(unittest.TestCase, _TestPenMixin): 117 def __init__(self, *args, **kwargs): 118 super(TestCu2QuPen, self).__init__(*args, **kwargs) 119 self.Glyph = DummyGlyph 120 self.Pen = DummyPen 121 self.Cu2QuPen = Cu2QuPen 122 self.pen_getter_name = "getPen" 123 self.draw_method_name = "draw" 124 125 def test_qCurveTo_1_point(self): 126 pen = DummyPen() 127 quadpen = Cu2QuPen(pen, MAX_ERR) 128 quadpen.moveTo((0, 0)) 129 quadpen.qCurveTo((1, 1)) 130 131 self.assertEqual( 132 str(pen).splitlines(), 133 [ 134 "pen.moveTo((0, 0))", 135 "pen.qCurveTo((1, 1))", 136 ], 137 ) 138 139 def test_qCurveTo_more_than_1_point(self): 140 pen = DummyPen() 141 quadpen = Cu2QuPen(pen, MAX_ERR) 142 quadpen.moveTo((0, 0)) 143 quadpen.qCurveTo((1, 1), (2, 2)) 144 145 self.assertEqual( 146 str(pen).splitlines(), 147 [ 148 "pen.moveTo((0, 0))", 149 "pen.qCurveTo((1, 1), (2, 2))", 150 ], 151 ) 152 153 def test_curveTo_1_point(self): 154 pen = DummyPen() 155 quadpen = Cu2QuPen(pen, MAX_ERR) 156 quadpen.moveTo((0, 0)) 157 quadpen.curveTo((1, 1)) 158 159 self.assertEqual( 160 str(pen).splitlines(), 161 [ 162 "pen.moveTo((0, 0))", 163 "pen.qCurveTo((1, 1))", 164 ], 165 ) 166 167 def test_curveTo_2_points(self): 168 pen = DummyPen() 169 quadpen = Cu2QuPen(pen, MAX_ERR) 170 quadpen.moveTo((0, 0)) 171 quadpen.curveTo((1, 1), (2, 2)) 172 173 self.assertEqual( 174 str(pen).splitlines(), 175 [ 176 "pen.moveTo((0, 0))", 177 "pen.qCurveTo((1, 1), (2, 2))", 178 ], 179 ) 180 181 def test_curveTo_3_points(self): 182 pen = DummyPen() 183 quadpen = Cu2QuPen(pen, MAX_ERR) 184 quadpen.moveTo((0, 0)) 185 quadpen.curveTo((1, 1), (2, 2), (3, 3)) 186 187 self.assertEqual( 188 str(pen).splitlines(), 189 [ 190 "pen.moveTo((0, 0))", 191 "pen.qCurveTo((0.75, 0.75), (2.25, 2.25), (3, 3))", 192 ], 193 ) 194 195 def test_curveTo_more_than_3_points(self): 196 # a 'SuperBezier' as described in fontTools.basePen.AbstractPen 197 pen = DummyPen() 198 quadpen = Cu2QuPen(pen, MAX_ERR) 199 quadpen.moveTo((0, 0)) 200 quadpen.curveTo((1, 1), (2, 2), (3, 3), (4, 4)) 201 202 self.assertEqual( 203 str(pen).splitlines(), 204 [ 205 "pen.moveTo((0, 0))", 206 "pen.qCurveTo((0.75, 0.75), (1.625, 1.625), (2, 2))", 207 "pen.qCurveTo((2.375, 2.375), (3.25, 3.25), (4, 4))", 208 ], 209 ) 210 211 212class TestCu2QuPointPen(unittest.TestCase, _TestPenMixin): 213 def __init__(self, *args, **kwargs): 214 super(TestCu2QuPointPen, self).__init__(*args, **kwargs) 215 self.Glyph = DummyPointGlyph 216 self.Pen = DummyPointPen 217 self.Cu2QuPen = Cu2QuPointPen 218 self.pen_getter_name = "getPointPen" 219 self.draw_method_name = "drawPoints" 220 221 def test_super_bezier_curve(self): 222 pen = DummyPointPen() 223 quadpen = Cu2QuPointPen(pen, MAX_ERR) 224 quadpen.beginPath() 225 quadpen.addPoint((0, 0), segmentType="move") 226 quadpen.addPoint((1, 1)) 227 quadpen.addPoint((2, 2)) 228 quadpen.addPoint((3, 3)) 229 quadpen.addPoint( 230 (4, 4), segmentType="curve", smooth=False, name="up", selected=1 231 ) 232 quadpen.endPath() 233 234 self.assertEqual( 235 str(pen).splitlines(), 236 """\ 237pen.beginPath() 238pen.addPoint((0, 0), name=None, segmentType='move', smooth=False) 239pen.addPoint((0.75, 0.75), name=None, segmentType=None, smooth=False) 240pen.addPoint((1.625, 1.625), name=None, segmentType=None, smooth=False) 241pen.addPoint((2, 2), name=None, segmentType='qcurve', smooth=True) 242pen.addPoint((2.375, 2.375), name=None, segmentType=None, smooth=False) 243pen.addPoint((3.25, 3.25), name=None, segmentType=None, smooth=False) 244pen.addPoint((4, 4), name='up', segmentType='qcurve', selected=1, smooth=False) 245pen.endPath()""".splitlines(), 246 ) 247 248 def test__flushContour_restore_starting_point(self): 249 pen = DummyPointPen() 250 quadpen = Cu2QuPointPen(pen, MAX_ERR) 251 252 # collect the output of _flushContour before it's sent to _drawPoints 253 new_segments = [] 254 255 def _drawPoints(segments): 256 new_segments.extend(segments) 257 Cu2QuPointPen._drawPoints(quadpen, segments) 258 259 quadpen._drawPoints = _drawPoints 260 261 # a closed path (ie. no "move" segmentType) 262 quadpen._flushContour( 263 [ 264 ( 265 "curve", 266 [ 267 ((2, 2), False, None, {}), 268 ((1, 1), False, None, {}), 269 ((0, 0), False, None, {}), 270 ], 271 ), 272 ( 273 "curve", 274 [ 275 ((1, 1), False, None, {}), 276 ((2, 2), False, None, {}), 277 ((3, 3), False, None, {}), 278 ], 279 ), 280 ] 281 ) 282 283 # the original starting point is restored: the last segment has become 284 # the first 285 self.assertEqual(new_segments[0][1][-1][0], (3, 3)) 286 self.assertEqual(new_segments[-1][1][-1][0], (0, 0)) 287 288 new_segments = [] 289 # an open path (ie. starting with "move") 290 quadpen._flushContour( 291 [ 292 ( 293 "move", 294 [ 295 ((0, 0), False, None, {}), 296 ], 297 ), 298 ( 299 "curve", 300 [ 301 ((1, 1), False, None, {}), 302 ((2, 2), False, None, {}), 303 ((3, 3), False, None, {}), 304 ], 305 ), 306 ] 307 ) 308 309 # the segment order stays the same before and after _flushContour 310 self.assertEqual(new_segments[0][1][-1][0], (0, 0)) 311 self.assertEqual(new_segments[-1][1][-1][0], (3, 3)) 312 313 def test_quad_no_oncurve(self): 314 """When passed a contour which has no on-curve points, the 315 Cu2QuPointPen will treat it as a special quadratic contour whose 316 first point has 'None' coordinates. 317 """ 318 self.maxDiff = None 319 pen = DummyPointPen() 320 quadpen = Cu2QuPointPen(pen, MAX_ERR) 321 quadpen.beginPath() 322 quadpen.addPoint((1, 1)) 323 quadpen.addPoint((2, 2)) 324 quadpen.addPoint((3, 3)) 325 quadpen.endPath() 326 327 self.assertEqual( 328 str(pen), 329 dedent( 330 """\ 331 pen.beginPath() 332 pen.addPoint((1, 1), name=None, segmentType=None, smooth=False) 333 pen.addPoint((2, 2), name=None, segmentType=None, smooth=False) 334 pen.addPoint((3, 3), name=None, segmentType=None, smooth=False) 335 pen.endPath()""" 336 ), 337 ) 338 339 340class TestCu2QuMultiPen(unittest.TestCase): 341 def test_multi_pen(self): 342 pens = [RecordingPen(), RecordingPen()] 343 pen = Cu2QuMultiPen(pens, 0.1) 344 pen.moveTo([((0, 0),), ((0, 0),)]) 345 pen.lineTo([((0, 1),), ((0, 1),)]) 346 pen.qCurveTo([((0, 2),), ((0, 2),)]) 347 pen.qCurveTo([((0, 3), (1, 3)), ((0, 3), (1, 4))]) 348 pen.curveTo([((2, 3), (0, 3), (0, 0)), ((1.1, 4), (0, 4), (0, 0))]) 349 pen.closePath() 350 351 assert len(pens[0].value) == 6 352 assert len(pens[1].value) == 6 353 354 for op0, op1 in zip(pens[0].value, pens[1].value): 355 assert op0[0] == op0[0] 356 assert op0[0] != "curveTo" 357 358 359class TestAllQuadraticFalse(unittest.TestCase): 360 def test_segment_pen_cubic(self): 361 rpen = RecordingPen() 362 pen = Cu2QuPen(rpen, 0.1, all_quadratic=False) 363 364 pen.moveTo((0, 0)) 365 pen.curveTo((0, 1), (2, 1), (2, 0)) 366 pen.closePath() 367 368 assert rpen.value == [ 369 ("moveTo", ((0, 0),)), 370 ("curveTo", ((0, 1), (2, 1), (2, 0))), 371 ("closePath", ()), 372 ] 373 374 def test_segment_pen_quadratic(self): 375 rpen = RecordingPen() 376 pen = Cu2QuPen(rpen, 0.1, all_quadratic=False) 377 378 pen.moveTo((0, 0)) 379 pen.curveTo((2, 2), (4, 2), (6, 0)) 380 pen.closePath() 381 382 assert rpen.value == [ 383 ("moveTo", ((0, 0),)), 384 ("qCurveTo", ((3, 3), (6, 0))), 385 ("closePath", ()), 386 ] 387 388 def test_point_pen_cubic(self): 389 rpen = RecordingPointPen() 390 pen = Cu2QuPointPen(rpen, 0.1, all_quadratic=False) 391 392 pen.beginPath() 393 pen.addPoint((0, 0), "move") 394 pen.addPoint((0, 1)) 395 pen.addPoint((2, 1)) 396 pen.addPoint((2, 0), "curve") 397 pen.endPath() 398 399 assert rpen.value == [ 400 ("beginPath", (), {}), 401 ("addPoint", ((0, 0), "move", False, None), {}), 402 ("addPoint", ((0, 1), None, False, None), {}), 403 ("addPoint", ((2, 1), None, False, None), {}), 404 ("addPoint", ((2, 0), "curve", False, None), {}), 405 ("endPath", (), {}), 406 ] 407 408 def test_point_pen_quadratic(self): 409 rpen = RecordingPointPen() 410 pen = Cu2QuPointPen(rpen, 0.1, all_quadratic=False) 411 412 pen.beginPath() 413 pen.addPoint((0, 0), "move") 414 pen.addPoint((2, 2)) 415 pen.addPoint((4, 2)) 416 pen.addPoint((6, 0), "curve") 417 pen.endPath() 418 419 assert rpen.value == [ 420 ("beginPath", (), {}), 421 ("addPoint", ((0, 0), "move", False, None), {}), 422 ("addPoint", ((3, 3), None, False, None), {}), 423 ("addPoint", ((6, 0), "qcurve", False, None), {}), 424 ("endPath", (), {}), 425 ] 426 427 428if __name__ == "__main__": 429 unittest.main() 430