removed build & eggs-info from git repo

pull/55/head
Andy 2018-07-24 14:34:28 -07:00
parent 7fc7e45113
commit 2b1670460e
17 changed files with 3 additions and 4500 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
*.pyc *.pyc
.* .*
/svgpathtools/nonunittests /svgpathtools/nonunittests
build
svgpathtools.egg-info
!.travis.yml !.travis.yml
!/.gitignore !/.gitignore

View File

@ -1,19 +0,0 @@
from .bezier import (bezier_point, bezier2polynomial,
polynomial2bezier, split_bezier,
bezier_bounding_box, bezier_intersections,
bezier_by_line_intersections)
from .path import (Path, Line, QuadraticBezier, CubicBezier, Arc,
bezier_segment, is_bezier_segment, is_path_segment,
is_bezier_path, concatpaths, poly2bez, bpoints2bezier,
closest_point_in_path, farthest_point_in_path,
path_encloses_pt, bbox2path)
from .parser import parse_path
from .paths2svg import disvg, wsvg
from .polytools import polyroots, polyroots01, rational_limit, real, imag
from .misctools import hex2rgb, rgb2hex
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
try:
from .svg2paths import svg2paths, svg2paths2
except ImportError:
pass

View File

@ -1,375 +0,0 @@
"""This submodule contains tools that deal with generic, degree n, Bezier
curves.
Note: Bezier curves here are always represented by the tuple of their control
points given by their standard representation."""
# External dependencies:
from __future__ import division, absolute_import, print_function
from math import factorial as fac, ceil, log, sqrt
from numpy import poly1d
# Internal dependencies
from .polytools import real, imag, polyroots, polyroots01
# Evaluation ##################################################################
def n_choose_k(n, k):
return fac(n)//fac(k)//fac(n-k)
def bernstein(n, t):
"""returns a list of the Bernstein basis polynomials b_{i, n} evaluated at
t, for i =0...n"""
t1 = 1-t
return [n_choose_k(n, k) * t1**(n-k) * t**k for k in range(n+1)]
def bezier_point(p, t):
"""Evaluates the Bezier curve given by it's control points, p, at t.
Note: Uses Horner's rule for cubic and lower order Bezier curves.
Warning: Be concerned about numerical stability when using this function
with high order curves."""
# begin arc support block ########################
try:
p.large_arc
return p.point(t)
except:
pass
# end arc support block ##########################
deg = len(p) - 1
if deg == 3:
return p[0] + t*(
3*(p[1] - p[0]) + t*(
3*(p[0] + p[2]) - 6*p[1] + t*(
-p[0] + 3*(p[1] - p[2]) + p[3])))
elif deg == 2:
return p[0] + t*(
2*(p[1] - p[0]) + t*(
p[0] - 2*p[1] + p[2]))
elif deg == 1:
return p[0] + t*(p[1] - p[0])
elif deg == 0:
return p[0]
else:
bern = bernstein(deg, t)
return sum(bern[k]*p[k] for k in range(deg+1))
# Conversion ##################################################################
def bezier2polynomial(p, numpy_ordering=True, return_poly1d=False):
"""Converts a tuple of Bezier control points to a tuple of coefficients
of the expanded polynomial.
return_poly1d : returns a numpy.poly1d object. This makes computations
of derivatives/anti-derivatives and many other operations quite quick.
numpy_ordering : By default (to accommodate numpy) the coefficients will
be output in reverse standard order."""
if len(p) == 4:
coeffs = (-p[0] + 3*(p[1] - p[2]) + p[3],
3*(p[0] - 2*p[1] + p[2]),
3*(p[1]-p[0]),
p[0])
elif len(p) == 3:
coeffs = (p[0] - 2*p[1] + p[2],
2*(p[1] - p[0]),
p[0])
elif len(p) == 2:
coeffs = (p[1]-p[0],
p[0])
elif len(p) == 1:
coeffs = p
else:
# https://en.wikipedia.org/wiki/Bezier_curve#Polynomial_form
n = len(p) - 1
coeffs = [fac(n)//fac(n-j) * sum(
(-1)**(i+j) * p[i] / (fac(i) * fac(j-i)) for i in range(j+1))
for j in range(n+1)]
coeffs.reverse()
if not numpy_ordering:
coeffs = coeffs[::-1] # can't use .reverse() as might be tuple
if return_poly1d:
return poly1d(coeffs)
return coeffs
def polynomial2bezier(poly):
"""Converts a cubic or lower order Polynomial object (or a sequence of
coefficients) to a CubicBezier, QuadraticBezier, or Line object as
appropriate."""
if isinstance(poly, poly1d):
c = poly.coeffs
else:
c = poly
order = len(c)-1
if order == 3:
bpoints = (c[3], c[2]/3 + c[3], (c[1] + 2*c[2])/3 + c[3],
c[0] + c[1] + c[2] + c[3])
elif order == 2:
bpoints = (c[2], c[1]/2 + c[2], c[0] + c[1] + c[2])
elif order == 1:
bpoints = (c[1], c[0] + c[1])
else:
raise AssertionError("This function is only implemented for linear, "
"quadratic, and cubic polynomials.")
return bpoints
# Curve Splitting #############################################################
def split_bezier(bpoints, t):
"""Uses deCasteljau's recursion to split the Bezier curve at t into two
Bezier curves of the same order."""
def split_bezier_recursion(bpoints_left_, bpoints_right_, bpoints_, t_):
if len(bpoints_) == 1:
bpoints_left_.append(bpoints_[0])
bpoints_right_.append(bpoints_[0])
else:
new_points = [None]*(len(bpoints_) - 1)
bpoints_left_.append(bpoints_[0])
bpoints_right_.append(bpoints_[-1])
for i in range(len(bpoints_) - 1):
new_points[i] = (1 - t_)*bpoints_[i] + t_*bpoints_[i + 1]
bpoints_left_, bpoints_right_ = split_bezier_recursion(
bpoints_left_, bpoints_right_, new_points, t_)
return bpoints_left_, bpoints_right_
bpoints_left = []
bpoints_right = []
bpoints_left, bpoints_right = \
split_bezier_recursion(bpoints_left, bpoints_right, bpoints, t)
bpoints_right.reverse()
return bpoints_left, bpoints_right
def halve_bezier(p):
# begin arc support block ########################
try:
p.large_arc
return p.split(0.5)
except:
pass
# end arc support block ##########################
if len(p) == 4:
return ([p[0], (p[0] + p[1])/2, (p[0] + 2*p[1] + p[2])/4,
(p[0] + 3*p[1] + 3*p[2] + p[3])/8],
[(p[0] + 3*p[1] + 3*p[2] + p[3])/8,
(p[1] + 2*p[2] + p[3])/4, (p[2] + p[3])/2, p[3]])
else:
return split_bezier(p, 0.5)
# Bounding Boxes ##############################################################
def bezier_real_minmax(p):
"""returns the minimum and maximum for any real cubic bezier"""
local_extremizers = [0, 1]
if len(p) == 4: # cubic case
a = [p.real for p in p]
denom = a[0] - 3*a[1] + 3*a[2] - a[3]
if denom != 0:
delta = a[1]**2 - (a[0] + a[1])*a[2] + a[2]**2 + (a[0] - a[1])*a[3]
if delta >= 0: # otherwise no local extrema
sqdelta = sqrt(delta)
tau = a[0] - 2*a[1] + a[2]
r1 = (tau + sqdelta)/denom
r2 = (tau - sqdelta)/denom
if 0 < r1 < 1:
local_extremizers.append(r1)
if 0 < r2 < 1:
local_extremizers.append(r2)
local_extrema = [bezier_point(a, t) for t in local_extremizers]
return min(local_extrema), max(local_extrema)
# find reverse standard coefficients of the derivative
dcoeffs = bezier2polynomial(a, return_poly1d=True).deriv().coeffs
# find real roots, r, such that 0 <= r <= 1
local_extremizers += polyroots01(dcoeffs)
local_extrema = [bezier_point(a, t) for t in local_extremizers]
return min(local_extrema), max(local_extrema)
def bezier_bounding_box(bez):
"""returns the bounding box for the segment in the form
(xmin, xmax, ymin, ymax).
Warning: For the non-cubic case this is not particularly efficient."""
# begin arc support block ########################
try:
bla = bez.large_arc
return bez.bbox() # added to support Arc objects
except:
pass
# end arc support block ##########################
if len(bez) == 4:
xmin, xmax = bezier_real_minmax([p.real for p in bez])
ymin, ymax = bezier_real_minmax([p.imag for p in bez])
return xmin, xmax, ymin, ymax
poly = bezier2polynomial(bez, return_poly1d=True)
x = real(poly)
y = imag(poly)
dx = x.deriv()
dy = y.deriv()
x_extremizers = [0, 1] + polyroots(dx, realroots=True,
condition=lambda r: 0 < r < 1)
y_extremizers = [0, 1] + polyroots(dy, realroots=True,
condition=lambda r: 0 < r < 1)
x_extrema = [x(t) for t in x_extremizers]
y_extrema = [y(t) for t in y_extremizers]
return min(x_extrema), max(x_extrema), min(y_extrema), max(y_extrema)
def box_area(xmin, xmax, ymin, ymax):
"""
INPUT: 2-tuple of cubics (given by control points)
OUTPUT: boolean
"""
return (xmax - xmin)*(ymax - ymin)
def interval_intersection_width(a, b, c, d):
"""returns the width of the intersection of intervals [a,b] and [c,d]
(thinking of these as intervals on the real number line)"""
return max(0, min(b, d) - max(a, c))
def boxes_intersect(box1, box2):
"""Determines if two rectangles, each input as a tuple
(xmin, xmax, ymin, ymax), intersect."""
xmin1, xmax1, ymin1, ymax1 = box1
xmin2, xmax2, ymin2, ymax2 = box2
if interval_intersection_width(xmin1, xmax1, xmin2, xmax2) and \
interval_intersection_width(ymin1, ymax1, ymin2, ymax2):
return True
else:
return False
# Intersections ###############################################################
class ApproxSolutionSet(list):
"""A class that behaves like a set but treats two elements , x and y, as
equivalent if abs(x-y) < self.tol"""
def __init__(self, tol):
self.tol = tol
def __contains__(self, x):
for y in self:
if abs(x - y) < self.tol:
return True
return False
def appadd(self, pt):
if pt not in self:
self.append(pt)
class BPair(object):
def __init__(self, bez1, bez2, t1, t2):
self.bez1 = bez1
self.bez2 = bez2
self.t1 = t1 # t value to get the mid point of this curve from cub1
self.t2 = t2 # t value to get the mid point of this curve from cub2
def bezier_intersections(bez1, bez2, longer_length, tol=1e-8, tol_deC=1e-8):
"""INPUT:
bez1, bez2 = [P0,P1,P2,...PN], [Q0,Q1,Q2,...,PN] defining the two
Bezier curves to check for intersections between.
longer_length - the length (or an upper bound) on the longer of the two
Bezier curves. Determines the maximum iterations needed together with tol.
tol - is the smallest distance that two solutions can differ by and still
be considered distinct solutions.
OUTPUT: a list of tuples (t,s) in [0,1]x[0,1] such that
abs(bezier_point(bez1[0],t) - bezier_point(bez2[1],s)) < tol_deC
Note: This will return exactly one such tuple for each intersection
(assuming tol_deC is small enough)."""
maxits = int(ceil(1-log(tol_deC/longer_length)/log(2)))
pair_list = [BPair(bez1, bez2, 0.5, 0.5)]
intersection_list = []
k = 0
approx_point_set = ApproxSolutionSet(tol)
while pair_list and k < maxits:
new_pairs = []
delta = 0.5**(k + 2)
for pair in pair_list:
bbox1 = bezier_bounding_box(pair.bez1)
bbox2 = bezier_bounding_box(pair.bez2)
if boxes_intersect(bbox1, bbox2):
if box_area(*bbox1) < tol_deC and box_area(*bbox2) < tol_deC:
point = bezier_point(bez1, pair.t1)
if point not in approx_point_set:
approx_point_set.append(point)
# this is the point in the middle of the pair
intersection_list.append((pair.t1, pair.t2))
# this prevents the output of redundant intersection points
for otherPair in pair_list:
if pair.bez1 == otherPair.bez1 or \
pair.bez2 == otherPair.bez2 or \
pair.bez1 == otherPair.bez2 or \
pair.bez2 == otherPair.bez1:
pair_list.remove(otherPair)
else:
(c11, c12) = halve_bezier(pair.bez1)
(t11, t12) = (pair.t1 - delta, pair.t1 + delta)
(c21, c22) = halve_bezier(pair.bez2)
(t21, t22) = (pair.t2 - delta, pair.t2 + delta)
new_pairs += [BPair(c11, c21, t11, t21),
BPair(c11, c22, t11, t22),
BPair(c12, c21, t12, t21),
BPair(c12, c22, t12, t22)]
pair_list = new_pairs
k += 1
if k >= maxits:
raise Exception("bezier_intersections has reached maximum "
"iterations without terminating... "
"either there's a problem/bug or you can fix by "
"raising the max iterations or lowering tol_deC")
return intersection_list
def bezier_by_line_intersections(bezier, line):
"""Returns tuples (t1,t2) such that bezier.point(t1) ~= line.point(t2)."""
# The method here is to translate (shift) then rotate the complex plane so
# that line starts at the origin and proceeds along the positive real axis.
# After this transformation, the intersection points are the real roots of
# the imaginary component of the bezier for which the real component is
# between 0 and abs(line[1]-line[0])].
assert len(line[:]) == 2
assert line[0] != line[1]
if not any(p != bezier[0] for p in bezier):
raise ValueError("bezier is nodal, use "
"bezier_by_line_intersection(bezier[0], line) "
"instead for a bool to be returned.")
# First let's shift the complex plane so that line starts at the origin
shifted_bezier = [z - line[0] for z in bezier]
shifted_line_end = line[1] - line[0]
line_length = abs(shifted_line_end)
# Now let's rotate the complex plane so that line falls on the x-axis
rotation_matrix = line_length/shifted_line_end
transformed_bezier = [rotation_matrix*z for z in shifted_bezier]
# Now all intersections should be roots of the imaginary component of
# the transformed bezier
transformed_bezier_imag = [p.imag for p in transformed_bezier]
coeffs_y = bezier2polynomial(transformed_bezier_imag)
roots_y = list(polyroots01(coeffs_y)) # returns real roots 0 <= r <= 1
transformed_bezier_real = [p.real for p in transformed_bezier]
intersection_list = []
for bez_t in set(roots_y):
xval = bezier_point(transformed_bezier_real, bez_t)
if 0 <= xval <= line_length:
line_t = xval/line_length
intersection_list.append((bez_t, line_t))
return intersection_list

