Repository: meerk40t/svgelements

Issue:
Use scipy if available for CubicBezier.length()
While trying to benchmark bezier curves (#52), I ran into the issue that `CubicBezier.length()`is *extremely* slow, due to using a linearisation approach. When available, `svgpathtools` uses scipy for this task:

```python
    def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
        """Calculate the length of the path up to a certain position"""
       
        # ...

        # using scipy.integrate.quad is quick
        if _quad_available:
            s = quad(lambda tau: abs(self.derivative(tau)), t0, t1,
                            epsabs=error, limit=1000)[0]
        else:
            s = segment_length(self, t0, t1, self.point(t0), self.point(t1),
                               error, min_depth, 0)

        # ...

        return s

    def derivative(self, t, n=1):
        """returns the nth derivative of the segment at t.
        Note: Bezier curves can have points where their derivative vanishes.
        If you are interested in the tangent direction, use the unit_tangent()
        method instead."""
        p = self.bpoints()
        if n == 1:
            return 3*(p[1] - p[0])*(1 - t)**2 + 6*(p[2] - p[1])*(1 - t)*t + 3*(
                p[3] - p[2])*t**2
        elif n == 2:
            return 6*(
                (1 - t)*(p[2] - 2*p[1] + p[0]) + t*(p[3] - 2*p[2] + p[1]))
        elif n == 3:
            return 6*(p[3] - 3*(p[2] - p[1]) - p[0])
        elif n > 3:
            return 0
        else:
            raise ValueError("n should be a positive integer.")
```

@tatarize would you be ok to have such an approach in `svgelements`?

Discussion:

Patch:
diff --git a/svgelements/svgelements.py b/svgelements/svgelements.py
index ed0e7a3..a1877c8 100644
--- a/svgelements/svgelements.py
+++ b/svgelements/svgelements.py
@@ -870,8 +870,6 @@ class Length(object):
 
     @staticmethod
     def str(s):
-        if s is None:
-            return "n/a"
         if isinstance(s, Length):
             if s.units == '':
                 s = s.amount
@@ -3674,9 +3672,30 @@ class CubicBezier(PathSegment):
         local_extrema = [self.point(t)[v] for t in local_extremizers]
         return min(local_extrema), max(local_extrema)
 
+    def _derivative(self, t):
+        """returns the nth derivative of the segment at t.
+        Note: Bezier curves can have points where their derivative vanishes.
+        If you are interested in the tangent direction, use the unit_tangent()
+        method instead."""
+        p = [self.start, self.control1, self.control2, self.end]
+        return 3 * (p[1] - p[0]) * (1 - t) ** 2 + 6 * (p[2] - p[1]) * (
+                    1 - t) * t + 3 * (
+                           p[3] - p[2]) * t ** 2
+
+    def _length_scipy(self, error=ERROR):
+        from scipy.integrate import quad
+        return quad(lambda tau: abs(self._derivative(tau)), 0., 1.,
+                    epsabs=error, limit=1000)[0]
+
+    def _length_default(self, error=ERROR, min_depth=MIN_DEPTH):
+        return self._line_length(0, 1, error, min_depth)
+
     def length(self, error=ERROR, min_depth=MIN_DEPTH):
         """Calculate the length of the path up to a certain position"""
-        return self._line_length(0, 1, error, min_depth)
+        try:
+            return self._length_scipy(error)
+        except ModuleNotFoundError:
+            return self._length_default(error, min_depth)
 
     def is_smooth_from(self, previous):
         """Checks if this segment would be a smooth segment following the previous"""


Test patch:
diff --git a/test/test_cubic_bezier_length.py b/test/test_cubic_bezier_length.py
new file mode 100644
index 0000000..6685b20
--- /dev/null
+++ b/test/test_cubic_bezier_length.py
@@ -0,0 +1,21 @@
+from __future__ import print_function
+
+import unittest
+from random import *
+
+from svgelements import *
+
+
+def get_random_cubic_bezier():
+    return CubicBezier((random() * 50, random() * 50), (random() * 50, random() * 50),
+                       (random() * 50, random() * 50), (random() * 50, random() * 50))
+
+
+class TestElementCubicBezierLength(unittest.TestCase):
+
+    def test_cubic_bezier_length(self):
+        for _ in range(100):
+            b = get_random_cubic_bezier()
+            l1 = b._length_scipy()
+            l2 = b._length_default()
+            self.assertAlmostEqual(l1, l2)
diff --git a/test/test_viewbox.py b/test/test_viewbox.py
index 72669af..f8a3f0f 100644
--- a/test/test_viewbox.py
+++ b/test/test_viewbox.py
@@ -31,11 +31,4 @@ class TestElementViewbox(unittest.TestCase):
 
     def test_viewbox_translate(self):
         v = Viewbox({'viewBox': '-50 -50 100 100', 'height': 100, 'width': 100})
-        self.assertEqual(v.transform(), 'translate(50, 50)')
-
-    def test_viewbox_incomplete_print(self):
-        v = Viewbox({'viewBox': None, 'height': 200, 'width': 200})
-        try:
-            str(v)
-        except TypeError:
-            self.fail("str(viewbox) should not fail for incomplete viewbox")
\ No newline at end of file
+        self.assertEqual(v.transform(), 'translate(50, 50)')
\ No newline at end of file