SVG路径对象和贝塞尔曲线操作和分析的工具集合。
项目描述
svgpathtools-light
svgpathtools-light是一个用于操作和分析SVG路径对象和贝塞尔曲线的工具集合。这是svgpathtools包的轻量级版本,不需要scipy或svgwrite。此包不能与svgpathtools一起使用。
功能
svgpathtools包含设计用来轻松读取、写入和显示SVG文件的函数,以及大量的几何工具,用于转换和分析路径元素。
此外,子模块bezier.py包含用于处理存储为n-元组的通用第n阶贝塞尔曲线的工具。
一些包含的工具
- 读取、写入和显示包含路径(和其他)SVG元素的SVG文件
- 将贝塞尔路径段转换为numpy.poly1d(多项式)对象
- 将多项式(标准形式)转换为它们的贝塞尔形式
- 计算切线向量以及(右手定则)法线向量
- 计算曲率
- 将不连续路径分解为其连续子路径。
- 高效计算路径和/或线段之间的交点
- 找到一个路径或线段的边界框
- 反转线段/路径方向
- 裁剪和分割路径和线段
- 平滑路径(即平滑出锋利点以使路径可微分)
- 从路径域到线段域及其反向的转换映射(T2t和t2T)
- 计算封闭路径所围成的面积
- 计算弧长
- 计算反弧长
- 将RGB颜色元组转换为十六进制颜色字符串及其反向转换
先决条件
- numpy
- svgwrite
- scipy(可选,但为了性能推荐使用)
设置
$ pip install svgpathtools-light
替代设置
您可以从Github下载源代码,并在包含setup.py的文件夹内使用以下命令进行安装
$ python setup.py install
致谢
此模块的大部分核心代码是从svg.path(v2.0)模块中获得的。感兴趣的svg.path用户应查看此readme最底部的兼容性说明。
基本用法
类
svgpathtools模块主要围绕四个路径线段类构建:Line
、QuadraticBezier
、CubicBezier
和Arc
。还有一个第五个类,Path
,其对象是(连接或断开1)路径线段对象的序列。
-
Line(start, end)
-
Arc(start, radius, rotation, large_arc, sweep, end)
注意:有关这些参数的详细解释,请参阅文档字符串 -
QuadraticBezier(start, control, end)
-
CubicBezier(start, control1, control2, end)
-
Path(*segments)
有关每个参数含义的更多信息,请参阅path.py中的相关文档字符串或官方SVG规范。
1 警告:此库中的一些功能尚未在断开路径对象上进行测试。然而,通过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
类是一个可变序列,因此其行为与列表相似。因此,可以append
、insert
、按索引设置、del
、enumerate
、slice
等。
# 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.
读取SVG文件
svg2paths()函数将svg文件转换为路径对象列表以及包含每个路径属性的单独字典列表。
注意:Line、Polyline、Polygon和Path SVG元素都可以使用此函数转换为路径对象。
# 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
写入SVG文件(以及一些几何函数和方法)
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')
下面将有更多关于写入和显示路径数据的示例。
.point()方法和路径与路径线段参数化之间的转换
SVG路径元素及其线段有官方的参数化。可以使用Path.point()
、Line.point()
、QuadraticBezier.point()
、CubicBezier.point()
和Arc.point()
方法访问这些参数化。所有这些参数化都是在0 <= t <= 1的域上定义的。
注意:在本文档以及内联文档和doctrings中,我在提到Path对象的参数化时使用大写T
,在谈论路径段对象(即Line、QuadraticBezier、CubicBezier和Arc对象)时使用小写t
。
给定一个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$ 分别是控制点 start
、control1
、control2
和 end
,svgpathtools使用这些点来定义CubicBezier对象。使用CubicBezier.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)
在贝塞尔对象与NumPy多项式对象之间进行转换的能力非常有用。首先,我们可以将一个贝塞尔段的列表转换为NumPy数组
对贝塞尔路径段进行NumPy数组操作
为了进一步说明将我们的贝塞尔曲线对象转换为numpy.poly1d对象并再次转换的能力,让我们以四种不同的方式计算上述CubicBezier对象b在t=0.5处的单位切线向量。
切线向量(以及关于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])
旋转和平移
# 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,显示从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')
贝塞尔曲线之间的交集
# 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))
高级应用:偏移路径
在这里,我们将找到几个路径的偏移曲线。
from svgpathtools import parse_path, Line, Path, wsvg
def offset_curve(path, offset_distance, steps=1000):
"""Takes in a Path object, `path`, and a distance,
`offset_distance`, and outputs an piecewise-linear approximation
of the 'parallel' offset curve."""
nls = []
for seg in path:
ct = 1
for k in range(steps):
t = k / steps
offset_vector = offset_distance * seg.normal(t)
nl = Line(seg.point(t), seg.point(t) + offset_vector)
nls.append(nl)
connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)]
if path.isclosed():
connect_the_dots.append(Line(nls[-1].end, nls[0].end))
offset_path = Path(*connect_the_dots)
return offset_path
# Examples:
path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ")
path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300)
path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j)
paths = [path1, path2, path3]
offset_distances = [10*k for k in range(1,51)]
offset_paths = []
for path in paths:
for distances in offset_distances:
offset_paths.append(offset_curve(path, distances))
# Let's take a look
wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg')
svg.path(v2.0)用户的兼容性说明
-
将Arc.arc属性重命名为Arc.large_arc
-
Path.d():为了与svg.path(v2.0)的行为类似2,将useSandT和use_closed_attrib都设置为True。
2 此行为将相同,但此方法中使用的字符串格式已更改为使用默认格式(而不是通用格式{:G}),以提高精度。 ↩
许可证
此模块受MIT许可证的约束。
项目详细信息
下载文件
下载适用于您平台上的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。
源代码分发
构建分发
svgpathtools-light-1.6.2.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 534a42a6119e68a86592689933037aec7b0861e4d445d06c0ee41bcc9d6e6a5e |
|
MD5 | b8f39bdbbeef66545ed4d0f32393f9a7 |
|
BLAKE2b-256 | 6d3fcafa66c7e231695cf1d54b1c9d74695bf9b878527a655f8059228065ba15 |
svgpathtools_light-1.6.2-py2.py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 49eaef1bc9839a51a4a3b1fa3a832c80da0f842e668607a8bb226eb59b8a7f12 |
|
MD5 | b53e11c41c60c2b3d11a2b658edd8827 |
|
BLAKE2b-256 | 69c522c54943c9995a2d72a463741174be76f3ad7982d869eb2e243dc4242c40 |