View File

@ -1,21 +0,0 @@
def directional_field(curve, tvals=np.linspace(0, 1, N), asize=1e-2,
colored=False):
size = asize * curve.length()
arrows = []
tvals = np.linspace(0, 1, N)
for t in tvals:
pt = curve.point(t)
ut = curve.unit_tangent(t)
un = curve.normal(t)
l1 = Line(pt, pt + size*(un - ut)/2).reversed()
l2 = Line(pt, pt + size*(-un - ut)/2)
if colored:
arrows.append(Path(l1, l2))
else:
arrows += [l1, l2]
if colored:
colors = [(int(255*t), 0, 0) for t in tvals]
return arrows, tvals, colors
else:
return Path(arrows)

View File

@ -1,64 +0,0 @@
"""This submodule contains miscellaneous tools that are used internally, but
aren't specific to SVGs or related mathematical objects."""
# External dependencies:
from __future__ import division, absolute_import, print_function
import os
import sys
import webbrowser
# stackoverflow.com/questions/214359/converting-hex-color-to-rgb-and-vice-versa
def hex2rgb(value):
"""Converts a hexadeximal color string to an RGB 3-tuple
EXAMPLE
-------
>>> hex2rgb('#0000FF')
(0, 0, 255)
"""
value = value.lstrip('#')
lv = len(value)
return tuple(int(value[i:i+lv//3], 16) for i in range(0, lv, lv//3))
# stackoverflow.com/questions/214359/converting-hex-color-to-rgb-and-vice-versa
def rgb2hex(rgb):
"""Converts an RGB 3-tuple to a hexadeximal color string.
EXAMPLE
-------
>>> rgb2hex((0,0,255))
'#0000FF'
"""
return ('#%02x%02x%02x' % rgb).upper()
def isclose(a, b, rtol=1e-5, atol=1e-8):
"""This is essentially np.isclose, but slightly faster."""
return abs(a - b) < (atol + rtol * abs(b))
def open_in_browser(file_location):
"""Attempt to open file located at file_location in the default web
browser."""
# If just the name of the file was given, check if it's in the Current
# Working Directory.
if not os.path.isfile(file_location):
file_location = os.path.join(os.getcwd(), file_location)
if not os.path.isfile(file_location):
raise IOError("\n\nFile not found.")
# For some reason OSX requires this adjustment (tested on 10.10.4)
if sys.platform == "darwin":
file_location = "file:///"+file_location
new = 2 # open in a new tab, if possible
webbrowser.get().open(file_location, new=new)
BugException = Exception("This code should never be reached. You've found a "
"bug. Please submit an issue to \n"
"https://github.com/mathandy/svgpathtools/issues"
"\nwith an easily reproducible example.")

View File

@ -1,196 +0,0 @@
"""This submodule contains the path_parse() function used to convert SVG path
element d-strings into svgpathtools Path objects.
Note: This file was taken (nearly) as is from the svg.path module
(v 2.0)."""
# External dependencies
from __future__ import division, absolute_import, print_function
import re
# Internal dependencies
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
def _tokenize_path(pathdef):
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:
yield x
for token in FLOAT_RE.findall(x):
yield token
def parse_path(pathdef, current_pos=0j):
# In the SVG specs, initial movetos are absolute, even if
# specified as 'm'. This is the default behavior here as well.
# But if you pass in a current_pos variable, the initial moveto
# will be relative to that current_pos. This is useful.
elements = list(_tokenize_path(pathdef))
# Reverse for easy use of .pop()
elements.reverse()
segments = Path()
start_pos = None
command = None
while elements:
if elements[-1] in COMMANDS:
# New command.
last_command = command # Used by S and T
command = elements.pop()
absolute = command in UPPERCASE
command = command.upper()
else:
# If this element starts with numbers, it is an implicit command
# and we don't change the command. Check that it's allowed:
if command is None:
raise ValueError("Unallowed implicit command in %s, position %s" % (
pathdef, len(pathdef.split()) - len(elements)))
if command == 'M':
# Moveto command.
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if absolute:
current_pos = pos
else:
current_pos += pos
# when M is called, reset start_pos
# This behavior of Z is defined in svg spec:
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
start_pos = current_pos
# Implicit moveto commands are treated as lineto commands.
# So we set command to lineto here, in case there are
# further implicit commands after this moveto.
command = 'L'
elif command == 'Z':
# Close path
if not (current_pos == start_pos):
segments.append(Line(current_pos, start_pos))
segments.closed = True
current_pos = start_pos
start_pos = None
command = None # You can't have implicit commands after closing.
elif command == 'L':
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if not absolute:
pos += current_pos
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'H':
x = elements.pop()
pos = float(x) + current_pos.imag * 1j
if not absolute:
pos += current_pos.real
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'V':
y = elements.pop()
pos = current_pos.real + float(y) * 1j
if not absolute:
pos += current_pos.imag * 1j
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'C':
control1 = float(elements.pop()) + float(elements.pop()) * 1j
control2 = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
control1 += current_pos
control2 += current_pos
end += current_pos
segments.append(CubicBezier(current_pos, control1, control2, end))
current_pos = end
elif command == 'S':
# Smooth curve. First control point is the "reflection" of
# the second control point in the previous path.
if last_command not in 'CS':
# If there is no previous command or if the previous command
# was not an C, c, S or s, assume the first control point is
# coincident with the current point.
control1 = current_pos
else:
# The first control point is assumed to be the reflection of
# the second control point on the previous command relative
# to the current point.
control1 = current_pos + current_pos - segments[-1].control2
control2 = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
control2 += current_pos
end += current_pos
segments.append(CubicBezier(current_pos, control1, control2, end))
current_pos = end
elif command == 'Q':
control = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
control += current_pos
end += current_pos
segments.append(QuadraticBezier(current_pos, control, end))
current_pos = end
elif command == 'T':
# Smooth curve. Control point is the "reflection" of
# the second control point in the previous path.
if last_command not in 'QT':
# If there is no previous command or if the previous command
# was not an Q, q, T or t, assume the first control point is
# coincident with the current point.
control = current_pos
else:
# The control point is assumed to be the reflection of
# the control point on the previous command relative
# to the current point.
control = current_pos + current_pos - segments[-1].control
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(QuadraticBezier(current_pos, control, end))
current_pos = end
elif command == 'A':
radius = float(elements.pop()) + float(elements.pop()) * 1j
rotation = float(elements.pop())
arc = float(elements.pop())
sweep = float(elements.pop())
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(Arc(current_pos, radius, rotation, arc, sweep, end))
current_pos = end
return segments

File diff suppressed because it is too large Load Diff

View File

@ -1,387 +0,0 @@
"""This submodule contains tools for creating svg files from paths and path
segments."""
# External dependencies:
from __future__ import division, absolute_import, print_function
from math import ceil
from os import getcwd, path as os_path, makedirs
from xml.dom.minidom import parse as md_xml_parse
from svgwrite import Drawing, text as txt
from time import time
from warnings import warn
# Internal dependencies
from .path import Path, Line, is_path_segment
from .misctools import open_in_browser
# Used to convert a string colors (identified by single chars) to a list.
color_dict = {'a': 'aqua',
'b': 'blue',
'c': 'cyan',
'd': 'darkblue',
'e': '',
'f': '',
'g': 'green',
'h': '',
'i': '',
'j': '',
'k': 'black',
'l': 'lime',
'm': 'magenta',
'n': 'brown',
'o': 'orange',
'p': 'pink',
'q': 'turquoise',
'r': 'red',
's': 'salmon',
't': 'tan',
'u': 'purple',
'v': 'violet',
'w': 'white',
'x': '',
'y': 'yellow',
'z': 'azure'}
def str2colorlist(s, default_color=None):
color_list = [color_dict[ch] for ch in s]
if default_color:
for idx, c in enumerate(color_list):
if not c:
color_list[idx] = default_color
return color_list
def is3tuple(c):
return isinstance(c, tuple) and len(c) == 3
def big_bounding_box(paths_n_stuff):
"""Finds a BB containing a collection of paths, Bezier path segments, and
points (given as complex numbers)."""
bbs = []
for thing in paths_n_stuff:
if is_path_segment(thing) or isinstance(thing, Path):
bbs.append(thing.bbox())
elif isinstance(thing, complex):
bbs.append((thing.real, thing.real, thing.imag, thing.imag))
else:
try:
complexthing = complex(thing)
bbs.append((complexthing.real, complexthing.real,
complexthing.imag, complexthing.imag))
except ValueError:
raise TypeError(
"paths_n_stuff can only contains Path, CubicBezier, "
"QuadraticBezier, Line, and complex objects.")
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
xmin = min(xmins)
xmax = max(xmaxs)
ymin = min(ymins)
ymax = max(ymaxs)
return xmin, xmax, ymin, ymax
def disvg(paths=None, colors=None,
filename=os_path.join(getcwd(), 'disvg_output.svg'),
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
openinbrowser=True, timestamp=False,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None, font_size=None,
attributes=None, svg_attributes=None):
"""Takes in a list of paths and creates an SVG file containing said paths.
REQUIRED INPUTS:
:param paths - a list of paths
OPTIONAL INPUT:
:param colors - specifies the path stroke color. By default all paths
will be black (#000000). This paramater can be input in a few ways
1) a list of strings that will be input into the path elements stroke
attribute (so anything that is understood by the svg viewer).
2) a string of single character colors -- e.g. setting colors='rrr' is
equivalent to setting colors=['red', 'red', 'red'] (see the
'color_dict' dictionary above for a list of possibilities).
3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...].
:param filename - the desired location/filename of the SVG file
created (by default the SVG will be stored in the current working
directory and named 'disvg_output.svg').
:param stroke_widths - a list of stroke_widths to use for paths
(default is 0.5% of the SVG's width or length)
:param nodes - a list of points to draw as filled-in circles
:param node_colors - a list of colors to use for the nodes (by default
nodes will be red)
:param node_radii - a list of radii to use for the nodes (by default
nodes will be radius will be 1 percent of the svg's width/length)
:param text - string or list of strings to be displayed
:param text_path - if text is a list, then this should be a list of
path (or path segments of the same length. Note: the path must be
long enough to display the text or the text will be cropped by the svg
viewer.
:param font_size - a single float of list of floats.
:param openinbrowser - Set to True to automatically open the created
SVG in the user's default web browser.
:param timestamp - if True, then the a timestamp will be appended to
the output SVG's filename. This will fix issues with rapidly opening
multiple SVGs in your browser.
:param margin_size - The min margin (empty area framing the collection
of paths) size used for creating the canvas and background of the SVG.
:param mindim - The minimum dimension (height or width) of the output
SVG (default is 600).
:param dimensions - The display dimensions of the output SVG. Using
this will override the mindim parameter.
:param viewbox - This specifies what rectangular patch of R^2 will be
viewable through the outputSVG. It should be input in the form
(min_x, min_y, width, height). This is different from the display
dimension of the svg, which can be set through mindim or dimensions.
:param attributes - a list of dictionaries of attributes for the input
paths. Note: This will override any other conflicting settings.
:param svg_attributes - a dictionary of attributes for output svg.
Note 1: This will override any other conflicting settings.
Note 2: Setting `svg_attributes={'debug': False}` may result in a
significant increase in speed.
NOTES:
-The unit of length here is assumed to be pixels in all variables.
-If this function is used multiple times in quick succession to
display multiple SVGs (all using the default filename), the
svgviewer/browser will likely fail to load some of the SVGs in time.
To fix this, use the timestamp attribute, or give the files unique
names, or use a pause command (e.g. time.sleep(1)) between uses.
"""
_default_relative_node_radius = 5e-3
_default_relative_stroke_width = 1e-3
_default_path_color = '#000000' # black
_default_node_color = '#ff0000' # red
_default_font_size = 12
# append directory to filename (if not included)
if os_path.dirname(filename) == '':
filename = os_path.join(getcwd(), filename)
# append time stamp to filename
if timestamp:
fbname, fext = os_path.splitext(filename)
dirname = os_path.dirname(filename)
tstamp = str(time()).replace('.', '')
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
filename = os_path.join(dirname, stfilename)
# check paths and colors are set
if isinstance(paths, Path) or is_path_segment(paths):
paths = [paths]
if paths:
if not colors:
colors = [_default_path_color] * len(paths)
else:
assert len(colors) == len(paths)
if isinstance(colors, str):
colors = str2colorlist(colors,
default_color=_default_path_color)
elif isinstance(colors, list):
for idx, c in enumerate(colors):
if is3tuple(c):
colors[idx] = "rgb" + str(c)
# check nodes and nodes_colors are set (node_radii are set later)
if nodes:
if not node_colors:
node_colors = [_default_node_color] * len(nodes)
else:
assert len(node_colors) == len(nodes)
if isinstance(node_colors, str):
node_colors = str2colorlist(node_colors,
default_color=_default_node_color)
elif isinstance(node_colors, list):
for idx, c in enumerate(node_colors):
if is3tuple(c):
node_colors[idx] = "rgb" + str(c)
# set up the viewBox and display dimensions of the output SVG
# along the way, set stroke_widths and node_radii if not provided
assert paths or nodes
stuff2bound = []
if viewbox:
szx, szy = viewbox[2:4]
else:
if paths:
stuff2bound += paths
if nodes:
stuff2bound += nodes
if text_path:
stuff2bound += text_path
xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound)
dx = xmax - xmin
dy = ymax - ymin
if dx == 0:
dx = 1
if dy == 0:
dy = 1
# determine stroke_widths to use (if not provided) and max_stroke_width
if paths:
if not stroke_widths:
sw = max(dx, dy) * _default_relative_stroke_width
stroke_widths = [sw]*len(paths)
max_stroke_width = sw
else:
assert len(paths) == len(stroke_widths)
max_stroke_width = max(stroke_widths)
else:
max_stroke_width = 0
# determine node_radii to use (if not provided) and max_node_diameter
if nodes:
if not node_radii:
r = max(dx, dy) * _default_relative_node_radius
node_radii = [r]*len(nodes)
max_node_diameter = 2*r
else:
assert len(nodes) == len(node_radii)
max_node_diameter = 2*max(node_radii)
else:
max_node_diameter = 0
extra_space_for_style = max(max_stroke_width, max_node_diameter)
xmin -= margin_size*dx + extra_space_for_style/2
ymin -= margin_size*dy + extra_space_for_style/2
dx += 2*margin_size*dx + extra_space_for_style
dy += 2*margin_size*dy + extra_space_for_style
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
if dimensions:
szx, szy = dimensions
else:
if dx > dy:
szx = str(mindim) + 'px'
szy = str(int(ceil(mindim * dy / dx))) + 'px'
else:
szx = str(int(ceil(mindim * dx / dy))) + 'px'
szy = str(mindim) + 'px'
# Create an SVG file
if svg_attributes:
dwg = Drawing(filename=filename, **svg_attributes)
else:
dwg = Drawing(filename=filename, size=(szx, szy), viewBox=viewbox)
# add paths
if paths:
for i, p in enumerate(paths):
if isinstance(p, Path):
ps = p.d()
elif is_path_segment(p):
ps = Path(p).d()
else: # assume this path, p, was input as a Path d-string
ps = p
if attributes:
good_attribs = {'d': ps}
for key in attributes[i]:
val = attributes[i][key]
if key != 'd':
try:
dwg.path(ps, **{key: val})
good_attribs.update({key: val})
except Exception as e:
warn(str(e))
dwg.add(dwg.path(**good_attribs))
else:
dwg.add(dwg.path(ps, stroke=colors[i],
stroke_width=str(stroke_widths[i]),
fill='none'))
# add nodes (filled in circles)
if nodes:
for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]):
dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt]))
# add texts
if text:
assert isinstance(text, str) or (isinstance(text, list) and
isinstance(text_path, list) and
len(text_path) == len(text))
if isinstance(text, str):
text = [text]
if not font_size:
font_size = [_default_font_size]
if not text_path:
pos = complex(xmin + margin_size*dx, ymin + margin_size*dy)
text_path = [Line(pos, pos + 1).d()]
else:
if font_size:
if isinstance(font_size, list):
assert len(font_size) == len(text)
else:
font_size = [font_size] * len(text)
else:
font_size = [_default_font_size] * len(text)
for idx, s in enumerate(text):
p = text_path[idx]
if isinstance(p, Path):
ps = p.d()
elif is_path_segment(p):
ps = Path(p).d()
else: # assume this path, p, was input as a Path d-string
ps = p
# paragraph = dwg.add(dwg.g(font_size=font_size[idx]))
# paragraph.add(dwg.textPath(ps, s))
pathid = 'tp' + str(idx)
dwg.defs.add(dwg.path(d=ps, id=pathid))
txter = dwg.add(dwg.text('', font_size=font_size[idx]))
txter.add(txt.TextPath('#'+pathid, s))
# save svg
if not os_path.exists(os_path.dirname(filename)):
makedirs(os_path.dirname(filename))
dwg.save()
# re-open the svg, make the xml pretty, and save it again
xmlstring = md_xml_parse(filename).toprettyxml()
with open(filename, 'w') as f:
f.write(xmlstring)
# try to open in web browser
if openinbrowser:
try:
open_in_browser(filename)
except:
print("Failed to open output SVG in browser. SVG saved to:")
print(filename)
def wsvg(paths=None, colors=None,
filename=os_path.join(getcwd(), 'disvg_output.svg'),
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
openinbrowser=False, timestamp=False,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None, font_size=None,
attributes=None, svg_attributes=None):
"""Convenience function; identical to disvg() except that
openinbrowser=False by default. See disvg() docstring for more info."""
disvg(paths, colors=colors, filename=filename,
stroke_widths=stroke_widths, nodes=nodes,
node_colors=node_colors, node_radii=node_radii,
openinbrowser=openinbrowser, timestamp=timestamp,
margin_size=margin_size, mindim=mindim, dimensions=dimensions,
viewbox=viewbox, text=text, text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes)

