Skip to main content

一组用于操作和分析 SVG 路径对象和贝塞尔曲线的工具。

项目描述

捐 Python 派皮 建造 PyPI - 下载

SVG路径工具

svgpathtools 是一组用于操作和分析 SVG 路径对象和贝塞尔曲线的工具。

特征

svgpathtools 包含旨在轻松读取、写入和显示 SVG 文件的功能,以及用于转换和分析路径元素的大量面向几何的工具

此外,子模块bezier.py包含用于处理存储为 n-tuples 的一般 n 阶贝塞尔曲线的工具。

一些包含的工具:

  • 读取写入显示包含 Path(和其他)SVG 元素的 SVG 文件
  • 将贝塞尔路径段转换为numpy.poly1d(多项式)对象
  • 将多项式(标准形式)转换为它们的贝塞尔形式
  • 计算切向量和(右手法则)法向量
  • 计算曲率
  • 将不连续的路径分解成它们的连续子路径。
  • 有效地计算路径和/或线段之间的交叉点
  • 找到路径或段的边界框
  • 反向段/路径方向
  • 裁剪拆分路径和段
  • 平滑路径(即消除扭结以使路径可区分)
  • 从路径域到段域并返回(T2t 和 t2T)的转换映射
  • 封闭路径包围的计算区域
  • 计算弧长
  • 计算反弧长
  • 将 RGB 颜色元组转换为十六进制颜色字符串并返回

先决条件

  • 麻木的
  • svgwrite
  • scipy(可选,但推荐用于性能)

设置

$ pip install svgpathtools

替代设置

您可以从 Github 下载源代码并使用以下命令进行安装(从包含 setup.py 的文件夹中):

$ python setup.py install

信用到期

这个模块的大部分核心取自svg.path (v2.0) 模块。有兴趣的 svg.path 用户应该查看本自述文件底部的兼容性说明。

基本用法

课程

svgpathtools 模块主要围绕四个路径段类构建:LineQuadraticBezierCubicBezierArc。还有第五类,Path,其对象是(连接或断开1)路径段对象的序列。

  • Line(start, end)

  • Arc(start, radius, rotation, large_arc, sweep, end) 注意:有关这些参数的详细说明,请参阅 docstring

  • QuadraticBezier(start, control, end)

  • CubicBezier(start, control1, control2, end)

  • Path(*segments)

有关每个参数含义的更多信息,请参阅path.py官方 SVG 规范中的相关文档字符串。

<u id="f1">1</u> 警告:此库中的某些功能尚未在不连续的 Path 对象上进行测试。然而,该方法提供了一个简单的解决Path.continuous_subpaths()方法。

from __future__ import division, print_function
# Coordinates are given as points in the complex plane
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc
seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)  # A cubic beginning at (300, 100) and ending at (200, 300)
seg2 = Line(200+300j, 250+350j)  # A line beginning at (200, 300) and ending at (250, 350)
path = Path(seg1, seg2)  # A path traversing the cubic and then the line

# We could alternatively created this Path object using a d-string
from svgpathtools import parse_path
path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')

# Let's check that these two methods are equivalent
print(path)
print(path_alt)
print(path == path_alt)

# On a related note, the Path.d() method returns a Path object's d-string
print(path.d())
print(parse_path(path.d()) == path)
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
     Line(start=(200+300j), end=(250+350j)))
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
     Line(start=(200+300j), end=(250+350j)))
True
M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0
True

该类Path是一个可变序列,因此它的行为很像一个列表。所以段可以追加插入、按索引设置、删除枚举d、切片d等。

# Let's append another to the end of it
path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))
print(path)

# Let's replace the first segment with a Line object
path[0] = Line(200+100j, 200+300j)
print(path)

# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)
print("path is continuous? ", path.iscontinuous())
print("path is closed? ", path.isclosed())

# The curve the path follows is not, however, smooth (differentiable)
from svgpathtools import kinks, smoothed_path
print("path contains non-differentiable points? ", len(kinks(path)) > 0)

# If we want, we can smooth these out (Experimental and only for line/cubic paths)
# Note:  smoothing will always works (except on 180 degree turns), but you may want 
# to play with the maxjointsize and tightness parameters to get pleasing results
# Note also: smoothing will increase the number of segments in a path
spath = smoothed_path(path)
print("spath contains non-differentiable points? ", len(kinks(spath)) > 0)
print(spath)

