Giter Site home page Giter Site logo

svgpathtools's Introduction

Donate Python PyPI PyPI - Downloads

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

Prerequisites

  • numpy
  • svgwrite
  • scipy (optional, but recommended for performance)

Setup

$ 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):

$ 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.

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 disconnected1) 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.

from __future__ import division, print_function
# 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)
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 appended, inserted, set by index, deleted, enumerated, sliced out, etc.

# 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)))
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.

# 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'])
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.

# Let's make a new SVG that's identical to the first
wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='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.

# 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))
True
True
False
True

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 $$\mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3$$ where $P_0$, $P_1$, $P_2$, and $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 $$\mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3$$ where $$\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.

# 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'))
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)

The ability to convert between Bezier objects to NumPy polynomial objects is very useful. For starters, we can take turn a list of Bézier segments into a NumPy array

Numpy Array operations on Bézier path segments

Example available here

To further illustrate the power 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 NumPy polynomials)

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)])
unit tangent check: True

Translations (shifts), reversing orientation, and normal vectors

# 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])

vectorframes.svg

Rotations and Translations

# 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')

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.

# 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')

output2.svg

Intersections between Bezier curves

# 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))

output_intersections.svg

An Advanced Application: Offsetting Paths

Here we'll find the offset curve for a few paths.

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))

# Let's take a look
wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='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 similar2 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.

Licence

This module is under a MIT License.

svgpathtools's People

Contributors

abey79 avatar andersgb avatar canule avatar catherineh avatar chanicpanic avatar davidromeroantequera avatar dervedro avatar elenzil avatar flyingsamson avatar jpcofr avatar kasbah avatar mathandy avatar mdejean avatar msea1 avatar mxgrey avatar nataliats avatar njhurst avatar remi-pr avatar saraedum avatar sebkuzminsky avatar skef avatar sumeet- avatar taoari avatar tatarize avatar ugultopu avatar vrroom avatar wesbz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

svgpathtools's Issues

Iterate split on 4 continuous Arc fails

Hi,

when I split iteratly a circle (if it is 4 Arcs for example - no problem if it's Bezier Curve), I obtain this error : math domain error
The problem is caused by acos() in parameterize function in Path.py
circle_arcsx4.svg.txt

. Value for u1.real can be -1.000000002 and it's the problem...

My solution : add this function in misctools (it should be useful everywhere...) :

#-- constrain(x,a,b)
def constrain(x,valMin,valMax):
if x < valMin :
return valMin

elif valMax < x :
    return valMax

else :
    return x

Import it in path.py :
from .misctools import BugException, constrain # added XH

And in def _parameterize, change :

    if u1.imag > 0:
        self.theta = degrees(acos(constrain(u1. real,-1.0, 1.0))) # acos need -1<=value<=1 - added XH
    elif u1.imag < 0:
        self.theta = -degrees(acos(constrain(u1. real,-1.0, 1.0))) # acos need -1<=value<=1 - added XH

And it's work well to iterate split on circle !

Test file joined. (remove .txt to obtain .svg - github seems not support svg file joined...)

Xavier HINAULT
France

Bug in Line.intersect(Line)

I found a bug where two obviously non-intersecting Lines still report an intersection using Line.intersect(). The bug is captured in this commit: SebKuzminsky/svgpathtools@847b270b
It is highly sensitive to the precise starting and ending points of the two lines - if you round the values down to three significant places (like Line.__repr__() does) then the bug does not trigger.

Convert SVG to list of lines and arcs

Hello, i am working on SVG to g-code convertor for bCNC and i need way to subdivide the SVG to list of paths made just from LINEs and ARCs. Because g-code does not support anything else except lines and arcs. Is there way to do this using your library? That would mean to approximate and replace both Bezier types using lines (or arcs).

Approximation Problem

Hi,

A Path object contain for example :

CubicBezier(start=(27.080179592571874+157.03434674518752j), control1=(27.61140677353281+155.774203506625j), control2=(28.261543320386714+154.57737991657814j), end=(29.016691803786323+153.45777340439065j)),
CubicBezier(start=(29.016691803786323+153.45777340439065j), control1=(29.771840287185935+152.33816689220313j), control2=(30.632000707131247+151.295777457875j), end=(31.583275634274997+150.34450253075j)),
CubicBezier(start=(31.583275634274997+150.34450253075j), control1=(32.53455056141875+149.393227603625j), control2=(33.57693999576094+148.53306718370314j), end=(34.696546507954295+147.77791870032814j)),
CubicBezier(start=(34.696546507954295+147.77791870032814j), control1=(35.81615302014765+147.02277021695315j), control2=(37.01297661019218+146.37263367012503j), end=(38.27311984874062+145.8414064891875j)),
CubicBezier(start=(38.27311984874062+145.8414064891875j), control1=(39.53326308728906+145.31017930824999j), control2=(40.8567259743414+144.89786149320312j), end=(42.229611080550384+144.61835047339065j)),
CubicBezier(start=(42.229611080550384+144.61835047339065j), control1=(43.60249618675937+144.33883945357815j), control2=(45.024803512125+144.192135229j), end=(46.4826356273+144.192135229j)))]