View File

@ -1,14 +0,0 @@
"""This submodule contains additional tools for working with paths composed of
Line and CubicBezier objects. QuadraticBezier and Arc objects are only
partially supported."""
# External dependencies:
from __future__ import division, absolute_import, print_function
# Internal dependencies
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
# Misc#########################################################################

View File

@ -1,80 +0,0 @@
"""This submodule contains tools for working with numpy.poly1d objects."""
# External Dependencies
from __future__ import division, absolute_import
from itertools import combinations
import numpy as np
# Internal Dependencies
from .misctools import isclose
def polyroots(p, realroots=False, condition=lambda r: True):
"""
Returns the roots of a polynomial with coefficients given in p.
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
INPUT:
p - Rank-1 array-like object of polynomial coefficients.
realroots - a boolean. If true, only real roots will be returned and the
condition function can be written assuming all roots are real.
condition - a boolean-valued function. Only roots satisfying this will be
returned. If realroots==True, these conditions should assume the roots
are real.
OUTPUT:
A list containing the roots of the polynomial.
NOTE: This uses np.isclose and np.roots"""
roots = np.roots(p)
if realroots:
roots = [r.real for r in roots if isclose(r.imag, 0)]
roots = [r for r in roots if condition(r)]
duplicates = []
for idx, (r1, r2) in enumerate(combinations(roots, 2)):
if isclose(r1, r2):
duplicates.append(idx)
return [r for idx, r in enumerate(roots) if idx not in duplicates]
def polyroots01(p):
"""Returns the real roots between 0 and 1 of the polynomial with
coefficients given in p,
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
p can also be a np.poly1d object. See polyroots for more information."""
return polyroots(p, realroots=True, condition=lambda tval: 0 <= tval <= 1)
def rational_limit(f, g, t0):
"""Computes the limit of the rational function (f/g)(t)
as t approaches t0."""
assert isinstance(f, np.poly1d) and isinstance(g, np.poly1d)
assert g != np.poly1d([0])
if g(t0) != 0:
return f(t0)/g(t0)
elif f(t0) == 0:
return rational_limit(f.deriv(), g.deriv(), t0)
else:
raise ValueError("Limit does not exist.")
def real(z):
try:
return np.poly1d(z.coeffs.real)
except AttributeError:
return z.real
def imag(z):
try:
return np.poly1d(z.coeffs.imag)
except AttributeError:
return z.imag
def poly_real_part(poly):
"""Deprecated."""
return np.poly1d(poly.coeffs.real)
def poly_imag_part(poly):
"""Deprecated."""
return np.poly1d(poly.coeffs.imag)

