| # Copyright 2016 Google Inc. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import sys |
| import unittest |
| |
| from fontTools.pens.cu2quPen import Cu2QuPen, Cu2QuPointPen |
| from . import CUBIC_GLYPHS, QUAD_GLYPHS |
| from .utils import DummyGlyph, DummyPointGlyph |
| from .utils import DummyPen, DummyPointPen |
| from fontTools.misc.loggingTools import CapturingLogHandler |
| from textwrap import dedent |
| import logging |
| |
| |
| 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. |
| """ |
| |
| maxDiff = None |
| |
| 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 not converted.approx(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('1' in stats) |
| self.assertEqual(type(stats['1']), 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() |
| try: |
| logging.captureWarnings(True) |
| with CapturingLogHandler("py.warnings", level="WARNING") as log: |
| quadpen = Cu2QuPen(pen, MAX_ERR, ignore_single_points=True) |
| finally: |
| logging.captureWarnings(False) |
| quadpen.moveTo((0, 0)) |
| quadpen.endPath() |
| quadpen.moveTo((1, 1)) |
| quadpen.closePath() |
| |
| self.assertGreaterEqual(len(log.records), 1) |
| if sys.version_info < (3, 11): |
| self.assertIn("ignore_single_points is deprecated", |
| log.records[0].args[0]) |
| else: |
| self.assertIn("ignore_single_points is deprecated", |
| log.records[0].msg) |
| |
| # 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)) |
| |
| def test_quad_no_oncurve(self): |
| """When passed a contour which has no on-curve points, the |
| Cu2QuPointPen will treat it as a special quadratic contour whose |
| first point has 'None' coordinates. |
| """ |
| self.maxDiff = None |
| pen = DummyPointPen() |
| quadpen = Cu2QuPointPen(pen, MAX_ERR) |
| quadpen.beginPath() |
| quadpen.addPoint((1, 1)) |
| quadpen.addPoint((2, 2)) |
| quadpen.addPoint((3, 3)) |
| quadpen.endPath() |
| |
| self.assertEqual( |
| str(pen), |
| dedent( |
| """\ |
| pen.beginPath() |
| pen.addPoint((1, 1), name=None, segmentType=None, smooth=False) |
| pen.addPoint((2, 2), name=None, segmentType=None, smooth=False) |
| pen.addPoint((3, 3), name=None, segmentType=None, smooth=False) |
| pen.endPath()""" |
| ) |
| ) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |