Metadata-Version: 1.1 Name: svgpathtools Version: 1.3 Summary: A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves. Home-page: https://github.com/mathandy/svgpathtools Author: Andy Port Author-email: AndyAPort@gmail.com License: MIT Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3 Description: svgpathtools ============ svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves. Features -------- svgpathtools contains functions designed to **easily read, write and display SVG files** as well as *a large selection of geometrically-oriented tools* to **transform and analyze path elements**. Additionally, the submodule *bezier.py* contains tools for for working with general **nth order Bezier curves stored as n-tuples**. Some included tools: - **read**, **write**, and **display** SVG files containing Path (and other) SVG elements - convert Bézier path segments to **numpy.poly1d** (polynomial) objects - convert polynomials (in standard form) to their Bézier form - compute **tangent vectors** and (right-hand rule) **normal vectors** - compute **curvature** - break discontinuous paths into their **continuous subpaths**. - efficiently compute **intersections** between paths and/or segments - find a **bounding box** for a path or segment - **reverse** segment/path orientation - **crop** and **split** paths and segments - **smooth** paths (i.e. smooth away kinks to make paths differentiable) - **transition maps** from path domain to segment domain and back (T2t and t2T) - compute **area** enclosed by a closed path - compute **arc length** - compute **inverse arc length** - convert RGB color tuples to hexadecimal color strings and back Note on Python 3 ---------------- While I am hopeful that this package entirely works with Python 3, it was born from a larger project coded in Python 2 and has not been thoroughly tested in Python 3. Please let me know if you find any incompatibilities. Prerequisites ------------- - **numpy** - **svgwrite** Setup ----- If not already installed, you can **install the prerequisites** using pip. .. code:: bash $ pip install numpy .. code:: bash $ pip install svgwrite Then **install svgpathtools**: .. code:: bash $ pip install svgpathtools Alternative Setup ~~~~~~~~~~~~~~~~~ You can download the source from Github and install by using the command (from inside the folder containing setup.py): .. code:: bash $ python setup.py install Credit where credit's due ------------------------- Much of the core of this module was taken from `the svg.path (v2.0) module `__. Interested svg.path users should see the compatibility notes at bottom of this readme. Also, a big thanks to the author(s) of `A Primer on Bézier Curves `_, an outstanding resource for learning about Bézier curves and Bézier curve-related algorithms. Basic Usage ----------- Classes ~~~~~~~ The svgpathtools module is primarily structured around four path segment classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. There is also a fifth class, ``Path``, whose objects are sequences of (connected or disconnected\ `1 <#f1>`__\ ) path segment objects. - ``Line(start, end)`` - ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See docstring for a detailed explanation of these parameters - ``QuadraticBezier(start, control, end)`` - ``CubicBezier(start, control1, control2, end)`` - ``Path(*segments)`` See the relevant docstrings in *path.py* or the `official SVG specifications `__ for more information on what each parameter means. 1 Warning: Some of the functionality in this library has not been tested on discontinuous Path objects. A simple workaround is provided, however, by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__ .. code:: python from __future__ import division, print_function .. code:: python # Coordinates are given as points in the complex plane from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300) seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350) path = Path(seg1, seg2) # A path traversing the cubic and then the line # We could alternatively created this Path object using a d-string from svgpathtools import parse_path path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350') # Let's check that these two methods are equivalent print(path) print(path_alt) print(path == path_alt) # On a related note, the Path.d() method returns a Path object's d-string print(path.d()) print(parse_path(path.d()) == path) .. parsed-literal:: Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), Line(start=(200+300j), end=(250+350j))) Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), Line(start=(200+300j), end=(250+350j))) True M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0 True The ``Path`` class is a mutable sequence, so it behaves much like a list. So segments can **append**\ ed, **insert**\ ed, set by index, **del**\ eted, **enumerate**\ d, **slice**\ d out, etc. .. code:: python # Let's append another to the end of it path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j)) print(path) # Let's replace the first segment with a Line object path[0] = Line(200+100j, 200+300j) print(path) # You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end) print("path is continuous? ", path.iscontinuous()) print("path is closed? ", path.isclosed()) # The curve the path follows is not, however, smooth (differentiable) from svgpathtools import kinks, smoothed_path print("path contains non-differentiable points? ", len(kinks(path)) > 0) # If we want, we can smooth these out (Experimental and only for line/cubic paths) # Note: smoothing will always works (except on 180 degree turns), but you may want # to play with the maxjointsize and tightness parameters to get pleasing results # Note also: smoothing will increase the number of segments in a path spath = smoothed_path(path) print("spath contains non-differentiable points? ", len(kinks(spath)) > 0) print(spath) # Let's take a quick look at the path and its smoothed relative # The following commands will open two browser windows to display path and spaths from svgpathtools import disvg from time import sleep disvg(path) sleep(1) # needed when not giving the SVGs unique names (or not using timestamp) disvg(spath) print("Notice that path contains {} segments and spath contains {} segments." "".format(len(path), len(spath))) .. parsed-literal:: Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), Line(start=(200+300j), end=(250+350j)), CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) Path(Line(start=(200+100j), end=(200+300j)), Line(start=(200+300j), end=(250+350j)), CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) path is continuous? True path is closed? True path contains non-differentiable points? True spath contains non-differentiable points? False Path(Line(start=(200+101.5j), end=(200+298.5j)), CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)), Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)), CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)), CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)), CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j))) Notice that path contains 3 segments and spath contains 6 segments. Reading SVGSs ~~~~~~~~~~~~~ | The **svg2paths()** function converts an svgfile to a list of Path objects and a separate list of dictionaries containing the attributes of each said path. | Note: Line, Polyline, Polygon, and Path SVG elements can all be converted to Path objects using this function. .. code:: python # Read SVG into a list of path objects and list of dictionaries of attributes from svgpathtools import svg2paths, wsvg paths, attributes = svg2paths('test.svg') # Update: You can now also extract the svg-attributes by setting # return_svg_attributes=True, or with the convenience function svg2paths2 from svgpathtools import svg2paths2 paths, attributes, svg_attributes = svg2paths2('test.svg') # Let's print out the first path object and the color it was in the SVG # We'll see it is composed of two CubicBezier objects and, in the SVG file it # came from, it was red redpath = paths[0] redpath_attribs = attributes[0] print(redpath) print(redpath_attribs['stroke']) .. parsed-literal:: Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)), CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j))) red Writing SVGSs (and some geometric functions and methods) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The **wsvg()** function creates an SVG file from a list of path. This function can do many things (see docstring in *paths2svg.py* for more information) and is meant to be quick and easy to use. Note: Use the convenience function **disvg()** (or set 'openinbrowser=True') to automatically attempt to open the created svg file in your default SVG viewer. .. code:: python # Let's make a new SVG that's identical to the first wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg') .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output1.svg :alt: output1.svg output1.svg There will be many more examples of writing and displaying path data below. The .point() method and transitioning between path and path segment parameterizations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SVG Path elements and their segments have official parameterizations. These parameterizations can be accessed using the ``Path.point()``, ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, and ``Arc.point()`` methods. All these parameterizations are defined over the domain 0 <= t <= 1. | **Note:** In this document and in inline documentation and doctrings, I use a capital ``T`` when referring to the parameterization of a Path object and a lower case ``t`` when referring speaking about path segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc objects). | Given a ``T`` value, the ``Path.T2t()`` method can be used to find the corresponding segment index, ``k``, and segment parameter, ``t``, such that ``path.point(T)=path[k].point(t)``. | There is also a ``Path.t2T()`` method to solve the inverse problem. .. code:: python # Example: # Let's check that the first segment of redpath starts # at the same point as redpath firstseg = redpath[0] print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start) # Let's check that the last segment of redpath ends on the same point as redpath lastseg = redpath[-1] print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end) # This next boolean should return False as redpath is composed multiple segments print(redpath.point(0.5) == firstseg.point(0.5)) # If we want to figure out which segment of redpoint the # point redpath.point(0.5) lands on, we can use the path.T2t() method k, t = redpath.T2t(0.5) print(redpath[k].point(t) == redpath.point(0.5)) .. parsed-literal:: True True False True Tangent vectors and Bezier curves as numpy polynomial objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | Another great way to work with the parameterizations for Line, QuadraticBezier, and CubicBezier objects is to convert them to ``numpy.poly1d`` objects. This is done easily using the ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` methods. | There's also a ``polynomial2bezier()`` function in the pathtools.py submodule to convert polynomials back to Bezier curves. **Note:** cubic Bezier curves are parameterized as .. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3 where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the control points ``start``, ``control1``, ``control2``, and ``end``, respectively, that svgpathtools uses to define a CubicBezier object. The ``CubicBezier.poly()`` method expands this polynomial to its standard form .. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3 where .. math:: \begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} = \begin{bmatrix} -1 & 3 & -3 & 1\\ 3 & -6 & -3 & 0\\ -3 & 3 & 0 & 0\\ 1 & 0 & 0 & 0\\ \end{bmatrix} \begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix} QuadraticBezier.poly() and Line.poly() are defined similarly. .. code:: python # Example: b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) p = b.poly() # p(t) == b.point(t) print(p(0.235) == b.point(0.235)) # What is p(t)? It's just the cubic b written in standard form. bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints()) print("The CubicBezier, b.point(x) = \n\n" + bpretty + "\n\n" + "can be rewritten in standard form as \n\n" + str(p).replace('x','t')) .. parsed-literal:: True The CubicBezier, b.point(x) = (300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3 can be rewritten in standard form as 3 2 (-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j) To illustrate the awesomeness of being able to convert our Bezier curve objects to numpy.poly1d objects and back, lets compute the unit tangent vector of the above CubicBezier object, b, at t=0.5 in four different ways. Tangent vectors (and more on polynomials) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python t = 0.5 ### Method 1: the easy way u1 = b.unit_tangent(t) ### Method 2: another easy way # Note: This way will fail if it encounters a removable singularity. u2 = b.derivative(t)/abs(b.derivative(t)) ### Method 2: a third easy way # Note: This way will also fail if it encounters a removable singularity. dp = p.deriv() u3 = dp(t)/abs(dp(t)) ### Method 4: the removable-singularity-proof numpy.poly1d way # Note: This is roughly how Method 1 works from svgpathtools import real, imag, rational_limit dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2 # Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and, # the limit_{t->t0}[f(t) / abs(f(t))] == # sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2]) from cmath import sqrt u4 = sqrt(rational_limit(dp**2, p_mag2, t)) print("unit tangent check:", u1 == u2 == u3 == u4) # Let's do a visual check mag = b.length()/4 # so it's not hard to see the tangent line tangent_line = Line(b.point(t), b.point(t) + mag*u1) disvg([b, tangent_line], 'bg', nodes=[b.point(t)]) .. parsed-literal:: unit tangent check: True Translations (shifts), reversing orientation, and normal vectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python # Speaking of tangents, let's add a normal vector to the picture n = b.normal(t) normal_line = Line(b.point(t), b.point(t) + mag*n) disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)]) # and let's reverse the orientation of b! # the tangent and normal lines should be sent to their opposites br = b.reversed() # Let's also shift b_r over a bit to the right so we can view it next to b # The simplest way to do this is br = br.translated(3*mag), but let's use # the .bpoints() instead, which returns a Bezier's control points br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t)) normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t)) wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r]) .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/vectorframes.svg :alt: vectorframes.svg vectorframes.svg Rotations and Translations ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python # Let's take a Line and an Arc and make some pictures top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1) midline = Line(-1.5, 1.5) # First let's make our ellipse whole bottom_half = top_half.rotated(180) decorated_ellipse = Path(top_half, bottom_half) # Now let's add the decorations for k in range(12): decorated_ellipse.append(midline.rotated(30*k)) # Let's move it over so we can see the original Line and Arc object next # to the final product decorated_ellipse = decorated_ellipse.translated(4+0j) wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg') .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/decorated_ellipse.svg :alt: decorated\_ellipse.svg decorated\_ellipse.svg arc length and inverse arc length ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Here we'll create an SVG that shows off the parametric and geometric midpoints of the paths from ``test.svg``. We'll need to compute use the ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the related inverse arc length methods ``.ilength()`` function to do this. .. code:: python # First we'll load the path data from the file test.svg paths, attributes = svg2paths('test.svg') # Let's mark the parametric midpoint of each segment # I say "parametric" midpoint because Bezier curves aren't # parameterized by arclength # If they're also the geometric midpoint, let's mark them # purple and otherwise we'll mark the geometric midpoint green min_depth = 5 error = 1e-4 dots = [] ncols = [] nradii = [] for path in paths: for seg in path: parametric_mid = seg.point(0.5) seg_length = seg.length() if seg.length(0.5)/seg.length() == 1/2: dots += [parametric_mid] ncols += ['purple'] nradii += [5] else: t_mid = seg.ilength(seg_length/2) geo_mid = seg.point(t_mid) dots += [parametric_mid, geo_mid] ncols += ['red', 'green'] nradii += [5] * 2 # In 'output2.svg' the paths will retain their original attributes wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, attributes=attributes, filename='output2.svg') .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output2.svg :alt: output2.svg output2.svg Intersections between Bezier curves ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python # Let's find all intersections between redpath and the other redpath = paths[0] redpath_attribs = attributes[0] intersections = [] for path in paths[1:]: for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path): intersections.append(redpath.point(T1)) disvg(paths, filename='output_intersections.svg', attributes=attributes, nodes = intersections, node_radii = [5]*len(intersections)) .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output_intersections.svg :alt: output\_intersections.svg output\_intersections.svg An Advanced Application: Offsetting Paths ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Here we'll find the `offset curve `__ for a few paths. .. code:: python from svgpathtools import parse_path, Line, Path, wsvg def offset_curve(path, offset_distance, steps=1000): """Takes in a Path object, `path`, and a distance, `offset_distance`, and outputs an piecewise-linear approximation of the 'parallel' offset curve.""" nls = [] for seg in path: ct = 1 for k in range(steps): t = k / steps offset_vector = offset_distance * seg.normal(t) nl = Line(seg.point(t), seg.point(t) + offset_vector) nls.append(nl) connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)] if path.isclosed(): connect_the_dots.append(Line(nls[-1].end, nls[0].end)) offset_path = Path(*connect_the_dots) return offset_path # Examples: path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ") path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300) path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j) paths = [path1, path2, path3] offset_distances = [10*k for k in range(1,51)] offset_paths = [] for path in paths: for distances in offset_distances: offset_paths.append(offset_curve(path, distances)) # Note: This will take a few moments wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg') .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/offset_curves.svg :alt: offset\_curves.svg offset\_curves.svg Compatibility Notes for users of svg.path (v2.0) ------------------------------------------------ - renamed Arc.arc attribute as Arc.large\_arc - Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0), set both useSandT and use\_closed\_attrib to be True. 2 The behavior would be identical, but the string formatting used in this method has been changed to use default format (instead of the General format, {:G}), for inceased precision. `↩ <#a2>`__ Licence ------- This module is under a MIT License. Keywords: svg,svg path,svg.path,bezier,parse svg path,display svg Platform: OS Independent Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Scientific/Engineering :: Image Recognition Classifier: Topic :: Scientific/Engineering :: Information Analysis Classifier: Topic :: Scientific/Engineering :: Mathematics Classifier: Topic :: Scientific/Engineering :: Visualization Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires: numpy Requires: svgwrite