From c7b6c030a6b78cb02d8603ae386d7efa6e0d1e1e Mon Sep 17 00:00:00 2001 From: taoari Date: Fri, 19 Jun 2020 21:43:29 -0400 Subject: [PATCH] relative path support (#81) * relative path support * add test for path.d(rel=True) * add path test to test_path.py --- svgpathtools/path.py | 80 ++++++++++++++++++++++++++++++++------------ test/test_path.py | 11 +++++- 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 6e43bf3..dccfc0e 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -2265,13 +2265,13 @@ class Path(MutableSequence): self._end = pt self._segments[-1].end = pt - def d(self, useSandT=False, use_closed_attrib=False): + def d(self, useSandT=False, use_closed_attrib=False, rel=False): """Returns a path d-string for the path object. For an explanation of useSandT and use_closed_attrib, see the compatibility notes in the README.""" - + if use_closed_attrib: - self_closed = self.closed(warning_on=False) + self_closed = self.iscontinuous() and self.isclosed() if self_closed: segments = self[:-1] else: @@ -2279,12 +2279,12 @@ class Path(MutableSequence): else: self_closed = False segments = self[:] - + current_pos = None parts = [] previous_segment = None end = self[-1].end - + for segment in segments: seg_start = segment.start # If the start of this segment does not coincide with the end of @@ -2292,44 +2292,80 @@ class Path(MutableSequence): # of a closed path, then we should start a new subpath here. if current_pos != seg_start or \ (self_closed and seg_start == end and use_closed_attrib): - parts.append('M {},{}'.format(seg_start.real, seg_start.imag)) - + if rel: + _seg_start = seg_start - current_pos if current_pos is not None else seg_start + else: + _seg_start = seg_start + parts.append('M {},{}'.format(_seg_start.real, _seg_start.imag)) + if isinstance(segment, Line): - args = segment.end.real, segment.end.imag - parts.append('L {},{}'.format(*args)) + if rel: + _seg_end = segment.end - seg_start + else: + _seg_end = segment.end + parts.append('L {},{}'.format(_seg_end.real, _seg_end.imag)) elif isinstance(segment, CubicBezier): if useSandT and segment.is_smooth_from(previous_segment, warning_on=False): - args = (segment.control2.real, segment.control2.imag, - segment.end.real, segment.end.imag) + if rel: + _seg_control2 = segment.control2 - seg_start + _seg_end = segment.end - seg_start + else: + _seg_control2 = segment.control2 + _seg_end = segment.end + args = (_seg_control2.real, _seg_control2.imag, + _seg_end.real, _seg_end.imag) parts.append('S {},{} {},{}'.format(*args)) else: - args = (segment.control1.real, segment.control1.imag, - segment.control2.real, segment.control2.imag, - segment.end.real, segment.end.imag) + if rel: + _seg_control1 = segment.control1 - seg_start + _seg_control2 = segment.control2 - seg_start + _seg_end = segment.end - seg_start + else: + _seg_control1 = segment.control1 + _seg_control2 = segment.control2 + _seg_end = segment.end + args = (_seg_control1.real, _seg_control1.imag, + _seg_control2.real, _seg_control2.imag, + _seg_end.real, _seg_end.imag) parts.append('C {},{} {},{} {},{}'.format(*args)) elif isinstance(segment, QuadraticBezier): if useSandT and segment.is_smooth_from(previous_segment, warning_on=False): - args = segment.end.real, segment.end.imag + if rel: + _seg_end = segment.end - seg_start + else: + _seg_end = segment.end + args = _seg_end.real, _seg_end.imag parts.append('T {},{}'.format(*args)) else: - args = (segment.control.real, segment.control.imag, - segment.end.real, segment.end.imag) + if rel: + _seg_control = segment.control - seg_start + _seg_end = segment.end - seg_start + else: + _seg_control = segment.control + _seg_end = segment.end + args = (_seg_control.real, _seg_control.imag, + _seg_end.real, _seg_end.imag) parts.append('Q {},{} {},{}'.format(*args)) - + elif isinstance(segment, Arc): + if rel: + _seg_end = segment.end - seg_start + else: + _seg_end = segment.end args = (segment.radius.real, segment.radius.imag, segment.rotation,int(segment.large_arc), - int(segment.sweep),segment.end.real, segment.end.imag) + int(segment.sweep),_seg_end.real, _seg_end.imag) parts.append('A {},{} {} {:d},{:d} {},{}'.format(*args)) current_pos = segment.end previous_segment = segment - + if self_closed: parts.append('Z') - - return ' '.join(parts) + + s = ' '.join(parts) + return s if not rel else s.lower() def joins_smoothly_with(self, previous, wrt_parameterization=False): """Checks if this Path object joins smoothly with previous diff --git a/test/test_path.py b/test/test_path.py index 079bb84..c8cec44 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -1052,7 +1052,16 @@ class TestPath(unittest.TestCase): pt_xpct = (pt_orig - complex(0, -100)) * 0.3 + complex(0, -100) self.assertAlmostEqual(pt_xpct, pt_trns, delta=0.000001) - + def test_d(self): + # the following two path represent the same path but in absolute and relative forms + abs_s = 'M 38.0,130.0 C 37.0,132.0 38.0,136.0 40.0,137.0 L 85.0,161.0 C 87.0,162.0 91.0,162.0 93.0,160.0 L 127.0,133.0 C 129.0,131.0 129.0,128.0 127.0,126.0 L 80.0,70.0 C 78.0,67.0 75.0,68.0 74.0,70.0 Z' + rel_s = 'm 38.0,130.0 c -1.0,2.0 0.0,6.0 2.0,7.0 l 45.0,24.0 c 2.0,1.0 6.0,1.0 8.0,-1.0 l 34.0,-27.0 c 2.0,-2.0 2.0,-5.0 0.0,-7.0 l -47.0,-56.0 c -2.0,-3.0 -5.0,-2.0 -6.0,0.0 z' + path1 = parse_path(abs_s) + path2 = parse_path(rel_s) + self.assertEqual(path1.d(use_closed_attrib=True), abs_s) + self.assertEqual(path2.d(use_closed_attrib=True), abs_s) + self.assertEqual(path1.d(use_closed_attrib=True, rel=True), rel_s) + self.assertEqual(path2.d(use_closed_attrib=True, rel=True), rel_s) class Test_ilength(unittest.TestCase):