The PointToSegmentPen translates between PointPen and (Segment)Pen protocol. In the SegmentPen protocol, closed contours always imply a final 'lineTo' segment from the last oncurve point to the starting point. So the PointToSegmentPen omits the final 'lineTo' segment for closed contours -- unless the option 'outputImpliedClosingLine' is True (it is False by default, and defcon.Glyph.draw method initializes the converter pen without this option). However, if the last oncurve point is on a "line" segment and has same coordinates as the starting point of a closed contour, the converter pen must always output the closing 'lineTo' explicitly (regardless of the value of the 'outputImpliedClosingLine' option) in order to disambiguate this case from the implied closing 'lineTo'. If it doesn't do that, a duplicate 'line' point at the end of a closed contour gets lost in the conversion. See https://github.com/googlefonts/fontmake/issues/572.
337 lines
9.3 KiB
Python
337 lines
9.3 KiB
Python
from fontTools.pens.recordingPen import RecordingPen
|
|
from fontTools.pens.reverseContourPen import ReverseContourPen
|
|
import pytest
|
|
|
|
|
|
TEST_DATA = [
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('lineTo', ((2, 2),)),
|
|
('lineTo', ((3, 3),)), # last not on move, line is implied
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((3, 3),)),
|
|
('lineTo', ((2, 2),)),
|
|
('lineTo', ((1, 1),)),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('lineTo', ((2, 2),)),
|
|
('lineTo', ((0, 0),)), # last on move, no implied line
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((2, 2),)),
|
|
('lineTo', ((1, 1),)),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('lineTo', ((2, 2),)),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((2, 2),)),
|
|
('lineTo', ((1, 1),)),
|
|
('lineTo', ((0, 0),)),
|
|
('lineTo', ((0, 0),)),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('curveTo', ((1, 1), (2, 2), (3, 3))),
|
|
('curveTo', ((4, 4), (5, 5), (0, 0))),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('curveTo', ((5, 5), (4, 4), (3, 3))),
|
|
('curveTo', ((2, 2), (1, 1), (0, 0))),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('curveTo', ((1, 1), (2, 2), (3, 3))),
|
|
('curveTo', ((4, 4), (5, 5), (6, 6))),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((6, 6),)), # implied line
|
|
('curveTo', ((5, 5), (4, 4), (3, 3))),
|
|
('curveTo', ((2, 2), (1, 1), (0, 0))),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)), # this line becomes implied
|
|
('curveTo', ((2, 2), (3, 3), (4, 4))),
|
|
('curveTo', ((5, 5), (6, 6), (7, 7))),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((7, 7),)),
|
|
('curveTo', ((6, 6), (5, 5), (4, 4))),
|
|
('curveTo', ((3, 3), (2, 2), (1, 1))),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('qCurveTo', ((1, 1), (2, 2))),
|
|
('qCurveTo', ((3, 3), (0, 0))),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('qCurveTo', ((3, 3), (2, 2))),
|
|
('qCurveTo', ((1, 1), (0, 0))),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('qCurveTo', ((1, 1), (2, 2))),
|
|
('qCurveTo', ((3, 3), (4, 4))),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((4, 4),)),
|
|
('qCurveTo', ((3, 3), (2, 2))),
|
|
('qCurveTo', ((1, 1), (0, 0))),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('qCurveTo', ((2, 2), (3, 3))),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((3, 3),)),
|
|
('qCurveTo', ((2, 2), (1, 1))),
|
|
('closePath', ()),
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('addComponent', ('a', (1, 0, 0, 1, 0, 0)))
|
|
],
|
|
[
|
|
('addComponent', ('a', (1, 0, 0, 1, 0, 0)))
|
|
]
|
|
),
|
|
(
|
|
[], []
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('endPath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('endPath', ()),
|
|
],
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('closePath', ()),
|
|
],
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('endPath', ()), # single-point paths is always open
|
|
],
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('endPath', ())
|
|
],
|
|
[
|
|
('moveTo', ((1, 1),)),
|
|
('lineTo', ((0, 0),)),
|
|
('endPath', ())
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('curveTo', ((1, 1), (2, 2), (3, 3))),
|
|
('endPath', ())
|
|
],
|
|
[
|
|
('moveTo', ((3, 3),)),
|
|
('curveTo', ((2, 2), (1, 1), (0, 0))),
|
|
('endPath', ())
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('curveTo', ((1, 1), (2, 2), (3, 3))),
|
|
('lineTo', ((4, 4),)),
|
|
('endPath', ())
|
|
],
|
|
[
|
|
('moveTo', ((4, 4),)),
|
|
('lineTo', ((3, 3),)),
|
|
('curveTo', ((2, 2), (1, 1), (0, 0))),
|
|
('endPath', ())
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('moveTo', ((0, 0),)),
|
|
('lineTo', ((1, 1),)),
|
|
('curveTo', ((2, 2), (3, 3), (4, 4))),
|
|
('endPath', ())
|
|
],
|
|
[
|
|
('moveTo', ((4, 4),)),
|
|
('curveTo', ((3, 3), (2, 2), (1, 1))),
|
|
('lineTo', ((0, 0),)),
|
|
('endPath', ())
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('qCurveTo', ((0, 0), (1, 1), (2, 2), None)),
|
|
('closePath', ())
|
|
],
|
|
[
|
|
('qCurveTo', ((0, 0), (2, 2), (1, 1), None)),
|
|
('closePath', ())
|
|
]
|
|
),
|
|
(
|
|
[
|
|
('qCurveTo', ((0, 0), (1, 1), (2, 2), None)),
|
|
('endPath', ())
|
|
],
|
|
[
|
|
('qCurveTo', ((0, 0), (2, 2), (1, 1), None)),
|
|
('closePath', ()) # this is always "closed"
|
|
]
|
|
),
|
|
# Test case from:
|
|
# https://github.com/googlei18n/cu2qu/issues/51#issue-179370514
|
|
(
|
|
[
|
|
('moveTo', ((848, 348),)),
|
|
('lineTo', ((848, 348),)), # duplicate lineTo point after moveTo
|
|
('qCurveTo', ((848, 526), (649, 704), (449, 704))),
|
|
('qCurveTo', ((449, 704), (248, 704), (50, 526), (50, 348))),
|
|
('lineTo', ((50, 348),)),
|
|
('qCurveTo', ((50, 348), (50, 171), (248, -3), (449, -3))),
|
|
('qCurveTo', ((449, -3), (649, -3), (848, 171), (848, 348))),
|
|
('closePath', ())
|
|
],
|
|
[
|
|
('moveTo', ((848, 348),)),
|
|
('qCurveTo', ((848, 171), (649, -3), (449, -3), (449, -3))),
|
|
('qCurveTo', ((248, -3), (50, 171), (50, 348), (50, 348))),
|
|
('lineTo', ((50, 348),)),
|
|
('qCurveTo', ((50, 526), (248, 704), (449, 704), (449, 704))),
|
|
('qCurveTo', ((649, 704), (848, 526), (848, 348))),
|
|
('lineTo', ((848, 348),)), # the duplicate point is kept
|
|
('closePath', ())
|
|
]
|
|
),
|
|
# Test case from https://github.com/googlefonts/fontmake/issues/572
|
|
# An additional closing lineTo is required to disambiguate a duplicate
|
|
# point at the end of a contour from the implied closing line.
|
|
(
|
|
[
|
|
('moveTo', ((0, 651),)),
|
|
('lineTo', ((0, 101),)),
|
|
('lineTo', ((0, 101),)),
|
|
('lineTo', ((0, 651),)),
|
|
('lineTo', ((0, 651),)),
|
|
('closePath', ())
|
|
],
|
|
[
|
|
('moveTo', ((0, 651),)),
|
|
('lineTo', ((0, 651),)),
|
|
('lineTo', ((0, 101),)),
|
|
('lineTo', ((0, 101),)),
|
|
('closePath', ())
|
|
]
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("contour, expected", TEST_DATA)
|
|
def test_reverse_pen(contour, expected):
|
|
recpen = RecordingPen()
|
|
revpen = ReverseContourPen(recpen)
|
|
for operator, operands in contour:
|
|
getattr(revpen, operator)(*operands)
|
|
assert recpen.value == expected
|
|
|
|
|
|
@pytest.mark.parametrize("contour, expected", TEST_DATA)
|
|
def test_reverse_point_pen(contour, expected):
|
|
from fontTools.ufoLib.pointPen import (
|
|
ReverseContourPointPen, PointToSegmentPen, SegmentToPointPen)
|
|
|
|
recpen = RecordingPen()
|
|
pt2seg = PointToSegmentPen(recpen, outputImpliedClosingLine=True)
|
|
revpen = ReverseContourPointPen(pt2seg)
|
|
seg2pt = SegmentToPointPen(revpen)
|
|
for operator, operands in contour:
|
|
getattr(seg2pt, operator)(*operands)
|
|
|
|
# for closed contours that have a lineTo following the moveTo,
|
|
# and whose points don't overlap, our current implementation diverges
|
|
# from the ReverseContourPointPen as wrapped by ufoLib's pen converters.
|
|
# In the latter case, an extra lineTo is added because of
|
|
# outputImpliedClosingLine=True. This is redundant but not incorrect,
|
|
# as the number of points is the same in both.
|
|
if (contour and contour[-1][0] == "closePath" and
|
|
contour[1][0] == "lineTo" and contour[1][1] != contour[0][1]):
|
|
expected = expected[:-1] + [("lineTo", contour[0][1])] + expected[-1:]
|
|
|
|
assert recpen.value == expected
|