# Let's take a quick look at the path and its smoothed relative
# The following commands will open two browser windows to display path and spaths
from svgpathtools import disvg
from time import sleep
disvg(path) 
sleep(1)  # needed when not giving the SVGs unique names (or not using timestamp)
disvg(spath)
print("Notice that path contains {} segments and spath contains {} segments."
      "".format(len(path), len(spath)))
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
     Line(start=(200+300j), end=(250+350j)),
     CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
Path(Line(start=(200+100j), end=(200+300j)),
     Line(start=(200+300j), end=(250+350j)),
     CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
path is continuous?  True
path is closed?  True
path contains non-differentiable points?  True
spath contains non-differentiable points?  False
Path(Line(start=(200+101.5j), end=(200+298.5j)),
     CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),
     Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),
     CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),
     CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),
     CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))
Notice that path contains 3 segments and spath contains 6 segments.

读取 SVGS

svg2paths ()函数将 svgfile 转换为 Path 对象列表和包含每个所述路径属性的单独字典列表。
注意:Line、Polyline、Polygon 和 Path SVG 元素都可以使用此函数转换为 Path 对象。

# Read SVG into a list of path objects and list of dictionaries of attributes 
from svgpathtools import svg2paths, wsvg
paths, attributes = svg2paths('test.svg')

# Update: You can now also extract the svg-attributes by setting
# return_svg_attributes=True, or with the convenience function svg2paths2
from svgpathtools import svg2paths2
paths, attributes, svg_attributes = svg2paths2('test.svg')

# Let's print out the first path object and the color it was in the SVG
# We'll see it is composed of two CubicBezier objects and, in the SVG file it 
# came from, it was red
redpath = paths[0]
redpath_attribs = attributes[0]
print(redpath)
print(redpath_attribs['stroke'])
Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),
     CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))
red

编写 SVGS(以及一些几何函数和方法)

wsvg ()函数从路径列表创建一个 SVG 文件。这个函数可以做很多事情(有关更多信息,请参见paths2svg.py中的文档字符串)并且旨在快速且易于使用。注意:使用便捷函数disvg()(或设置 'openinbrowser=True')自动尝试在默认 SVG 查看器中打开创建的 svg 文件。

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

输出1.svg

下面将有更多编写和显示路径数据的示例。

.point() 方法和路径和路径段参数化之间的转换

SVG 路径元素及其段具有官方参数化。可以使用Path.point()Line.point()QuadraticBezier.point()CubicBezier.point()Arc.point()方法访问这些参数化。所有这些参数化都是在域 0 <= t <= 1 上定义的。

注意:在本文档以及内联文档和文档中,我T在提及 Path 对象的参数化时使用大写字母,t在提及路径段对象(即 Line、QaudraticBezier、CubicBezier 和 Arc 对象)时使用小写字母。
给定一个T值,该Path.T2t()方法可用于查找相应的段索引k和段参数 ,t使得path.point(T)=path[k].point(t)
还有一种Path.t2T()方法可以解决逆问题。

# Example:

# Let's check that the first segment of redpath starts 
# at the same point as redpath
firstseg = redpath[0] 
print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)

# Let's check that the last segment of redpath ends on the same point as redpath
lastseg = redpath[-1] 
print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)

# This next boolean should return False as redpath is composed multiple segments
print(redpath.point(0.5) == firstseg.point(0.5))

# If we want to figure out which segment of redpoint the 
# point redpath.point(0.5) lands on, we can use the path.T2t() method
k, t = redpath.T2t(0.5)
print(redpath[k].point(t) == redpath.point(0.5))
True
True
False
True

贝塞尔曲线作为 NumPy 多项式对象

Line使用、QuadraticBezier和对象的参数化的另一种好方法CubicBezier是将它们转换为numpy.poly1d对象。Line.poly()使用,QuadraticBezier.poly()CubicBezier.poly()方法很容易做到这一点。
pathtools.py 子模块中还有一个polynomial2bezier()函数可以将多项式转换回贝塞尔曲线。

注意:三次贝塞尔曲线参数化为 $$\mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3 $$ 其中 $P_0$、$P_1$、$P_2$ 和 $P_3$分别是 svgpathtools 用于定义 CubicBezier 对象的控制点startcontrol1control2和。endCubicBezier.poly()方法将此多项式扩展为其标准形式 $$\mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3$$ 其中 $$\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()并且Line.poly()定义类似

# Example:
b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)
p = b.poly()

