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