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