# p(t) == b.point(t)
print(p(0.235) == b.point(0.235))

# What is p(t)?  It's just the cubic b written in standard form.  
bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints())
print("The CubicBezier, b.point(x) = \n\n" + 
      bpretty + "\n\n" + 
      "can be rewritten in standard form as \n\n" +
      str(p).replace('x','t'))
True
The CubicBezier, b.point(x) = 

(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3

can be rewritten in standard form as 

                3                2
(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)

将 Bezier 对象转换为 NumPy 多项式对象的能力非常有用。对于初学者,我们可以将 Bézier 段列表转换为 NumPy 数组

Bézier 路径段上的 Numpy 数组操作

此处提供示例

为了进一步说明能够将我们的 Bezier 曲线对象转换为 numpy.poly1d 对象并返回的能力,让我们以四种不同的方式计算上述 CubicBezier 对象在 t=0.5 时的单位切线向量 b。

切向量(更多关于 NumPy 多项式)

t = 0.5
### Method 1: the easy way
u1 = b.unit_tangent(t)

### Method 2: another easy way 
# Note: This way will fail if it encounters a removable singularity.
u2 = b.derivative(t)/abs(b.derivative(t))

### Method 2: a third easy way 
# Note: This way will also fail if it encounters a removable singularity.
dp = p.deriv() 
u3 = dp(t)/abs(dp(t))

### Method 4: the removable-singularity-proof numpy.poly1d way  
# Note: This is roughly how Method 1 works
from svgpathtools import real, imag, rational_limit
dx, dy = real(dp), imag(dp)  # dp == dx + 1j*dy 
p_mag2 = dx**2 + dy**2  # p_mag2(t) = |p(t)|**2
# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,
#  the limit_{t->t0}[f(t) / abs(f(t))] == 
# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])
from cmath import sqrt
u4 = sqrt(rational_limit(dp**2, p_mag2, t))

print("unit tangent check:", u1 == u2 == u3 == u4)

# Let's do a visual check
mag = b.length()/4  # so it's not hard to see the tangent line
tangent_line = Line(b.point(t), b.point(t) + mag*u1)
disvg([b, tangent_line], 'bg', nodes=[b.point(t)])
unit tangent check: True

平移(移位)、反转方向和法线向量

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

矢量帧.svg

旋转和平移

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

装饰椭圆.svg

弧长和反弧长

在这里,我们将创建一个 SVG 来展示路径的参数和几何中点test.svg。我们需要计算使用Path.length()Line.length()QuadraticBezier.length()CubicBezier.length()Arc.length()方法,以及相关的反弧长方法.ilength()函数来执行此操作。

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

输出2.svg

贝塞尔曲线之间的交点

# Let's find all intersections between redpath and the other 
redpath = paths[0]
redpath_attribs = attributes[0]
intersections = []
for path in paths[1:]:
    for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):
        intersections.append(redpath.point(T1))
        
disvg(paths, filename='output_intersections.svg', attributes=attributes,
      nodes = intersections, node_radii = [5]*len(intersections))

output_intersections.svg

高级应用:偏移路径

在这里,我们将找到一些路径的偏移曲线

from  svgpathtools  import  parse_path ,  Line ,  Path ,  wsvg 
def  offset_curve ( path ,  offset_distance ,  steps = 1000 ): 
    """接受一个 Path 对象,`path`,和一个距离,
    `offset_distance`,并输出一个分段线性近似
    '平行'偏移曲线。""" 
    nls  =  [] 
    for  seg  in  path : 
        ct  =  1 
        for  k  in  range ( steps ): 
            t =  k  / 
            offset_vector  =  offset_distance  *  seg 正常( t ) 
            nl  = 线( seg.point ( t ) , seg.point ( t ) + offset_vector ) nls . _ _ _ 追加( nl ) connect_the_dots = [( nls [ k ] . end , nls [ k   
            
       + 1 ] end )  for  k  in  range ( len ( nls ) - 1 )] 
    if  path . 封闭():
        connect_the_dots 附加nls [ - 1 ] .end nls [ 0 ] .end offset_path = Path * connect_the_dots 返回offset_path _ 
      
     

# 示例:
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" ) 已翻译( 300 ) 
path3  =  parse_path ( "m 117,695 c 237,-7 -103,-146 457,0" ) 翻译后的( 500 + 400 j )
路径 =  [ path1 ,  path2 ,