View File

@ -1,201 +0,0 @@
"""This submodule contains functions related to smoothing paths of Bezier
curves."""
# External Dependencies
from __future__ import division, absolute_import, print_function
# Internal Dependencies
from .path import Path, CubicBezier, Line
from .misctools import isclose
from .paths2svg import disvg
def is_differentiable(path, tol=1e-8):
for idx in range(len(path)):
u = path[(idx-1) % len(path)].unit_tangent(1)
v = path[idx].unit_tangent(0)
u_dot_v = u.real*v.real + u.imag*v.imag
if abs(u_dot_v - 1) > tol:
return False
return True
def kinks(path, tol=1e-8):
"""returns indices of segments that start on a non-differentiable joint."""
kink_list = []
for idx in range(len(path)):
if idx == 0 and not path.isclosed():
continue
try:
u = path[(idx - 1) % len(path)].unit_tangent(1)
v = path[idx].unit_tangent(0)
u_dot_v = u.real*v.real + u.imag*v.imag
flag = False
except ValueError:
flag = True
if flag or abs(u_dot_v - 1) > tol:
kink_list.append(idx)
return kink_list
def _report_unfixable_kinks(_path, _kink_list):
mes = ("\n%s kinks have been detected at that cannot be smoothed.\n"
"To ignore these kinks and fix all others, run this function "
"again with the second argument 'ignore_unfixable_kinks=True' "
"The locations of the unfixable kinks are at the beginnings of "
"segments: %s" % (len(_kink_list), _kink_list))
disvg(_path, nodes=[_path[idx].start for idx in _kink_list])
raise Exception(mes)
def smoothed_joint(seg0, seg1, maxjointsize=3, tightness=1.99):
""" See Andy's notes on
Smoothing Bezier Paths for an explanation of the method.
Input: two segments seg0, seg1 such that seg0.end==seg1.start, and
jointsize, a positive number
Output: seg0_trimmed, elbow, seg1_trimmed, where elbow is a cubic bezier
object that smoothly connects seg0_trimmed and seg1_trimmed.
"""
assert seg0.end == seg1.start
assert 0 < maxjointsize
assert 0 < tightness < 2
# sgn = lambda x:x/abs(x)
q = seg0.end
try: v = seg0.unit_tangent(1)
except: v = seg0.unit_tangent(1 - 1e-4)
try: w = seg1.unit_tangent(0)
except: w = seg1.unit_tangent(1e-4)
max_a = maxjointsize / 2
a = min(max_a, min(seg1.length(), seg0.length()) / 20)
if isinstance(seg0, Line) and isinstance(seg1, Line):
'''
Note: Letting
c(t) = elbow.point(t), v= the unit tangent of seg0 at 1, w = the
unit tangent vector of seg1 at 0,
Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
The elbow will be the unique CubicBezier, c, such that
c(0)= Q-av, c(1)=Q+aw, c'(0) = bv, and c'(1) = bw
where a and b are derived above/below from tightness and
maxjointsize.
'''
# det = v.imag*w.real-v.real*w.imag
# Note:
# If det is negative, the curvature of elbow is negative for all
# real t if and only if b/a > 6
# If det is positive, the curvature of elbow is negative for all
# real t if and only if b/a < 2
# if det < 0:
# b = (6+tightness)*a
# elif det > 0:
# b = (2-tightness)*a
# else:
# raise Exception("seg0 and seg1 are parallel lines.")
b = (2 - tightness)*a
elbow = CubicBezier(q - a*v, q - (a - b/3)*v, q + (a - b/3)*w, q + a*w)
seg0_trimmed = Line(seg0.start, elbow.start)
seg1_trimmed = Line(elbow.end, seg1.end)
return seg0_trimmed, [elbow], seg1_trimmed
elif isinstance(seg0, Line):
'''
Note: Letting
c(t) = elbow.point(t), v= the unit tangent of seg0 at 1,
w = the unit tangent vector of seg1 at 0,
Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
The elbow will be the unique CubicBezier, c, such that
c(0)= Q-av, c(1)=Q, c'(0) = bv, and c'(1) = bw
where a and b are derived above/below from tightness and
maxjointsize.
'''
# det = v.imag*w.real-v.real*w.imag
# Note: If g has the same sign as det, then the curvature of elbow is
# negative for all real t if and only if b/a < 4
b = (4 - tightness)*a
# g = sgn(det)*b
elbow = CubicBezier(q - a*v, q + (b/3 - a)*v, q - b/3*w, q)
seg0_trimmed = Line(seg0.start, elbow.start)
return seg0_trimmed, [elbow], seg1
elif isinstance(seg1, Line):
args = (seg1.reversed(), seg0.reversed(), maxjointsize, tightness)
rseg1_trimmed, relbow, rseg0 = smoothed_joint(*args)
elbow = relbow[0].reversed()
return seg0, [elbow], rseg1_trimmed.reversed()
else:
# find a point on each seg that is about a/2 away from joint. Make
# line between them.
t0 = seg0.ilength(seg0.length() - a/2)
t1 = seg1.ilength(a/2)
seg0_trimmed = seg0.cropped(0, t0)
seg1_trimmed = seg1.cropped(t1, 1)
seg0_line = Line(seg0_trimmed.end, q)
seg1_line = Line(q, seg1_trimmed.start)
args = (seg0_trimmed, seg0_line, maxjointsize, tightness)
dummy, elbow0, seg0_line_trimmed = smoothed_joint(*args)
args = (seg1_line, seg1_trimmed, maxjointsize, tightness)
seg1_line_trimmed, elbow1, dummy = smoothed_joint(*args)
args = (seg0_line_trimmed, seg1_line_trimmed, maxjointsize, tightness)
seg0_line_trimmed, elbowq, seg1_line_trimmed = smoothed_joint(*args)
elbow = elbow0 + [seg0_line_trimmed] + elbowq + [seg1_line_trimmed] + elbow1
return seg0_trimmed, elbow, seg1_trimmed
def smoothed_path(path, maxjointsize=3, tightness=1.99, ignore_unfixable_kinks=False):
"""returns a path with no non-differentiable joints."""
if len(path) == 1:
return path
assert path.iscontinuous()
sharp_kinks = []
new_path = [path[0]]
for idx in range(len(path)):
if idx == len(path)-1:
if not path.isclosed():
continue
else:
seg1 = new_path[0]
else:
seg1 = path[idx + 1]
seg0 = new_path[-1]
try:
unit_tangent0 = seg0.unit_tangent(1)
unit_tangent1 = seg1.unit_tangent(0)
flag = False
except ValueError:
flag = True # unit tangent not well-defined
if not flag and isclose(unit_tangent0, unit_tangent1): # joint is already smooth
if idx != len(path)-1:
new_path.append(seg1)
continue
else:
kink_idx = (idx + 1) % len(path) # kink at start of this seg
if not flag and isclose(-unit_tangent0, unit_tangent1):
# joint is sharp 180 deg (must be fixed manually)
new_path.append(seg1)
sharp_kinks.append(kink_idx)
else: # joint is not smooth, let's smooth it.
args = (seg0, seg1, maxjointsize, tightness)
new_seg0, elbow_segs, new_seg1 = smoothed_joint(*args)
new_path[-1] = new_seg0
new_path += elbow_segs
if idx == len(path) - 1:
new_path[0] = new_seg1
else:
new_path.append(new_seg1)
# If unfixable kinks were found, let the user know
if sharp_kinks and not ignore_unfixable_kinks:
_report_unfixable_kinks(path, sharp_kinks)
return Path(*new_path)