I want to reuse the start and end point from each object to build Line object with :
spt.Line(elem.point(0), elem.point(1))

where elem is an element in Path source

I obtain :

 Line(start=(27.080179592571874+157.03434674518752j), end=(29.01669180378633+153.45777340439054j)),
 Line(start=(29.016691803786323+153.45777340439065j), end=(31.58327563427502+150.34450253075013j)),
 Line(start=(31.583275634274997+150.34450253075j), end=(34.69654650795428+147.77791870032806j)),
 Line(start=(34.696546507954295+147.77791870032814j), end=(38.27311984874062+145.84140648918756j)),
 Line(start=(38.27311984874062+145.8414064891875j), end=(42.2296110805504+144.61835047339065j)),
 Line(start=(42.229611080550384+144.61835047339065j), end=(46.48263562729995+144.1921352290001j)))]

As you can see, there is a little difference :
29.016691803786323+153.45777340439065j give 29.01669180378633+153.45777340439054j
31.58327563427502+150.34450253075013j give 31.583275634274997+150.34450253075j
46.4826356273+144.192135229j give 46.48263562729995+144.1921352290001j

Only end point seem have this problem.

Logically, Path() obtained is not continuous, etc.

Is it possible to give a tolerance for continuous or limit precision to 6 or 8 decimal for example ?

Thanks in advance for your suggest.

SVG Style when using wsvg

Apologies if this is not an issue and the way I'm doing it. I'm writing to an svg file using wsvg but it doesn't retain the style. Instead it seems to default to black and the stroke vanishes. When I add this style back in via illustrator then it works fine. Is there any way of keeping the style from when you read an svg to when you write it?

intersection of a line and cubicbezier

I found that there is an issue with intersection function. May be there is no issue but this code did something that I do not expect. So imagine there are 2 Paths and you want find out the intersection point.

from svgpathtools import *

# some shapes
A = Line(start=(0+200j), end=(300+200j))
B = CubicBezier(start=(50+150j), control1=(70+200j), control2=(120+250j), end=(200+300j))

# intersection points ((T1, seg1, t1), (T2, seg2, t2))
A_intersection = Path(A).intersect(B, justonemode=True)
B_intersection = Path(B).intersect(A, justonemode=True)

disvg([A,B], nodes=[A.point(A_intersection[0][0]), B.point(B_intersection[0][0])])

I except 2 nodes with approximately same coordinates, but they differs. May be using fixed indices at A_intersection[0][0] is not the best option to figure out the T value of the path. Let's see what is in A_intersection and B_intersection:

>>> A_intersection
((0.33333333333333331, Line(start=200j, end=(200+200j)), 0.33333333333333331),
 (0.40000000000000002,
  CubicBezier(start=(50+150j), control1=(70+200j), control2=(120+250j), end=(200+300j)),
  0.40000000000000002))
>>> B_intersection
((0.33333333333333331,
  CubicBezier(start=(50+150j), control1=(70+200j), control2=(120+250j), end=(200+300j)),
  0.33333333333333331),
 (0.40000000000000002, Line(start=200j, end=(200+200j)), 0.40000000000000002))

It looks for me like if you search for intersection of Line and CubicBezier and return values of intersect() on Line object have wrong positions. So you get something like ((T2, seg1, t2), (T1, seg2, t1))

useSandT?

I want output a d string using 'S' instead of 'C'. the d() methode has the useSandT argument to influence this, but I haven't found anyway to set this when using disvg().

Approximate cubic Bézie using line segments

Hi folks - thanks for the great library.

I'm not sure if this functionality is within the scope of this library, but I'm going to put it up for discussion here anyway.

I'd like to approximate a CubicBezier with a series of Line objects. This has many applications, for example in drawing SVGs using an XY plotter (which is my specific need).

The trivial way to do this is to divide the curve into an equal number of points and create Line objects connecting each new segment along the path. This however is not guaranteed to give a good representation of the curve, and is likely to contain an excessive number of points.

A good way to make this approximation is to do it in an adaptive way. Break the curve into smaller curves (CubicBezier.split()) until a certain flatness criterion is met. After doing a bit of research, this seems to be the most referneced article on the subject. The method is critiqued here, especially for not having consulted prior academic research on the subject.

I'd like to implement something like this, and possibly contribute it to this library if you see that it would fit. But before I do so, I'd like to get your recommendations on what algorithm would be appropriate for this purpose.

The antigrain algorithm is one possibility, and in the google groups thread there's another interesting suggestion:

A well-known flatness test is both cheaper and more reliable than the ones you have tried. The essential observation is that when the curve is a uniform speed straight line from end to end, the control points are evenly spaced from beginning to end. Therefore, our measure of how far we deviate from that ideal uses distance of the middle controls, not from the line itself, but from their ideal arrangement. Point 2 should be half-way between points 1 and 3; point 3 should be half-way between points 2 and 4.
This, too, can be improved. Yes, we can eliminate the square roots in the distance tests, retaining the Euclidean metric; but the taxicab metric is faster, and also safe. The length of displacement (x,y) in this metric is |x|+|y|.

