From 3a1fe8695dd6236af81b5477714e74a094e3b088 Mon Sep 17 00:00:00 2001 From: tatarize Date: Sat, 16 Jan 2021 20:08:58 -0800 Subject: [PATCH] Bug Corrections. Closes #113, #95, #94, and #71 (#136) * Closes #113 Previous fix stopped working because numpy is more liberal dividing by zero and returning nan values. * Closes #71 * Closes #95 * Closes #94 --- svgpathtools/path.py | 59 ++++++++++++++++++++++++-------------------- test/test_path.py | 47 +++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 54ddac4..9f6ea3f 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -18,7 +18,7 @@ from itertools import tee # in order to encourage code that generalizes to vector inputs from numpy import sqrt, cos, sin, tan, arccos as acos, arcsin as asin, \ degrees, radians, log, pi, ceil -from numpy import exp, sqrt as csqrt, angle as phase +from numpy import exp, sqrt as csqrt, angle as phase, isnan try: from scipy.integrate import quad @@ -898,31 +898,30 @@ class QuadraticBezier(object): if abs(a) < 1e-12: s = abs(b)*(t1 - t0) - elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12: - tstar = abs(b)/(2*abs(a)) - if t1 < tstar: - return abs(a)*(t0**2 - t1**2) - abs(b)*(t0 - t1) - elif tstar < t0: - return abs(a)*(t1**2 - t0**2) - abs(b)*(t1 - t0) - else: - return abs(a)*(t1**2 + t0**2) - abs(b)*(t1 + t0) + \ - abs(b)**2/(2*abs(a)) else: - c2 = 4*(a.real**2 + a.imag**2) - c1 = 4*a_dot_b - c0 = b.real**2 + b.imag**2 + c2 = 4 * (a.real ** 2 + a.imag ** 2) + c1 = 4 * a_dot_b + c0 = b.real ** 2 + b.imag ** 2 - beta = c1/(2*c2) - gamma = c0/c2 - beta**2 + beta = c1 / (2 * c2) + gamma = c0 / c2 - beta ** 2 - dq1_mag = sqrt(c2*t1**2 + c1*t1 + c0) - dq0_mag = sqrt(c2*t0**2 + c1*t0 + c0) - logarand = (sqrt(c2)*(t1 + beta) + dq1_mag) / \ - (sqrt(c2)*(t0 + beta) + dq0_mag) - - s = (t1 + beta)*dq1_mag - (t0 + beta)*dq0_mag + \ - gamma*sqrt(c2)*log(logarand) + dq1_mag = sqrt(c2 * t1 ** 2 + c1 * t1 + c0) + dq0_mag = sqrt(c2 * t0 ** 2 + c1 * t0 + c0) + logarand = (sqrt(c2) * (t1 + beta) + dq1_mag) / \ + (sqrt(c2) * (t0 + beta) + dq0_mag) + s = (t1 + beta) * dq1_mag - (t0 + beta) * dq0_mag + \ + gamma * sqrt(c2) * log(logarand) s /= 2 + if isnan(s): + tstar = abs(b) / (2 * abs(a)) + if t1 < tstar: + return abs(a) * (t0 ** 2 - t1 ** 2) - abs(b) * (t0 - t1) + elif tstar < t0: + return abs(a) * (t1 ** 2 - t0 ** 2) - abs(b) * (t1 - t0) + else: + return abs(a) * (t1 ** 2 + t0 ** 2) - abs(b) * (t1 + t0) + \ + abs(b) ** 2 / (2 * abs(a)) if t0 == 1 and t1 == 0: self._length_info['length'] = s @@ -2445,7 +2444,10 @@ class Path(MutableSequence): lengths = [each.length(error=error, min_depth=min_depth) for each in self._segments] self._length = sum(lengths) - self._lengths = [each/self._length for each in lengths] + if self._length == 0: + self._lengths = lengths # all lengths are 0. + else: + self._lengths = [each / self._length for each in lengths] def point(self, pos): @@ -2522,7 +2524,10 @@ class Path(MutableSequence): return self.start == self.end def _is_closable(self): - end = self[-1].end + try: + end = self[-1].end + except IndexError: + return True for segment in self: if segment.start == end: return True @@ -2574,7 +2579,8 @@ class Path(MutableSequence): """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 len(self) == 0: + return '' if use_closed_attrib: self_closed = self.iscontinuous() and self.isclosed() if self_closed: @@ -2866,8 +2872,7 @@ class Path(MutableSequence): # redundant intersection. This code block checks for and removes said # redundancies. if intersection_list: - pts = [seg1.point(_t1) - for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]] + pts = [_seg1.point(_t1) for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]] indices2remove = [] for ind1 in range(len(pts)): for ind2 in range(ind1 + 1, len(pts)): diff --git a/test/test_path.py b/test/test_path.py index 3769bd0..5b7d448 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -687,7 +687,6 @@ class ArcTest(unittest.TestCase): self.assertAlmostEqual(d,0.0, delta=2) - class TestPath(unittest.TestCase): def test_circle(self): @@ -1660,7 +1659,6 @@ class Test_intersect(unittest.TestCase): assert_intersections(a0, a1, intersections, 0) - class TestPathTools(unittest.TestCase): # moved from test_pathtools.py @@ -1950,5 +1948,50 @@ class TestPathTools(unittest.TestCase): self.assertTrue(enclosing_shape.is_contained_by(larger_shape)) +class TestPathBugs(unittest.TestCase): + + def test_issue_113(self): + """ + Tests against issue regebro/svg.path#61 mathandy/svgpathtools#113 + """ + p = Path('M 206.5,525 Q 162.5,583 162.5,583') + self.assertAlmostEqual(p.length(), 72.80109889280519) + p = Path('M 425.781 446.289 Q 410.40000000000003 373.047 410.4 373.047') + self.assertAlmostEqual(p.length(), 74.83959997888816) + p = Path('M 639.648 568.115 Q 606.6890000000001 507.568 606.689 507.568') + self.assertAlmostEqual(p.length(), 68.93645544992873) + p = Path('M 288.818 616.699 Q 301.025 547.3629999999999 301.025 547.363') + self.assertAlmostEqual(p.length(), 70.40235610403947) + p = Path('M 339.927 706.25 Q 243.92700000000002 806.25 243.927 806.25') + self.assertAlmostEqual(p.length(), 138.6217876093077) + p = Path('M 539.795 702.637 Q 548.0959999999999 803.4669999999999 548.096 803.467') + self.assertAlmostEqual(p.length(), 101.17111989594662) + p = Path('M 537.815 555.042 Q 570.1680000000001 499.1600000000001 570.168 499.16') + self.assertAlmostEqual(p.length(), 64.57177814649368) + p = Path('M 615.297 470.503 Q 538.797 694.5029999999999 538.797 694.503') + self.assertAlmostEqual(p.length(), 236.70287281737836) + + def test_issue_71(self): + p = Path("M327 468z") + m = p.closed + q = p.d() # Failing to Crash is good. + + def test_issue_95(self): + """ + Corrects: + https://github.com/mathandy/svgpathtools/issues/95 + """ + p = Path('M261 166 L261 166') + self.assertEqual(p.length(), 0) + + def test_issue_94(self): + # clipping rectangle + p1 = Path('M0.0 0.0 L27.84765625 0.0 L27.84765625 242.6669922 L0.0 242.6669922 z') + # clipping rectangle + p2 = Path('M166.8359375,235.5478516c0,3.7773438-3.0859375,6.8691406-6.8701172,6.8691406H7.1108398c-3.7749023,0-6.8608398-3.0917969-6.8608398-6.8691406V7.1201172C0.25,3.3427734,3.3359375,0.25,7.1108398,0.25h152.8549805c3.7841797,0,6.8701172,3.0927734,6.8701172,6.8701172v228.4277344z') + self.assertEqual(len(p1.intersect(p2)), len(p2.intersect(p1))) + + + if __name__ == '__main__': unittest.main()