View File

@ -1,126 +0,0 @@
"""This submodule contains tools for creating path objects from SVG files.
The main tool being the svg2paths() function."""
# External dependencies
from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse
from os import path as os_path, getcwd
from shutil import copyfile
# Internal dependencies
from .parser import parse_path
def polyline2pathd(polyline_d):
"""converts the string from a polyline d-attribute to a string for a Path
object d-attribute"""
points = polyline_d.replace(', ', ',')
points = points.replace(' ,', ',')
points = points.split()
if points[0] == points[-1]:
closed = True
else:
closed = False
d = 'M' + points.pop(0).replace(',', ' ')
for p in points:
d += 'L' + p.replace(',', ' ')
if closed:
d += 'z'
return d
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
"""
if os_path.dirname(svg_file_location) == '':
svg_file_location = os_path.join(getcwd(), svg_file_location)
# if pathless_svg:
# copyfile(svg_file_location, pathless_svg)
# doc = parse(pathless_svg)
# else:
doc = parse(svg_file_location)
def dom2dict(element):
"""Converts DOM elements to dictionaries of attributes."""
keys = list(element.attributes.keys())
values = [val.value for val in list(element.attributes.values())]
return dict(list(zip(keys, values)))
# Use minidom to extract path strings from input SVG
paths = [dom2dict(el) for el in doc.getElementsByTagName('path')]
d_strings = [el['d'] for el in paths]
attribute_dictionary_list = paths
# if pathless_svg:
# for el in doc.getElementsByTagName('path'):
# el.parentNode.removeChild(el)
# Use minidom to extract polyline strings from input SVG, convert to
# path strings, add to list
if convert_polylines_to_paths:
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
d_strings += [polyline2pathd(pl['points']) for pl in plins]
attribute_dictionary_list += plins
# Use minidom to extract polygon strings from input SVG, convert to
# path strings, add to list
if convert_polygons_to_paths:
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
d_strings += [polyline2pathd(pg['points']) + 'z' for pg in pgons]
attribute_dictionary_list += pgons
if convert_lines_to_paths:
lines = [dom2dict(el) for el in doc.getElementsByTagName('line')]
d_strings += [('M' + l['x1'] + ' ' + l['y1'] +
'L' + l['x2'] + ' ' + l['y2']) for l in lines]
attribute_dictionary_list += lines
# if pathless_svg:
# with open(pathless_svg, "wb") as f:
# doc.writexml(f)
if return_svg_attributes:
svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0])
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list, svg_attributes
else:
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list
def svg2paths2(svg_file_location,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
return_svg_attributes=True):
"""Convenience function; identical to svg2paths() except that
return_svg_attributes=True by default. See svg2paths() docstring for more
info."""
return svg2paths(svg_file_location=svg_file_location,
convert_lines_to_paths=convert_lines_to_paths,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
return_svg_attributes=return_svg_attributes)

View File

@ -1,662 +0,0 @@
Metadata-Version: 1.1
Name: svgpathtools
Version: 1.3.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.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
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 <https://github.com/regebro/svg.path>`__. 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 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 <http://www.w3.org/TR/SVG/paths.html>`__ 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:: ipython2
from __future__ import division, print_function
.. code:: ipython2
# 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:: ipython2
# 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:: ipython2
# 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:: ipython2
# 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:: ipython2
# 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
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 <https://en.wikipedia.org/wiki/B%C3%A9zier_curve#General_definition>`__.
.. code:: ipython2
# 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)
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 <https://github.com/mathandy/svgpathtools/blob/master/examples/compute-many-points-quickly-using-numpy-arrays.py>`__
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)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: ipython2
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:: ipython2
# 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:: ipython2
# 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:: ipython2
# 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:: ipython2
# 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 <https://en.wikipedia.org/wiki/Parallel_curve>`__ for a few paths.
.. code:: ipython2
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:
for k in range(steps):
t = k / float(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

View File

@ -1,39 +0,0 @@
LICENSE.txt
LICENSE2.txt
MANIFEST.in
README.rst
decorated_ellipse.svg
offset_curves.svg
output1.svg
output2.svg
output_intersections.svg
path.svg
setup.cfg
setup.py
test.svg
vectorframes.svg
svgpathtools/__init__.py
svgpathtools/bezier.py
svgpathtools/misctools.py
svgpathtools/parser.py
svgpathtools/path.py
svgpathtools/paths2svg.py
svgpathtools/polytools.py
svgpathtools/smoothing.py
svgpathtools/svg_to_paths.py
svgpathtools.egg-info/PKG-INFO
svgpathtools.egg-info/SOURCES.txt
svgpathtools.egg-info/dependency_links.txt
svgpathtools.egg-info/requires.txt
svgpathtools.egg-info/top_level.txt
test/circle.svg
test/ellipse.svg
test/polygons.svg
test/rects.svg
test/test.svg
test/test_bezier.py
test/test_generation.py
test/test_parsing.py
test/test_path.py
test/test_polytools.py
test/test_svg2paths.py

View File

@ -1 +0,0 @@

View File

@ -1,2 +0,0 @@
numpy
svgwrite

View File

@ -1 +0,0 @@
svgpathtools