What do you suggest as a good algorithm to implement in this case? Would love some feedback.

bug in Path.area()

I'm seeing this problem on the current tip of master, ae42197.

from svgpathtools.path import *
p = Path(Arc(start=(106.58928+132.95833j), radius=(40.82143+83.910713j), rotation=0.0, large_arc=False, sweep=True, end=(74.472411+214.93917j)),
     Arc(start=(74.472411+214.93917j), radius=(40.82143+83.910713j), rotation=0.0, large_arc=False, sweep=True, end=(28.658655+167.9207j)),
     Arc(start=(28.658655+167.9207j), radius=(40.82143+83.910713j), rotation=0.0, large_arc=False, sweep=True, end=(41.237335+65.887886j)),
     Line(start=(41.237335+65.887886j), end=(65.767853+132.95833j)),
     Line(start=(65.767853+132.95833j), end=(106.58928+132.95833j)))
p.area()

This hangs until I interrupt it with ^C, then I get this backtrace:

KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-4-38eb451037da> in <module>()
----> 1 p.area()

/home/seb/svg2gcode/svgpathtools/svgpathtools/path.pyc in area(self, chord_length)
   2568             else:
   2569                 bezier_path_approximation.append(seg)
-> 2570         return area_without_arcs(Path(*bezier_path_approximation))
   2571 
   2572     def intersect(self, other_curve, justonemode=False, tol=1e-12):

/home/seb/svg2gcode/svgpathtools/svgpathtools/path.pyc in area_without_arcs(path)
   2548             for seg in path:
   2549                 x = real(seg.poly())
-> 2550                 dy = imag(seg.poly()).deriv()
   2551                 integrand = x*dy
   2552                 integral = integrand.integ()

/home/seb/svg2gcode/svgpathtools/svgpathtools/polytools.pyc in imag(z)
     66 def imag(z):
     67     try:
---> 68         return np.poly1d(z.coeffs.imag)
     69     except AttributeError:
     70         return z.imag

/usr/lib/python2.7/dist-packages/numpy/lib/polynomial.pyc in __init__(self, c_or_r, r, variable)
   1054         if len(c_or_r.shape) > 1:
   1055             raise ValueError("Polynomial must be 1d only.")
-> 1056         c_or_r = trim_zeros(c_or_r, trim='f')
   1057         if len(c_or_r) == 0:
   1058             c_or_r = NX.array([0.])

/usr/lib/python2.7/dist-packages/numpy/lib/function_base.pyc in trim_zeros(filt, trim)
   2097             else:
   2098                 last = last - 1
-> 2099     return filt[first:last]
   2100 
   2101 

KeyboardInterrupt: 

Bug: Path curvature not correctly scaled

I was a bit puzzled by a discrepancy in the curvature of a complete path and the curvature of its corresponding segment. Try running the following

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
T=0.5
k = path.curvature(T)
idx,t = path.T2t(T)
k_seg = path[idx].curvature(t)
print(k,k_seg)

This bug arises because the derivatives of the Path object are taken with respect to the t variable of the segment and not the T variable of the Path object itself. This can be fixed by modifying the following lines.

dz = self.derivative(t)

ddz = self.derivative(t, n=2)

It would also seem to be possible to use
return seg.curvature(t)
rather than the explicit formula itself.

divide by zero error when computing path length

Computing length of each path returned by svg2paths() fails when reading the following svg:
https://www.dropbox.com/s/2b13vqykmvid0eg/13865.svg?dl=0

Procedure to reproduce:

from svgpathtools import svg2paths
paths, path_attrs = svg2paths('13865.svg', return_svg_attributes=False)
for path_id, path in enumerate(paths): 
    path_attr = path_attrs[path_id]
    plen = int(path.length())

The error it throws:

/usr/local/lib/python3.5/dist-packages/svgpathtools/path.py in length(self, T0, T1, error, min_depth)
   1899 
   1900     def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
-> 1901         self._calc_lengths(error=error, min_depth=min_depth)
   1902         if T0 == 0 and T1 == 1:
   1903             return self._length

/usr/local/lib/python3.5/dist-packages/svgpathtools/path.py in _calc_lengths(self, error, min_depth)
   1876                    self._segments]
   1877         self._length = sum(lengths)
-> 1878         self._lengths = [each/self._length for each in lengths]
   1879 
   1880     def point(self, pos):

/usr/local/lib/python3.5/dist-packages/svgpathtools/path.py in <listcomp>(.0)
   1876                    self._segments]
   1877         self._length = sum(lengths)
-> 1878         self._lengths = [each/self._length for each in lengths]
   1879 
   1880     def point(self, pos):

ZeroDivisionError: float division by zero

The error seems to come from self._length being zero in path.py. It is unusual but possible as the sketch above has a path:
'M261 166 L261 166 '
which looks like a single dot on the canvas.

Adding an epsilon to self._length seems to be a workaround:
self._length = sum(lengths) + np.finfo(np.float32).eps

zero lenght closing lines after path parsing

There is a issue with closed paths. If the penultimate command ends on the start point, the z command creates a zero length svgpathtools.path.Line. That breaks a lot of things later, something like div by zero. I think a simple check for current_pos == start_pos in parser will do well.

Create relatives path instead of absolute.

Is there anyway to get out svg to generate a relative path instead of an absolute one?

Instead of this:
<path d="M 0.0,1.0 L 2.0,1.0 L 3.0,2.0" fill="none" stroke="#000000" stroke-width="1.175"/>

something like this:
<path d="m 18.815667,1 h 2 l 1,1" fill="none" stroke="#000000" stroke-width="1.175"/>

subpaths do not need to start with M/m

For example m0 0h50v50zv20h20z right now causes an error:

Traceback (most recent call last):
File "", line 1, in
File "C:\python36\lib\site-packages\svgpathtools-1.3.2-py3.6.egg\svgpathtools\parser.py", line 81, in parse_path
File "C:\python36\lib\site-packages\svgpathtools-1.3.2-py3.6.egg\svgpathtools\path.py", line 1883, in closed
ValueError: End does not coincide with a segment start.

But this is ok

If a "closepath" is followed immediately by any other command, then the next subpath starts at the same initial point as the current subpath.

When installing with pip3, svg2paths is not matching documentation

First time using svgpathtools today, also not too experienced with python. I basically want to use svgpathtools to run some checks and based on them change the path element slightly. That's just how I got to the issue though:

The documentation specifies the following function arguments:

svg2paths(svg_file_location, return_svg_attributes=False, convert_circles_to_paths=True, convert_ellipses_to_paths=True, convert_lines_to_paths=True, convert_polylines_to_paths=True, convert_polygons_to_paths=True, convert_rectangles_to_paths=True)

I installed svgpathtools with pip3 install svgpathtools. When checking for the function used with inspect.getsource(svg2paths), I get the following:

def svg2paths(svg_file_location,
              convert_lines_to_paths=True,
              convert_polylines_to_paths=True,
              convert_polygons_to_paths=True,
              return_svg_attributes=False):
    """
    Converts an SVG file into a list of Path objects and a list of
    dictionaries containing their attributes.  This currently supports
    SVG Path, Line, Polyline, and Polygon elements.
    :param svg_file_location: the location of the svg file
    :param convert_lines_to_paths: Set to False to disclude SVG-Line objects
    (converted to Paths)
    :param convert_polylines_to_paths: Set to False to disclude SVG-Polyline
    objects (converted to Paths)
    :param convert_polygons_to_paths: Set to False to disclude SVG-Polygon
    objects (converted to Paths)
    :param return_svg_attributes: Set to True and a dictionary of
    svg-attributes will be extracted and returned
    :return: list of Path objects, list of path attribute dictionaries, and
    (optionally) a dictionary of svg-attributes

Where does this decrepancy come from? Just a general question, since until now, I didn't have any problems installing modules with pip3

Converting complex bbox coordinates to ViewBox relative points

Hello,
I have an svg with viewbox(0,0,612,792) . bbox on any path elements gives complex numbers .Is there a way to convert them into coordinates with respect to top left corner of viewbox?
Path(Line(start=(790.996+1827j), end=(790.996+1804j)))
I am using path[0].length() to find lines with certain length and replace them with lines across/along the viewbox at same locations.

If line1= x1,y1,x1,y2 then replacement would be x1,y1,x1,612

No Support for SVG images using defs

The following code is taken from the README. I'm attempting to parse SVG images and then reconstruct them without any losses.

from svgpathtools import svg2paths2, wsvg
paths, attributes, svg_attributes = svg2paths2('def_ex.svg')

# Let's make a new SVG that's identical to the first
wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='def_ex_processed.svg')

When parsed the picture is modified, images below:

Original Image

<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
  <!-- Some graphical objects to use -->
  <defs>
    <circle id="myCircle" cx="0" cy="0" r="5" />
  </defs>
 
  <!-- using my graphical objects -->
  <use x="5" y="5" xlink:href="#myCircle"/>
</svg>

Reconstructed Image

<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" version="1.1" viewBox="0 0 10 10" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
	<defs/>
	<path cx="0" cy="0" d="M -5.0,0.0 A 5.0,5.0 0.0 1,0 5.0,0.0 A 5.0,5.0 0.0 1,0 -5.0,0.0" id="myCircle" r="5"/>
</svg>

Sorry If this can't easily be viewed I'm not sure of the best way to post SVG images to issues.

The images loses its positioning which is important for what I'm trying do. I believe the issue is occurring with the line <use x="5" y="5" xlink:href="#myCircle"/> where svg2paths2 is unable to understand the reference to the myCircle def from before

Relevant Docs on defs in SVG

Path.intersect() withholds some intersections from the result due to a typo

With the following shapes intersecting in 2 places:


from svgpathtools.parser import parse_path

# clipping rectangle
p1 = parse_path('M0.0 0.0 L27.84765625 0.0 L27.84765625 242.6669922 L0.0 242.6669922 z')

# frame with curved corners
p2 = parse_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')

I see this REPL output:


>>> print(len(p1.intersect(p2)))
2
>>> print(len(p2.intersect(p1)))
1

This seems to be caused by Path.intersect() removing some intersections from the result due to a typo. If I patch svgpathtools/path.py version 1.3.3 at line 2196 as follows:

<            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]]

I get the expected result:


>>> print(len(p1.intersect(p2)))
2
>>> print(len(p2.intersect(p1)))
2

BTW, it would be nice to make a new release of svgpathtools and pick up all bug fixes accumulated since last summer, including a fix for this issue :)

Possible to save SVG with mm as standard unit?

Question as above.

Context: I am currently working on a lasercutting project. All our toolchains use mm as unit. Currently, when using paths2svg.wsvg, then the unit is set to px. Is there a way to change this to mm by default?

Lacking support for paths made up of multiple subpaths

Currently, svgpathtools cannot parse/render this kind of path:

M 0,0 V 1 H 1 V 0 Z M 2,0 V 1 H 3 V 0 Z   # (1)

The issue is that the path contains two 'Z's. Svgpathtools is not equipped to remember the middle Z. It will forget it, and print this instead to the file:

M 0,0 V 1 H 1 V 0 H 0 M 2,0 V 1 H 3 V 0 Z   # (2)

But (1) renders like so:

display_temp

Whereas (2) renders like so:

display_temp

Also, svgpathtools cannot parse/render this kind of path:

M 0,0 H 1 M 1,0 V 1   # (3)

The issue is that the path contains an in-place move, which svgpathtools ignores. Svgpathtools will print this instead:

M 0,0 1,0 1,1   # (4)

But (3) renders as

display_temp

Whereas (4) renders as

display_temp

Basically, one way or the other, svgpathtools doesn't know how to properly handle paths with multiple subpaths. In fact, a "subpath" class is entirely missing.

I have been preparing a fork over at https://github.com/vistuleB/svgpathtools to remedy these issues. I hope this fork will be the object of a pull request at some point. The changes are quite extensive though and right now I am still preparing the new README for the new fork.

Interested people can take a look. The fork contains other new capabilities, such as path offsets, strokes, conversion of elliptical arcs to bezier curves, and new SaxDocument capabilities.

Now would be a good time to give me feedback, before I make the pull request.

Thanks.

First pull request of my life, pls help

Hi, I've never contributed to an open source project before, I hardly know how to use git. (Mathematician here.)

Could someone guide me through my first pull request?

The modifications I've made (small to big)

-- correct bug in path_encloses_pt [current bug: double-counts intersections at segment endpoints]

-- add a "multisplit" functionality on top of "split" to paths

-- add a feature for approximating arc segments by a sequence of cubic bezier curves... so now any path can be turned into a purely bezier path... the user can control the accuracy of the approximation, as well as well as the "safety" (number of points measured along the curve to ensure said accuracy is respected)... (The cubic beziers I used are those described in the paper "Drawing an Elliptical Arc using Polylines, Quadratic or Cubic Bezier Curves" by L. Maisonobe.)

-- (the above could also be used for measuring area of paths with arc segments, though I didn't make the change since I don't need area myself)

-- path offsets: offset a path by some amount; exact offset is not possible for bezier curves (or arcs), so the user gets to choose an accuracy, and bezier curves are recursively (and adaptively) subdivided into smaller bezier curves (of same degree) until the desired accuracy is respected everywhere; accurately produces cusps, etc, for concave offsets; requires the "arc to bezier" transformation above for paths with elliptical arcs (i.e., elliptical arcs are first transformed into a sequence of cubic beziers, after which these are offset)

-- joins: afore-mentioned path offset accommodates the standard SVG segment joins: miter, bevel, and round; miter-limit also implemented

-- path stroke: two-sided path offset with added line caps and line cap options 'butt', 'square' and 'round'---basically, now one can implement a "stroke to path" operation, as found in popular software like inkscape in 1 command, without resorting to approximation by polylines, and with all the standard line cap/segment join SVG options

-- also works with discontinuous and/or closed paths and/or mixtures thereof :)

-- fun fact: the above offset and stroke operations are robust enough to be used with negative offsets / stroke widths... in such a case the endcaps of the path take an inverted shape, as expected.

I'd like to contribute all this to the repo, but will need a little bit of guidance... thanks...

Add install requirements

Is there a reason the install_requires line in setup.py is commented out? Uncommenting this would make pip automatically install dependencies.

Writing long paths to file takes a while

Saving long paths with a lot of segments is really slow. I figured out svgwrite runs by default with some debug flags. Svgwrite does some validation stuff like checking syntax of path's d attribute. The parser that they use runs amok.

I think the best approach is to disable debug mode and to turn validation off (#7). The data you put into svgwrite is valid, paths are syntactically correct. And if some arguments are broken, most programs can deal with it.

SVG Groups are ignored. Especially transformations acting on the paths inside the group.

Example in an SVG created in Inkscape:

<g
id="g3450-3"
transform="matrix(-1e-4,-1,1,-1e-4,670.77002,132.92999)">
<path
inkscape:connector-curvature="0"
d="m 28.35,0 c 0,15.65621 -12.69107,28.3485 -28.34728,28.35"
style="fill:none;stroke:#000000;stroke-width:0.72000003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none"
id="path3452-8" />

The group transformation is ignored when calling svgpathtools.svg2paths().

Bezier curve inflection points

Hi,

First of all, thank you for the svgpathtools.
It has saved me lots of time.

Really just a small issue.
I did not see a convenient way to obtain the inflection points of a bezier curve.
@mathandy, Would you accept a contribution for that?

All the best,
olaf

A function like disvg but returning a svgwrite.Drawing would be helpful.

It would be helpful if your great lib would offer a function which would not write to a .svg file, like the existing disvg(), but returning the svgwrite.Drawing object.
This way it would be possible to add additional stuff like defs to the Drawing. Or is there already a way to do this?

Text bounding box

Could you suggest a method to extract text and their bounding boxe coordinates with respect to viewbox .

weird code/dist issue in svg2paths/svg_to_paths

Hi

I'm having a weird issue. i install svgpathtools (v 1.3.3) via a setup.py script in a package i have. For some reason the installed package ends up with two similar files that are not present in the repo here, as far as i can tell.

Here is an example:


15:37:46•anders@xyz:/usr/local/lib/python3.5/dist-packages
  cd svgpathtools-1.3.3-py3.5.egg 

15:37:48•anders@xyz:/usr/local/lib/python3.5/dist-packages/svgpathtools-1.3.3-py3.5.egg
  ls
EGG-INFO  svgpathtools

15:37:48•anders@xyz:/usr/local/lib/python3.5/dist-packages/svgpathtools-1.3.3-py3.5.egg
  cd svgpathtools 
15:37:51•anders@xyz:/usr/local/lib/python3.5/dist-packages/svgpathtools-1.3.3-py3.5.egg/svgpathtools
  ll
total 180K
-rw-r--r-- 1 root staff  14K feb.  25 15:37 bezier.py
-rw-r--r-- 1 root staff  654 feb.  25 15:37 directional_field.py
-rw-r--r-- 1 root staff  890 feb.  25 15:37 __init__.py
-rw-r--r-- 1 root staff 2,0K feb.  25 15:37 misctools.py
-rw-r--r-- 1 root staff 7,0K feb.  25 15:37 parser.py
-rw-r--r-- 1 root staff  92K feb.  25 15:37 path.py
-rw-r--r-- 1 root staff  15K feb.  25 15:37 paths2svg.py
-rw-r--r-- 1 root staff  439 feb.  25 15:37 pathtools.py
-rw-r--r-- 1 root staff 2,5K feb.  25 15:37 polytools.py
-rw-r--r-- 1 root staff 7,5K feb.  25 15:37 smoothing.py
-rw-r--r-- 1 root staff 4,8K feb.  25 15:37 svg2paths.py   <--   NOTICE
-rw-r--r-- 1 root staff 8,3K feb.  25 15:37 svg_to_paths.py  <-- NOTICE

15:37:51•anders@xyz:/usr/local/lib/python3.5/dist-packages/svgpathtools-1.3.3-py3.5.egg/svgpathtools
  ag svg2paths
__init__.py
17:    from .svg2paths import svg2paths, svg2paths2

svg2paths.py
2:The main tool being the svg2paths() function."""
34:def svg2paths(svg_file_location,
114:def svg2paths2(svg_file_location,
119:    """Convenience function; identical to svg2paths() except that
120:    return_svg_attributes=True by default.  See svg2paths() docstring for more
122:    return svg2paths(svg_file_location=svg_file_location,

svg_to_paths.py
2:The main tool being the svg2paths() function."""
92:def svg2paths(svg_file_location,
110:            `svg2paths2()` function.
129:        dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
193:def svg2paths2(svg_file_location,
201:    """Convenience function; identical to svg2paths() except that
202:    return_svg_attributes=True by default.  See svg2paths() docstring for more
204:    return svg2paths(svg_file_location=svg_file_location,
15:38:18•anders@xyz:/usr/local/lib/python3.5/dist-packages/svgpathtools-1.3.3-py3.5.egg/svgpathtools

I noticed this because one version of svg2paths is capable of converting rectangles (svg rect) and the other is not.

I made sure this package was not installed in this location before i performed the installation.

The wheel that is installed (https://files.pythonhosted.org/packages/71/96/cc91050f3b53c2cea0eda18f371d0584e7f43713ce606738384e8001a877/svgpathtools-1.3.3-py2.py3-none-any.whl#sha256=7f7bdafe2c03b312178460104705e1d554d8cf36c898bec41bdce9fed3504746) does contain both files. From the setuptools output:

Searching for svgpathtools==1.3.3
Reading https://pypi.python.org/simple/svgpathtools/
Downloading https://files.pythonhosted.org/packages/71/96/cc91050f3b53c2cea0eda18f371d0584e7f43713ce606738384e8001a877/svgpathtools-1.3.3-py2.py3-none-any.whl#sha256=7f7bdafe2c03b312178460104705e1d554d8cf36c898bec41bdce9fed3504746
Best match: svgpathtools 1.3.3
Processing svgpathtools-1.3.3-py2.py3-none-any.whl
Installing svgpathtools-1.3.3-py2.py3-none-any.whl to /usr/local/lib/python3.5/dist-packages
writing requirements to /usr/local/lib/python3.5/dist-packages/svgpathtools-1.3.3-py3.5.egg/EGG-INFO/requires.txt
Adding svgpathtools 1.3.3 to easy-install.pth file

Is this the intended contents of this package, or is there an error somewhere?

Happy to provide more info, but I don't know what is the intended behaviour here.

A.

Functions for converting other SVG primitives to paths not importable

We need the functionality used in the primitive conversion functions: ellipse2pathd, polyline2pathd, polygon2pathd and rect2pathd

Unfortunately, as svgpathtools.__init__ exposes a function with the same name as the module svg2paths, importing these externally is not possible.

Would it be possible to allow these to be importable? I am happy to do any refactoring required if this is a change you would accept. At the moment we have the code copy / pasted in our repo, and would be nicer to avoid this duplication.

Unable to read circle/ellipse when cx and cy are not specified.

Reading a file containing below svg throws error.

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-100 -100 200 200">
  <circle r="100" />
 </svg>

Below is the error.

In [5]: svgpathtools.svg2paths2('disvg_output.svg')                                                                                             
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-b24a2854f246> in <module>
----> 1 svgpathtools.svg2paths2('../red-chalice/svgtransform/disvg_output.svg')

~/Data/Projects/svgpathtools/svgpathtools/svg_to_paths.py in svg2paths2(svg_file_location, return_svg_attributes, convert_circles_to_paths, convert_ellipses_to_paths, convert_lines_to_paths, convert_polylines_to_paths, convert_polygons_to_paths, convert_rectangles_to_paths)
    213                      convert_polylines_to_paths=convert_polylines_to_paths,
    214                      convert_polygons_to_paths=convert_polygons_to_paths,
--> 215                      convert_rectangles_to_paths=convert_rectangles_to_paths)

~/Data/Projects/svgpathtools/svgpathtools/svg_to_paths.py in svg2paths(svg_file_location, return_svg_attributes, convert_circles_to_paths, convert_ellipses_to_paths, convert_lines_to_paths, convert_polylines_to_paths, convert_polygons_to_paths, convert_rectangles_to_paths)
    176     if convert_circles_to_paths:
    177         circles = [dom2dict(el) for el in doc.getElementsByTagName('circle')]
--> 178         d_strings += [ellipse2pathd(c) for c in circles]
    179         attribute_dictionary_list += circles
    180 

~/Data/Projects/svgpathtools/svgpathtools/svg_to_paths.py in <listcomp>(.0)
    176     if convert_circles_to_paths:
    177         circles = [dom2dict(el) for el in doc.getElementsByTagName('circle')]
--> 178         d_strings += [ellipse2pathd(c) for c in circles]
    179         attribute_dictionary_list += circles
    180 

~/Data/Projects/svgpathtools/svgpathtools/svg_to_paths.py in ellipse2pathd(ellipse)
     37         ry = float(ry)
     38 
---> 39     cx = float(cx)
     40     cy = float(cy)
     41 

TypeError: float() argument must be a string or a number, not 'NoneType'

SVG definition says, if cx and cy are not specified consider it as "0"
https://www.w3.org/TR/SVG11/shapes.html#CircleElementCXAttribute

I dig into the code and found that here default value should be 0

Reason for using complex numbers to store coordinates

I am not sure this is an issue exactly but is there a reason complex numbers are used to store the coordinates of points? It would make sense if it was converted to polar coordinates but unless I am misunderstanding things, it is using Cartesian and the real number is the X and the imaginary is the Y.

I think this is a holdover from the adaptation from the svg.path but I wanted to do some more work concerning the points and found it easier to just replace the complex numbers with a simple point class that has some convenience functions. My fork: https://github.com/brinnLabs/svgpathtools

Rect not closed as output

Hi,

Thanks for your good library !

An SVG file contain a simple rect. If i open this SVG file with a rect (=closed) with :
paths,attribute=spt.svg2paths('test.svg')

I obtain :
paths
Out[14]:
[Path(Line(start=(28.571428299+29.5050621033j), end=(325.714281082+29.5050621033j)),
Line(start=(325.714281082+29.5050621033j), end=(325.714281082+280.933635711j)),
Line(start=(325.714281082+280.933635711j), end=(28.571428299+280.933635711j)),
Line(start=(28.571428299+280.933635711j), end=(28.571428299+29.5050621033j)))]

And now, if I display the d string, I obtain :
paths[0].d()
Out[15]: 'M 28.571428299,29.5050621033 L 325.714281082,29.5050621033 L 325.714281082,280.933635711 L 28.571428299,280.933635711 L 28.571428299,29.5050621033'

And internally :
paths[0].isclosed()
Out[11]: True

But when I open the file with :
spt.disvg(paths)

It's logically open (see d), not closed. I think the d attribute should be :
'M 28.571428299,29.5050621033 L 325.714281082,29.5050621033 L 325.714281082,280.933635711 L 28.571428299,280.933635711 Z'

The problem seem exist with circle so.

I think Z should be added to d field when path is closest : isn't it ?

Xavier HINAULT
France

Memory leak of CubicBezier.length()

I found that there is a memory leak for function CubicBezier.length().
The test code is below:

    for i in range(10000):
        paths, _, _ = svg2paths2(file)
        seg = paths[1][0]
        if type(seg) is CubicBezier:
            print(seg.length())

My test environment is OSX10.12, python3.6.

The Dom parser crashes on Polyline and Polygon

The Document class uses a dict to lookup the functions for the leaf node objects and calls them from the svg_to_paths file but the polyline and polygon functions do not properly take a group they specifically take a points_string, so they actually fail when called like that.

"""converts the string from a polyline points-attribute to a string for a
Path object d-attribute"""

Compare this to something like ellipse:

"""converts the parameters from an ellipse or a circle to a string for a 
Path object d-attribute"""

Do note, that simply fixing the polyline function to set polyline_d = polyline['d'] will seemingly change the functionality of dom2dict() in svg_to_paths:

    if convert_polygons_to_paths:
        pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
        d_strings += [polygon2pathd(pg['points']) for pg in pgons]
        attribute_dictionary_list += pgons

I rewrote the functions in a consistent manner in the svg_parser.py file in pull request #69. Generally because it the inconsistency of extracting the points early for those two conversions bits was an eyesore.

Path.area() fails if it includes Arc objects

Path.area() calls poly() on each segment, but Arc() doesn't provide that method. The area can be approximated using linear interpolation of the Arc:

def approximate_path_area(path):

    """Approximates the path area by converting each Arc to 1,000
    Lines."""

    assert(path.isclosed())
    tmp = svgpathtools.path.Path()
    for seg in path:
        if type(seg) == svgpathtools.path.Arc:
            for i in range(0, 1000):
                t0 = i/1000.0
                t1 = (i+1)/1000.0
                l = svgpathtools.path.Line(start=seg.point(t0), end=seg.point(t1))
                tmp.append(l)
        else:
            tmp.append(seg)
    return tmp.area()

a selfish request for credit.

Heylo,

thanks for your comment on my Primer on Bézier Curves, it's always great to see an application for what I've documented, but I noticed your README.md has a section called "Credit where credit's due" - while you of course don't need to mention the primer by URL, it might be useful for others as a further reference link they can visit and sink their teeth into =)

Path drawing position bugs

I am drawing contours, referenced from origin (0,0). Those objects have strictly positive coordinates.
However, when drawing them with wsvg, the following issues appear:

  • Objects are oriented pointing downwards, with negative coordinates
  • Objects are referenced from document origin (located at (0, document_height)), not global origin.

Is there a way to avoid this behavior that sound like a bug to me ?

EDIT:
From my experiments, it looks like document coordinate system is oriented y-down, while SVG coordinate system is y-up. Is it normal ?

Decouple the Parser from the Path

As is the parser for the SVG paths instances the Path object and the Line objects and the curve objects. It would be better if it didn't do that. Passing the path object into the parse_path command and as it parses the commands it would call a command on path that add_line() or add_curve() or start(), or calling these on a different class that simple delegated to segments.append(Line(start_pos,end_pos)) or whatever.

It would make that code rather independently useful, as anybody could cook up a parser object and throw it in there, even if they weren't going to actually use the rest of the path tools as they were intended. The code has some reasonable svg parse capabilities and those could be broken off and decoupled more effectively from the math commands and logic in the primary classes.

This could even be taken to the somewhat logical extreme of branching that project off completely and writing an svgread module, which would be completely ambiguous as to where the data ends up. Just including the parsing tools as such loading the data in either a proper dom or the various other methods of loading and extracting that data. Including the svg path parsing. Which would basically yield proper calls parse that data. As is the intersection between the parser bits, the svgpathtools and svgwrite all seems kind of sloppy.

svg2paths not recognizing ellipses

I haven't yet dived into why this is the case, but this svg generates empty lists for both the attributes and paths returned values for svg2paths. I'll try to look deeper into this later today.

<svg width="262" height="178" xmlns="http://www.w3.org/2000/svg"> <ellipse ry="51" rx="51" id="svg_3" cy="101" cx="77.5" stroke-width="1.5" stroke="#00ffff" fill="none"/> <ellipse ry="57.5" rx="57.5" id="svg_4" cy="100.5" cx="145" stroke-width="1.5" stroke="#ff0000" fill="none"/> </svg>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.