initial commit

pull/4/head 1.0.1
Andy 2016-07-05 21:51:11 -07:00
commit 9b3d6fe5e3
35 changed files with 15588 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.pyc
.*
/svgpathtools/nonunittests
!/.gitignore

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Andrew Allan Port
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
LICENSE2.txt Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013-2014 Lennart Regebro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

12
MANIFEST Normal file
View File

@ -0,0 +1,12 @@
# 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

753
README.ipynb Normal file
View File

@ -0,0 +1,753 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# svgpathtools\n",
"\n",
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n",
"\n",
"## Features\n",
"\n",
"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**.\n",
"\n",
"Additionally, the submodule *bezier.py* contains tools for for working with general **nth order Bezier curves stored as n-tuples**.\n",
"\n",
"Some included tools:\n",
"\n",
"- **read**, **write**, and **display** SVG files containing Path (and other) SVG elements\n",
"- convert Bézier path segments to **numpy.poly1d** (polynomial) objects\n",
"- convert polynomials (in standard form) to their Bézier form\n",
"- compute **tangent vectors** and (right-hand rule) **normal vectors**\n",
"- compute **curvature**\n",
"- break discontinuous paths into their **continuous subpaths**.\n",
"- efficiently compute **intersections** between paths and/or segments\n",
"- find a **bounding box** for a path or segment\n",
"- **reverse** segment/path orientation\n",
"- **crop** and **split** paths and segments\n",
"- **smooth** paths (i.e. smooth away kinks to make paths differentiable)\n",
"- **transition maps** from path domain to segment domain and back (T2t and t2T)\n",
"- compute **area** enclosed by a closed path\n",
"- compute **arc length**\n",
"- compute **inverse arc length**\n",
"- convert RGB color tuples to hexadecimal color strings and back\n",
"\n",
"## Prerequisites\n",
"- **numpy**\n",
"- **svgwrite**\n",
"\n",
"## Setup\n",
"\n",
"If not already installed, you can **install the prerequisites** using pip.\n",
"\n",
"```bash\n",
"$ pip install numpy\n",
"```\n",
"\n",
"```bash\n",
"$ pip install svgwrite\n",
"```\n",
"\n",
"Then **install svgpathtools**:\n",
"```bash\n",
"$ pip install svgpathtools\n",
"``` \n",
" \n",
"### Alternative Setup \n",
"You can download the source from Github and install by using the command (from inside the folder containing setup.py):\n",
"\n",
"```bash\n",
"$ python setup.py install\n",
"```\n",
"\n",
"## Credit where credit's due\n",
"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.\n",
"\n",
"## Basic Usage\n",
"\n",
"### Classes\n",
"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<sup id=\"a1\">[1](#f1)</sup>) path segment objects.\n",
"\n",
"* ``Line(start, end)``\n",
"\n",
"* ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See docstring for a detailed explanation of these parameters\n",
"\n",
"* ``QuadraticBezier(start, control, end)``\n",
"\n",
"* ``CubicBezier(start, control1, control2, end)``\n",
"\n",
"* ``Path(*segments)``\n",
"\n",
"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.\n",
"\n",
"<u id=\"f1\">1</u> 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)"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from __future__ import division, print_function"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)))\n",
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)))\n",
"True\n",
"M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0\n",
"True\n"
]
}
],
"source": [
"# Coordinates are given as points in the complex plane\n",
"from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc\n",
"seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300)\n",
"seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350)\n",
"path = Path(seg1, seg2) # A path traversing the cubic and then the line\n",
"\n",
"# We could alternatively created this Path object using a d-string\n",
"from svgpathtools import parse_path\n",
"path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')\n",
"\n",
"# Let's check that these two methods are equivalent\n",
"print(path)\n",
"print(path_alt)\n",
"print(path == path_alt)\n",
"\n",
"# On a related note, the Path.d() method returns a Path object's d-string\n",
"print(path.d())\n",
"print(parse_path(path.d()) == path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``Path`` class is a mutable sequence, so it behaves much like a list.\n",
"So segments can **append**ed, **insert**ed, set by index, **del**eted, **enumerate**d, **slice**d out, etc."
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)),\n",
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))\n",
"Path(Line(start=(200+100j), end=(200+300j)),\n",
" Line(start=(200+300j), end=(250+350j)),\n",
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))\n",
"path is continuous? True\n",
"path is closed? True\n",
"path contains non-differentiable points? True\n",
"spath contains non-differentiable points? False\n",
"Path(Line(start=(200+101.5j), end=(200+298.5j)),\n",
" CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),\n",
" Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),\n",
" CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),\n",
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),\n",
" CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))\n",
"Notice that path contains 3 segments and spath contains 6 segments.\n"
]
}
],
"source": [
"# Let's append another to the end of it\n",
"path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))\n",
"print(path)\n",
"\n",
"# Let's replace the first segment with a Line object\n",
"path[0] = Line(200+100j, 200+300j)\n",
"print(path)\n",
"\n",
"# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)\n",
"print(\"path is continuous? \", path.iscontinuous())\n",
"print(\"path is closed? \", path.isclosed())\n",
"\n",
"# The curve the path follows is not, however, smooth (differentiable)\n",
"from svgpathtools import kinks, smoothed_path\n",
"print(\"path contains non-differentiable points? \", len(kinks(path)) > 0)\n",
"\n",
"# If we want, we can smooth these out (Experimental and only for line/cubic paths)\n",
"# Note: smoothing will always works (except on 180 degree turns), but you may want \n",
"# to play with the maxjointsize and tightness parameters to get pleasing results\n",
"# Note also: smoothing will increase the number of segments in a path\n",
"spath = smoothed_path(path)\n",
"print(\"spath contains non-differentiable points? \", len(kinks(spath)) > 0)\n",
"print(spath)\n",
"\n",
"# Let's take a quick look at the path and its smoothed relative\n",
"# The following commands will open two browser windows to display path and spaths\n",
"from svgpathtools import disvg\n",
"from time import sleep\n",
"disvg(path) \n",
"sleep(1) # needed when not giving the SVGs unique names (or not using timestamp)\n",
"disvg(spath)\n",
"print(\"Notice that path contains {} segments and spath contains {} segments.\"\n",
" \"\".format(len(path), len(spath)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Reading SVGSs\n",
"\n",
"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. \n",
"Note: Line, Polyline, Polygon, and Path SVG elements can all be converted to Path objects using this function."
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),\n",
" CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))\n",
"red\n"
]
}
],
"source": [
"# Read SVG into a list of path objects and list of dictionaries of attributes \n",
"from svgpathtools import svg2paths, wsvg\n",
"paths, attributes = svg2paths('test.svg')\n",
"\n",
"# Let's print out the first path object and the color it was in the SVG\n",
"# We'll see it is composed of two CubicBezier objects and, in the SVG file it \n",
"# came from, it was red\n",
"redpath = paths[0]\n",
"redpath_attribs = attributes[0]\n",
"print(redpath)\n",
"print(redpath_attribs['stroke'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Writing SVGSs (and some geometric functions and methods)\n",
"\n",
"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.\n",
"Note: Use the convenience function **disvg()** (or set 'openinbrowser=True') to automatically attempt to open the created svg file in your default SVG viewer."
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Let's make a new SVG that's identical to the first\n",
"wsvg(paths, attributes=attributes, filename='output1.svg')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![output1.svg](output1.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There will be many more examples of writing and displaying path data below.\n",
"\n",
"### The .point() method and transitioning between path and path segment parameterizations\n",
"SVG Path elements and their segments have official parameterizations.\n",
"These parameterizations can be accessed using the ``Path.point()``, ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, and ``Arc.point()`` methods.\n",
"All these parameterizations are defined over the domain 0 <= t <= 1.\n",
"\n",
"**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). \n",
"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)``. \n",
"There is also a ``Path.t2T()`` method to solve the inverse problem."
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"True\n",
"False\n",
"True\n"
]
}
],
"source": [
"# Example:\n",
"\n",
"# Let's check that the first segment of redpath starts \n",
"# at the same point as redpath\n",
"firstseg = redpath[0] \n",
"print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)\n",
"\n",
"# Let's check that the last segment of redpath ends on the same point as redpath\n",
"lastseg = redpath[-1] \n",
"print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)\n",
"\n",
"# This next boolean should return False as redpath is composed multiple segments\n",
"print(redpath.point(0.5) == firstseg.point(0.5))\n",
"\n",
"# If we want to figure out which segment of redpoint the \n",
"# point redpath.point(0.5) lands on, we can use the path.T2t() method\n",
"k, t = redpath.T2t(0.5)\n",
"print(redpath[k].point(t) == redpath.point(0.5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Tangent vectors and Bezier curves as numpy polynomial objects\n",
"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. \n",
"There's also a ``polynomial2bezier()`` function in the pathtools.py submodule to convert polynomials back to Bezier curves. \n",
"\n",
"**Note:** cubic Bezier curves are parameterized as $$\\mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3$$\n",
"where $P_0$, $P_1$, $P_2$, and $P_3$ are the control points ``start``, ``control1``, ``control2``, and ``end``, respectively, that svgpathtools uses to define a CubicBezier object. The ``CubicBezier.poly()`` method expands this polynomial to its standard form \n",
"$$\\mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3$$\n",
"where\n",
"$$\\begin{bmatrix}c_0\\\\c_1\\\\c_2\\\\c_3\\end{bmatrix} = \n",
"\\begin{bmatrix}\n",
"-1 & 3 & -3 & 1\\\\\n",
"3 & -6 & -3 & 0\\\\\n",
"-3 & 3 & 0 & 0\\\\\n",
"1 & 0 & 0 & 0\\\\\n",
"\\end{bmatrix}\n",
"\\begin{bmatrix}P_0\\\\P_1\\\\P_2\\\\P_3\\end{bmatrix}$$ \n",
"\n",
"QuadraticBezier.poly() and Line.poly() are defined similarly."
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"The CubicBezier, b.point(x) = \n",
"\n",
"(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3\n",
"\n",
"can be rewritten in standard form as \n",
"\n",
" 3 2\n",
"(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)\n"
]
}
],
"source": [
"# Example:\n",
"b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)\n",
"p = b.poly()\n",
"\n",
"# p(t) == b.point(t)\n",
"print(p(0.235) == b.point(0.235))\n",
"\n",
"# What is p(t)? It's just the cubic b written in standard form. \n",
"bpretty = \"{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3\".format(*b.bpoints())\n",
"print(\"The CubicBezier, b.point(x) = \\n\\n\" + \n",
" bpretty + \"\\n\\n\" + \n",
" \"can be rewritten in standard form as \\n\\n\" +\n",
" str(p).replace('x','t'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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.\n",
"\n",
"### Tangent vectors (and more on polynomials)"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"unit tangent check: True\n"
]
}
],
"source": [
"t = 0.5\n",
"### Method 1: the easy way\n",
"u1 = b.unit_tangent(t)\n",
"\n",
"### Method 2: another easy way \n",
"# Note: This way will fail if it encounters a removable singularity.\n",
"u2 = b.derivative(t)/abs(b.derivative(t))\n",
"\n",
"### Method 2: a third easy way \n",
"# Note: This way will also fail if it encounters a removable singularity.\n",
"dp = p.deriv() \n",
"u3 = dp(t)/abs(dp(t))\n",
"\n",
"### Method 4: the removable-singularity-proof numpy.poly1d way \n",
"# Note: This is roughly how Method 1 works\n",
"from svgpathtools import real, imag, rational_limit\n",
"dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy \n",
"p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2\n",
"# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,\n",
"# the limit_{t->t0}[f(t) / abs(f(t))] == \n",
"# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])\n",
"from cmath import sqrt\n",
"u4 = sqrt(rational_limit(dp**2, p_mag2, t))\n",
"\n",
"print(\"unit tangent check:\", u1 == u2 == u3 == u4)\n",
"\n",
"# Let's do a visual check\n",
"mag = b.length()/4 # so it's not hard to see the tangent line\n",
"tangent_line = Line(b.point(t), b.point(t) + mag*u1)\n",
"disvg([b, tangent_line], 'bg', nodes=[b.point(t)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Translations (shifts), reversing orientation, and normal vectors"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Speaking of tangents, let's add a normal vector to the picture\n",
"n = b.normal(t)\n",
"normal_line = Line(b.point(t), b.point(t) + mag*n)\n",
"disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)])\n",
"\n",
"# and let's reverse the orientation of b! \n",
"# the tangent and normal lines should be sent to their opposites\n",
"br = b.reversed()\n",
"\n",
"# Let's also shift b_r over a bit to the right so we can view it next to b\n",
"# The simplest way to do this is br = br.translated(3*mag), but let's use \n",
"# the .bpoints() instead, which returns a Bezier's control points\n",
"br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # \n",
"\n",
"tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t))\n",
"normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t))\n",
"wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], \n",
" 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', \n",
" text=[\"b's tangent\", \"br's tangent\"], text_path=[tangent_line, tangent_line_r])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![vectorframes.svg](vectorframes.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Rotations and Translations"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Let's take a Line and an Arc and make some pictures\n",
"top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1)\n",
"midline = Line(-1.5, 1.5)\n",
"\n",
"# First let's make our ellipse whole\n",
"bottom_half = top_half.rotated(180)\n",
"decorated_ellipse = Path(top_half, bottom_half)\n",
"\n",
"# Now let's add the decorations\n",
"for k in range(12):\n",
" decorated_ellipse.append(midline.rotated(30*k))\n",
" \n",
"# Let's move it over so we can see the original Line and Arc object next\n",
"# to the final product\n",
"decorated_ellipse = decorated_ellipse.translated(4+0j)\n",
"wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![decorated_ellipse.svg](decorated_ellipse.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### arc length and inverse arc length\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# First we'll load the path data from the file test.svg\n",
"paths, attributes = svg2paths('test.svg')\n",
"\n",
"# Let's mark the parametric midpoint of each segment\n",
"# I say \"parametric\" midpoint because Bezier curves aren't \n",
"# parameterized by arclength \n",
"# If they're also the geometric midpoint, let's mark them\n",
"# purple and otherwise we'll mark the geometric midpoint green\n",
"min_depth = 5\n",
"error = 1e-4\n",
"dots = []\n",
"ncols = []\n",
"nradii = []\n",
"for path in paths:\n",
" for seg in path:\n",
" parametric_mid = seg.point(0.5)\n",
" seg_length = seg.length()\n",
" if seg.length(0.5)/seg.length() == 1/2:\n",
" dots += [parametric_mid]\n",
" ncols += ['purple']\n",
" nradii += [5]\n",
" else:\n",
" t_mid = seg.ilength(seg_length/2)\n",
" geo_mid = seg.point(t_mid)\n",
" dots += [parametric_mid, geo_mid]\n",
" ncols += ['red', 'green']\n",
" nradii += [5] * 2\n",
"\n",
"# In 'output2.svg' the paths will retain their original attributes\n",
"wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, \n",
" attributes=attributes, filename='output2.svg')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![output2.svg](output2.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Intersections between Bezier curves"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Let's find all intersections between redpath and the other \n",
"redpath = paths[0]\n",
"redpath_attribs = attributes[0]\n",
"intersections = []\n",
"for path in paths[1:]:\n",
" for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):\n",
" intersections.append(redpath.point(T1))\n",
" \n",
"disvg(paths, filename='output_intersections.svg', attributes=attributes,\n",
" nodes = intersections, node_radii = [5]*len(intersections))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![output_intersections.svg](output_intersections.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### An Advanced Application: Offsetting Bezier Curves\n",
"Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) for a few Bezier cubics."
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"def display_offset_curve(bezpath, offset_distance, steps=1000,\n",
" stripe_width=3, stripe_colors='ygb',\n",
" name='offsetcurves.svg', show_normal_line=True):\n",
" \"\"\"Takes in a path of bezier curves, `bezpath`, and a distance,\n",
" `offset_distance`, and displays the 'parallel' offset curve by placing a\n",
" dot at a distance of `offset_distance` away at each step.\"\"\"\n",
" nls = []\n",
" nodes = []\n",
" line_colors = ''\n",
" node_colors = ''\n",
"\n",
" for bez in bezpath:\n",
" ct = 1\n",
" for k in range(steps):\n",
" t = k / steps\n",
" nl = Line(bez.point(t),\n",
" bez.point(t) + offset_distance * bez.normal(t))\n",
" nls.append(nl)\n",
" line_colors += stripe_colors[ct % 3]\n",
" if not (k % stripe_width):\n",
" ct += 1\n",
" nodes.append(bez.point(t))\n",
" nodes.append(nl.end)\n",
" node_colors += 'kr'\n",
" # nls.reverse()\n",
" if show_normal_line:\n",
" wsvg([bezpath] + nls, 'k' + line_colors,\n",
" nodes=nodes, node_colors=node_colors,\n",
" filename=name)\n",
" else:\n",
" wsvg(bezpath, 'k',\n",
" nodes=nodes, node_colors=node_colors,\n",
" filename=name)\n",
"\n",
"bez1 = parse_path(\"m 288,600 c -52,-28 -42,-61 0,-97 \")[0]\n",
"bez2 = parse_path(\"M 151,395 C 407,485 726.17662,160 634,339\")[0]\n",
"bez3 = parse_path(\"m 117,695 c 237,-7 -103,-146 457,0\")[0]\n",
"path = Path(bez1, bez2.translated(300), bez3.translated(500+400j))\n",
"\n",
"display_offset_curve(path, 500)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![offsetcurves.svg](offsetcurves.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Compatibility Notes for users of svg.path (v2.0)\n",
"\n",
"- renamed Arc.arc attribute as Arc.large_arc\n",
"\n",
"- Path.d() : For behavior similar<sup id=\"a2\">[2](#f2)</sup> to svg.path (v2.0), set both useSandT and use_closed_attrib to be True.\n",
"\n",
"<u id=\"f2\">2</u> 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)\n",
"\n",
"## To Do\n",
"\n",
"- Implement Arc X Arc intersections.\n",
"- Implement Arc.radialrange() method.\n",
"\n",
"Licence\n",
"-------\n",
"\n",
"This module is under a MIT License."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.11"
}
},
"nbformat": 4,
"nbformat_minor": 0
}

634
README.rst Normal file
View File

@ -0,0 +1,634 @@
svgpathtools
============
svgpathtools is a collection of tools for manipulating and analyzing SVG
Path objects and Bézier curves.
Features
--------
svgpathtools contains functions designed to **easily read, write and
display SVG files** as well as *a large selection of
geometrically-oriented tools* to **transform and analyze path
elements**.
Additionally, the submodule *bezier.py* contains tools for for working
with general **nth order Bezier curves stored as n-tuples**.
Some included tools:
- **read**, **write**, and **display** SVG files containing Path (and
other) SVG elements
- convert Bézier path segments to **numpy.poly1d** (polynomial) objects
- convert polynomials (in standard form) to their Bézier form
- compute **tangent vectors** and (right-hand rule) **normal vectors**
- compute **curvature**
- break discontinuous paths into their **continuous subpaths**.
- efficiently compute **intersections** between paths and/or segments
- find a **bounding box** for a path or segment
- **reverse** segment/path orientation
- **crop** and **split** paths and segments
- **smooth** paths (i.e. smooth away kinks to make paths
differentiable)
- **transition maps** from path domain to segment domain and back (T2t
and t2T)
- compute **area** enclosed by a closed path
- compute **arc length**
- compute **inverse arc length**
- convert RGB color tuples to hexadecimal color strings and back
Prerequisites
-------------
- **numpy**
- **svgwrite**
Setup
-----
If not already installed, you can **install the prerequisites** using
pip.
.. code:: bash
$ pip install numpy
.. code:: bash
$ pip install svgwrite
Then **install svgpathtools**:
.. code:: bash
$ pip install svgpathtools
Alternative Setup
~~~~~~~~~~~~~~~~~
You can download the source from Github and install by using the command
(from inside the folder containing setup.py):
.. code:: bash
$ python setup.py install
Credit where credit's due
-------------------------
Much of the core of this module was taken from `the svg.path (v2.0)
module <https://github.com/regebro/svg.path>`__. Interested svg.path
users should see the compatibility notes at bottom of this readme.
Basic Usage
-----------
Classes
~~~~~~~
The svgpathtools module is primarily structured around four path segment
classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``.
There is also a fifth class, ``Path``, whose objects are sequences of
(connected or disconnected\ `1 <#f1>`__\ ) path segment objects.
- ``Line(start, end)``
- ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See
docstring for a detailed explanation of these parameters
- ``QuadraticBezier(start, control, end)``
- ``CubicBezier(start, control1, control2, end)``
- ``Path(*segments)``
See the relevant docstrings in *path.py* or the `official SVG
specifications <http://www.w3.org/TR/SVG/paths.html>`__ for more
information on what each parameter means.
1 Warning: Some of the functionality in this library has not been tested
on discontinuous Path objects. A simple workaround is provided, however,
by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__
.. code:: 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')
# 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, filename='output1.svg')
.. figure:: 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:: 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:: 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:: 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:: output_intersections.svg
:alt: output\_intersections.svg
output\_intersections.svg
An Advanced Application: Offsetting Bezier Curves
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here we'll find the `offset
curve <https://en.wikipedia.org/wiki/Parallel_curve>`__ for a few Bezier
cubics.
.. code:: python
def display_offset_curve(bezpath, offset_distance, steps=1000,
stripe_width=3, stripe_colors='ygb',
name='offsetcurves.svg', show_normal_line=True):
"""Takes in a path of bezier curves, `bezpath`, and a distance,
`offset_distance`, and displays the 'parallel' offset curve by placing a
dot at a distance of `offset_distance` away at each step."""
nls = []
nodes = []
line_colors = ''
node_colors = ''
for bez in bezpath:
ct = 1
for k in range(steps):
t = k / steps
nl = Line(bez.point(t),
bez.point(t) + offset_distance * bez.normal(t))
nls.append(nl)
line_colors += stripe_colors[ct % 3]
if not (k % stripe_width):
ct += 1
nodes.append(bez.point(t))
nodes.append(nl.end)
node_colors += 'kr'
# nls.reverse()
if show_normal_line:
wsvg([bezpath] + nls, 'k' + line_colors,
nodes=nodes, node_colors=node_colors,
filename=name)
else:
wsvg(bezpath, 'k',
nodes=nodes, node_colors=node_colors,
filename=name)
bez1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ")[0]
bez2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339")[0]
bez3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0")[0]
path = Path(bez1, bez2.translated(300), bez3.translated(500+400j))
display_offset_curve(path, 500)
.. figure:: offsetcurves.svg
:alt: offsetcurves.svg
offsetcurves.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>`__
To Do
-----
- Implement Arc X Arc intersections.
- Implement Arc.radialrange() method.
Licence
-------
This module is under a MIT License.

7
decorated_ellipse.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="344px" version="1.1" viewBox="-2.2035 -2.4035 8.407 4.807" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<path d="M -1,0 A 1.0,2.0 0 1,1 1,0" fill="none" stroke="#000000" stroke-width="0.007"/>
<path d="M -1.5,0.0 L 1.5,0.0" fill="none" stroke="#000000" stroke-width="0.007"/>
<path d="M 3.0,0.0 A 1.0,2.0 0 1,1 5.0,0.0 M 5.0,-1.22464679915e-16 A 1.0,2.0 180 1,1 3.0,1.22464679915e-16 M 2.5,0.0 L 5.5,0.0 M 2.70096189432,-0.75 L 5.29903810568,0.75 M 3.25,-1.29903810568 L 4.75,1.29903810568 M 4.0,-1.5 L 4.0,1.5 M 4.75,-1.29903810568 L 3.25,1.29903810568 M 5.29903810568,-0.75 L 2.70096189432,0.75 M 5.5,-1.83697019872e-16 L 2.5,1.83697019872e-16 M 5.29903810568,0.75 L 2.70096189432,-0.75 M 4.75,1.29903810568 L 3.25,-1.29903810568 M 4.0,1.5 L 4.0,-1.5 M 3.25,1.29903810568 L 4.75,-1.29903810568 M 2.70096189432,0.75 L 5.29903810568,-0.75" fill="none" stroke="#000000" stroke-width="0.007"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
dist/svgpathtools-1.0.tar.gz vendored Normal file

Binary file not shown.

8
disvg_output.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" version="1.1" viewBox="161.5 79.0 152.0 242.0" width="377px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<path d="M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0" fill="none" stroke="blue" stroke-width="0.2"/>
<path d="M 175.0,162.5 L 175.0,236.805280985" fill="none" stroke="green" stroke-width="0.2"/>
<path d="M 175.0,162.5 L 249.305280985,162.5" fill="none" stroke="pink" stroke-width="0.2"/>
<circle cx="175.0" cy="162.5" fill="#ff0000" r="1.0"/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

9005
offsetcurves.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 835 KiB

8
output1.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" version="1.1" viewBox="-20.6525 -20.6525 366.305 366.305" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<path d="M 10.5,80.0 C 40.0,10.0 65.0,10.0 95.0,80.0 C 125.0,150.0 150.0,150.0 180.0,80.0" fill="none" stroke="red"/>
<path d="M 84.0,92.0 C 94.0,102.0 114.0,102.0 124.0,92.0" fill="none" stroke="purple"/>
<path d="M 40.0,100.0 Q 100.0,50.0 65.0,20.0" fill="none" stroke="blue"/>
<path d="M 10.0,315.0 L 110.0,215.0 A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45 L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9 L 315.0,10.0" fill="green" fill-opacity="0.5" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

21
output2.svg Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" version="1.1" viewBox="-25.5 -25.5 376.0 376.0" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<path d="M 10.5,80.0 C 40.0,10.0 65.0,10.0 95.0,80.0 C 125.0,150.0 150.0,150.0 180.0,80.0" fill="none" stroke="red"/>
<path d="M 84.0,92.0 C 94.0,102.0 114.0,102.0 124.0,92.0" fill="none" stroke="purple"/>
<path d="M 40.0,100.0 Q 100.0,50.0 65.0,20.0" fill="none" stroke="blue"/>
<path d="M 10.0,315.0 L 110.0,215.0 A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45 L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9 L 315.0,10.0" fill="green" fill-opacity="0.5" stroke="black" stroke-width="2"/>
<circle cx="52.5625" cy="27.5" fill="red" r="5"/>
<circle cx="52.6653942856" cy="27.5003296408" fill="green" r="5"/>
<circle cx="137.5" cy="132.5" fill="purple" r="5"/>
<circle cx="104.0" cy="99.5" fill="purple" r="5"/>
<circle cx="76.25" cy="55.0" fill="red" r="5"/>
<circle cx="72.4661531169" cy="63.8283469353" fill="green" r="5"/>
<circle cx="60.0" cy="265.0" fill="purple" r="5"/>
<circle cx="120.5100001" cy="144.933334054" fill="red" r="5"/>
<circle cx="115.010503028" cy="151.954994361" fill="green" r="5"/>
<circle cx="167.55" cy="157.45" fill="purple" r="5"/>
<circle cx="159.42414315" cy="96.77414315" fill="red" r="5"/>
<circle cx="159.42414315" cy="96.77414315" fill="green" r="5"/>
<circle cx="265.05" cy="59.95" fill="purple" r="5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

12
output_intersections.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" version="1.1" viewBox="-25.5 -25.5 376.0 376.0" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<path d="M 10.5,80.0 C 40.0,10.0 65.0,10.0 95.0,80.0 C 125.0,150.0 150.0,150.0 180.0,80.0" fill="none" stroke="red"/>
<path d="M 84.0,92.0 C 94.0,102.0 114.0,102.0 124.0,92.0" fill="none" stroke="purple"/>
<path d="M 40.0,100.0 Q 100.0,50.0 65.0,20.0" fill="none" stroke="blue"/>
<path d="M 10.0,315.0 L 110.0,215.0 A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45 L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9 L 315.0,10.0" fill="green" fill-opacity="0.5" stroke="black" stroke-width="2"/>
<circle cx="104.172804482" cy="99.4995576437" fill="#ff0000" r="5"/>
<circle cx="77.8257146608" cy="46.8489740154" fill="#ff0000" r="5"/>
<circle cx="175.113071158" cy="90.8738283981" fill="#ff0000" r="5"/>
<circle cx="154.99853466" cy="123.15366017" fill="#ff0000" r="5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

5
path.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" version="1.1" viewBox="193.894237886 74.875 72.0191453624 300.25" width="144px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<path d="M 200.0,100.0 L 200.0,300.0 L 250.0,350.0 C 275.0,350.0 250.0,225.0 200.0,100.0" fill="none" stroke="#000000" stroke-width="0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

25
setup.py Normal file
View File

@ -0,0 +1,25 @@
from distutils.core import setup
VERSION = '1.0'
AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
setup(name='svgpathtools',
packages=['svgpathtools'],
version=VERSION,
description=('A collection of tools for manipulating and analyzing SVG '
'Path objects and Bezier curves.'),
long_description=open('README.rst').read(),
author=AUTHOR_NAME,
author_email=AUTHOR_EMAIL,
url='https://github.com/mathandy/svgpathtools',
download_url = 'http://github.com/mathandy/svgpathtools/tarball/1.0',
license='MIT',
# install_requires=['numpy', 'svgwrite'],
platforms="OS Independent",
# test_suite='tests',
requires=['numpy', 'svgwrite'],
keywords=['svg', 'bezier', 'svg.path'],
classifiers = [],
)

19
svgpathtools/__init__.py Normal file
View File

@ -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, rational_limit, real, imag
from .misctools import hex2rgb, rgb2hex
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
try:
from .svg2paths import svg2paths
except ImportError:
pass

374
svgpathtools/bezier.py Normal file
View File

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

64
svgpathtools/misctools.py Normal file
View File

@ -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.")

195
svgpathtools/parser.py Normal file
View File

@ -0,0 +1,195 @@
"""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
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

2130
svgpathtools/path.py Normal file

File diff suppressed because it is too large Load Diff

379
svgpathtools/paths2svg.py Normal file
View File

@ -0,0 +1,379 @@
"""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):
"""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 dictionary of attributes for the input paths.
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
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):
"""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)

14
svgpathtools/pathtools.py Normal file
View File

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

80
svgpathtools/polytools.py Normal file
View File

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

201
svgpathtools/smoothing.py Normal file
View File

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

87
svgpathtools/svg2paths.py Normal file
View File

@ -0,0 +1,87 @@
"""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
# 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):
"""
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)
:return: list of Path objects
"""
if os_path.dirname(svg_file_location) == '':
svg_file_location = os_path.join(getcwd(), svg_file_location)
doc = parse(svg_file_location) # parseString also exists
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
# 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
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list

View File

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="744.09448819"
height="1052.3622047"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="New document 1">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="397.2841"
inkscape:cy="523.28933"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 165.66502,474.55493 c 77.30655,-9.33703 147.47762,-100.73205 234.35539,0 58.00169,-86.91626 49.61109,-140.63637 0,-173.74624 l 94.95434,0 c 59.26228,20.20305 59.26229,51.18106 0,92.93403 25.42828,25.65512 50.19764,62.5117 80.8122,0 22.30135,41.03559 78.56919,92.26113 0,103.03556 l -72.73098,0 c 12.66417,43.22036 75.15432,97.51317 0,121.21831 -82.83986,8.81955 -137.45198,91.73688 -258.59905,0 -16.20448,-28.83144 -149.811143,44.10743 -109.09647,10.10152 l 24.24365,-101.01525 0,-52.52793 z"
id="path2985"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 206.07112,858.41289 206.07112,-2.02031 c -50.738,-81.14814 -20.36402,-105.87055 52.52793,-101.01525 l 103.03556,0 0,111.11678"
id="path2987"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,21 @@
from __future__ import division, absolute_import, print_function
# import unittest
# try:
# from ..bezier import *
# except ValueError:
# from svgpathtools.bezier import *
#
#
# class Test_bezier(unittest.TestCase):
# def test_bernstein(self):
# self.fail()
#
# def test_bezier_point(self):
# self.fail()
#
# def test_split_bezier(self):
# self.fail()
#
#
# if __name__ == '__main__':
# unittest.main()

View File

@ -0,0 +1,56 @@
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
#------------------------------------------------------------------------------
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import *
class TestGeneration(unittest.TestCase):
def test_svg_examples(self):
"""Examples from the SVG spec"""
paths = [
'M 100,100 L 300,100 L 200,300 Z',
'M 0,0 L 50,20 M 100,100 L 300,100 L 200,300 Z',
'M 100,100 L 200,200',
'M 100,200 L 200,100 L -100,-200',
'M 100,200 C 100,100 250,100 250,200 S 400,300 400,200',
'M 100,200 C 100,100 400,100 400,200',
'M 100,500 C 25,400 475,400 400,500',
'M 100,800 C 175,700 325,700 400,800',
'M 600,200 C 675,100 975,100 900,200',
'M 600,500 C 600,350 900,650 900,500',
'M 600,800 C 625,700 725,700 750,800 S 875,900 900,800',
'M 200,300 Q 400,50 600,300 T 1000,300',
'M -3.4E+38,3.4E+38 L -3.4E-38,3.4E-38',
'M 0,0 L 50,20 M 50,20 L 200,100 Z',
'M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275',
]
pathsf = [
'M 100.0,100.0 L 300.0,100.0 L 200.0,300.0 L 100.0,100.0',
'M 0.0,0.0 L 50.0,20.0 M 100.0,100.0 L 300.0,100.0 L 200.0,300.0 L 100.0,100.0',
'M 100.0,100.0 L 200.0,200.0',
'M 100.0,200.0 L 200.0,100.0 L -100.0,-200.0',
'M 100.0,200.0 C 100.0,100.0 250.0,100.0 250.0,200.0 C 250.0,300.0 400.0,300.0 400.0,200.0',
'M 100.0,200.0 C 100.0,100.0 400.0,100.0 400.0,200.0',
'M 100.0,500.0 C 25.0,400.0 475.0,400.0 400.0,500.0',
'M 100.0,800.0 C 175.0,700.0 325.0,700.0 400.0,800.0',
'M 600.0,200.0 C 675.0,100.0 975.0,100.0 900.0,200.0',
'M 600.0,500.0 C 600.0,350.0 900.0,650.0 900.0,500.0',
'M 600.0,800.0 C 625.0,700.0 725.0,700.0 750.0,800.0 C 775.0,900.0 875.0,900.0 900.0,800.0',
'M 200.0,300.0 Q 400.0,50.0 600.0,300.0 Q 800.0,550.0 1000.0,300.0',
'M -3.4e+38,3.4e+38 L -3.4e-38,3.4e-38',
'M 0.0,0.0 L 50.0,20.0 L 200.0,100.0 L 50.0,20.0',
'M 600.0,350.0 L 650.0,325.0 A 27.9508497187,27.9508497187 -30.0 0,1 700.0,300.0 L 750.0,275.0'
]
for k, path in enumerate(paths):
self.assertTrue(parse_path(path).d() in (path, pathsf[k]))
def test_normalizing(self):
# Relative paths will be made absolute, subpaths merged if they can,
# and syntax will change.
path = 'M0 0L3.4E2-10L100.0,100M100,100l100,-100'
ps = 'M 0,0 L 340,-10 L 100,100 L 200,0'
psf = 'M 0.0,0.0 L 340.0,-10.0 L 100.0,100.0 L 200.0,0.0'
self.assertTrue(parse_path(path).d() in (ps, psf))

View File

@ -0,0 +1,139 @@
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
#------------------------------------------------------------------------------
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import *
class TestParser(unittest.TestCase):
def test_svg_examples(self):
"""Examples from the SVG spec"""
path1 = parse_path('M 100 100 L 300 100 L 200 300 z')
self.assertEqual(path1, Path(Line(100 + 100j, 300 + 100j),
Line(300 + 100j, 200 + 300j),
Line(200 + 300j, 100 + 100j)))
self.assertTrue(path1.isclosed())
# for Z command behavior when there is multiple subpaths
path1 = parse_path('M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z')
self.assertEqual(path1, Path(
Line(0 + 0j, 50 + 20j),
Line(100 + 100j, 300 + 100j),
Line(300 + 100j, 200 + 300j),
Line(200 + 300j, 100 + 100j)))
path1 = parse_path('M 100 100 L 200 200')
path2 = parse_path('M100 100L200 200')
self.assertEqual(path1, path2)
path1 = parse_path('M 100 200 L 200 100 L -100 -200')
path2 = parse_path('M 100 200 L 200 100 -100 -200')
self.assertEqual(path1, path2)
path1 = parse_path("""M100,200 C100,100 250,100 250,200
S400,300 400,200""")
self.assertEqual(path1,
Path(CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j),
CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j)))
path1 = parse_path('M100,200 C100,100 400,100 400,200')
self.assertEqual(path1,
Path(CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j)))
path1 = parse_path('M100,500 C25,400 475,400 400,500')
self.assertEqual(path1,
Path(CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j)))
path1 = parse_path('M100,800 C175,700 325,700 400,800')
self.assertEqual(path1,
Path(CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j)))
path1 = parse_path('M600,200 C675,100 975,100 900,200')
self.assertEqual(path1,
Path(CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j)))
path1 = parse_path('M600,500 C600,350 900,650 900,500')
self.assertEqual(path1,
Path(CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)))
path1 = parse_path("""M600,800 C625,700 725,700 750,800
S875,900 900,800""")
self.assertEqual(path1,
Path(CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j),
CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j)))
path1 = parse_path('M200,300 Q400,50 600,300 T1000,300')
self.assertEqual(path1,
Path(QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j),
QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j)))
path1 = parse_path('M300,200 h-150 a150,150 0 1,0 150,-150 z')
self.assertEqual(path1,
Path(Line(300 + 200j, 150 + 200j),
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
Line(300 + 50j, 300 + 200j)))
path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 z')
self.assertEqual(path1,
Path(Line(275 + 175j, 275 + 25j),
Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j),
Line(125 + 175j, 275 + 175j)))
path1 = parse_path("""M600,350 l 50,-25
a25,25 -30 0,1 50,-25 l 50,-25
a25,50 -30 0,1 50,-25 l 50,-25
a25,75 -30 0,1 50,-25 l 50,-25
a25,100 -30 0,1 50,-25 l 50,-25""")
self.assertEqual(path1,
Path(Line(600 + 350j, 650 + 325j),
Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j),
Line(700 + 300j, 750 + 275j),
Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j),
Line(800 + 250j, 850 + 225j),
Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j),
Line(900 + 200j, 950 + 175j),
Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j),
Line(1000 + 150j, 1050 + 125j)))
def test_others(self):
# Other paths that need testing:
# Relative moveto:
path1 = parse_path('M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z')
self.assertEqual(path1, Path(
Line(0 + 0j, 50 + 20j),
Line(100 + 100j, 300 + 100j),
Line(300 + 100j, 200 + 300j),
Line(200 + 300j, 100 + 100j)))
# Initial smooth and relative CubicBezier
path1 = parse_path("""M100,200 s 150,-100 150,0""")
self.assertEqual(path1,
Path(CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j)))
# Initial smooth and relative QuadraticBezier
path1 = parse_path("""M100,200 t 150,0""")
self.assertEqual(path1,
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
# Relative QuadraticBezier
path1 = parse_path("""M100,200 q 0,0 150,0""")
self.assertEqual(path1,
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
def test_negative(self):
"""You don't need spaces before a minus-sign"""
path1 = parse_path('M100,200c10-5,20-10,30-20')
path2 = parse_path('M 100 200 c 10 -5 20 -10 30 -20')
self.assertEqual(path1, path2)
def test_numbers(self):
"""Exponents and other number format cases"""
# It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
path1 = parse_path('M-3.4e38 3.4E+38L-3.4E-38,3.4e-38')
path2 = Path(Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j))
self.assertEqual(path1, path2)
def test_errors(self):
self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200')

View File

@ -0,0 +1,906 @@
# External dependencies
from __future__ import division, absolute_import, print_function
import unittest
from math import sqrt, pi
# Internal dependencies
from svgpathtools import *
# A note left from the svg.path tools module:
# -------------------------------------------
# Most of these test points are not calculated separately, as that would
# take too long and be too error prone. Instead the curves have been verified
# to be correct visually with the disvg() function.
class LineTest(unittest.TestCase):
def test_lines(self):
# These points are calculated, and not just regression tests.
line1 = Line(0j, 400 + 0j)
self.assertAlmostEqual(line1.point(0), 0j)
self.assertAlmostEqual(line1.point(0.3), (120 + 0j))
self.assertAlmostEqual(line1.point(0.5), (200 + 0j))
self.assertAlmostEqual(line1.point(0.9), (360 + 0j))
self.assertAlmostEqual(line1.point(1), (400 + 0j))
self.assertAlmostEqual(line1.length(), 400)
line2 = Line(400 + 0j, 400 + 300j)
self.assertAlmostEqual(line2.point(0), (400 + 0j))
self.assertAlmostEqual(line2.point(0.3), (400 + 90j))
self.assertAlmostEqual(line2.point(0.5), (400 + 150j))
self.assertAlmostEqual(line2.point(0.9), (400 + 270j))
self.assertAlmostEqual(line2.point(1), (400 + 300j))
self.assertAlmostEqual(line2.length(), 300)
line3 = Line(400 + 300j, 0j)
self.assertAlmostEqual(line3.point(0), (400 + 300j))
self.assertAlmostEqual(line3.point(0.3), (280 + 210j))
self.assertAlmostEqual(line3.point(0.5), (200 + 150j))
self.assertAlmostEqual(line3.point(0.9), (40 + 30j))
self.assertAlmostEqual(line3.point(1), 0j)
self.assertAlmostEqual(line3.length(), 500)
def test_equality(self):
# This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual
line = Line(0j, 400 + 0j)
self.assertTrue(line == Line(0, 400))
self.assertTrue(line != Line(100, 400))
self.assertFalse(line == str(line))
self.assertTrue(line != str(line))
self.assertFalse(
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line)
class CubicBezierTest(unittest.TestCase):
def test_approx_circle(self):
"""This is a approximate circle drawn in Inkscape"""
cub1 = CubicBezier(
complex(0, 0),
complex(0, 109.66797),
complex(-88.90345, 198.57142),
complex(-198.57142, 198.57142)
)
self.assertAlmostEqual(cub1.point(0), 0j)
self.assertAlmostEqual(cub1.point(0.1), (-2.59896457 + 32.20931647j))
self.assertAlmostEqual(cub1.point(0.2), (-10.12330256 + 62.76392816j))
self.assertAlmostEqual(cub1.point(0.3), (-22.16418039 + 91.25500149j))
self.assertAlmostEqual(cub1.point(0.4), (-38.31276448 + 117.27370288j))
self.assertAlmostEqual(cub1.point(0.5), (-58.16022125 + 140.41119875j))
self.assertAlmostEqual(cub1.point(0.6), (-81.29771712 + 160.25865552j))
self.assertAlmostEqual(cub1.point(0.7), (-107.31641851 + 176.40723961j))
self.assertAlmostEqual(cub1.point(0.8), (-135.80749184 + 188.44811744j))
self.assertAlmostEqual(cub1.point(0.9), (-166.36210353 + 195.97245543j))
self.assertAlmostEqual(cub1.point(1), (-198.57142 + 198.57142j))
cub2 = CubicBezier(
complex(-198.57142, 198.57142),
complex(-109.66797 - 198.57142, 0 + 198.57142),
complex(-198.57143 - 198.57142, -88.90345 + 198.57142),
complex(-198.57143 - 198.57142, 0),
)
self.assertAlmostEqual(cub2.point(0), (-198.57142 + 198.57142j))
self.assertAlmostEqual(cub2.point(0.1), (-230.78073675 + 195.97245543j))
self.assertAlmostEqual(cub2.point(0.2), (-261.3353492 + 188.44811744j))
self.assertAlmostEqual(cub2.point(0.3), (-289.82642365 + 176.40723961j))
self.assertAlmostEqual(cub2.point(0.4), (-315.8451264 + 160.25865552j))
self.assertAlmostEqual(cub2.point(0.5), (-338.98262375 + 140.41119875j))
self.assertAlmostEqual(cub2.point(0.6), (-358.830082 + 117.27370288j))
self.assertAlmostEqual(cub2.point(0.7), (-374.97866745 + 91.25500149j))
self.assertAlmostEqual(cub2.point(0.8), (-387.0195464 + 62.76392816j))
self.assertAlmostEqual(cub2.point(0.9), (-394.54388515 + 32.20931647j))
self.assertAlmostEqual(cub2.point(1), (-397.14285 + 0j))
cub3 = CubicBezier(
complex(-198.57143 - 198.57142, 0),
complex(0 - 198.57143 - 198.57142, -109.66797),
complex(88.90346 - 198.57143 - 198.57142, -198.57143),
complex(-198.57142, -198.57143)
)
self.assertAlmostEqual(cub3.point(0), (-397.14285 + 0j))
self.assertAlmostEqual(cub3.point(0.1), (-394.54388515 - 32.20931675j))
self.assertAlmostEqual(cub3.point(0.2), (-387.0195464 - 62.7639292j))
self.assertAlmostEqual(cub3.point(0.3), (-374.97866745 - 91.25500365j))
self.assertAlmostEqual(cub3.point(0.4), (-358.830082 - 117.2737064j))
self.assertAlmostEqual(cub3.point(0.5), (-338.98262375 - 140.41120375j))
self.assertAlmostEqual(cub3.point(0.6), (-315.8451264 - 160.258662j))
self.assertAlmostEqual(cub3.point(0.7), (-289.82642365 - 176.40724745j))
self.assertAlmostEqual(cub3.point(0.8), (-261.3353492 - 188.4481264j))
self.assertAlmostEqual(cub3.point(0.9), (-230.78073675 - 195.97246515j))
self.assertAlmostEqual(cub3.point(1), (-198.57142 - 198.57143j))
cub4 = CubicBezier(
complex(-198.57142, -198.57143),
complex(109.66797 - 198.57142, 0 - 198.57143),
complex(0, 88.90346 - 198.57143),
complex(0, 0),
)
self.assertAlmostEqual(cub4.point(0), (-198.57142 - 198.57143j))
self.assertAlmostEqual(cub4.point(0.1), (-166.36210353 - 195.97246515j))
self.assertAlmostEqual(cub4.point(0.2), (-135.80749184 - 188.4481264j))
self.assertAlmostEqual(cub4.point(0.3), (-107.31641851 - 176.40724745j))
self.assertAlmostEqual(cub4.point(0.4), (-81.29771712 - 160.258662j))
self.assertAlmostEqual(cub4.point(0.5), (-58.16022125 - 140.41120375j))
self.assertAlmostEqual(cub4.point(0.6), (-38.31276448 - 117.2737064j))
self.assertAlmostEqual(cub4.point(0.7), (-22.16418039 - 91.25500365j))
self.assertAlmostEqual(cub4.point(0.8), (-10.12330256 - 62.7639292j))
self.assertAlmostEqual(cub4.point(0.9), (-2.59896457 - 32.20931675j))
self.assertAlmostEqual(cub4.point(1), 0j)
def test_svg_examples(self):
# M100,200 C100,100 250,100 250,200
path1 = CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j)
self.assertAlmostEqual(path1.point(0), (100 + 200j))
self.assertAlmostEqual(path1.point(0.3), (132.4 + 137j))
self.assertAlmostEqual(path1.point(0.5), (175 + 125j))
self.assertAlmostEqual(path1.point(0.9), (245.8 + 173j))
self.assertAlmostEqual(path1.point(1), (250 + 200j))
# S400,300 400,200
path2 = CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j)
self.assertAlmostEqual(path2.point(0), (250 + 200j))
self.assertAlmostEqual(path2.point(0.3), (282.4 + 263j))
self.assertAlmostEqual(path2.point(0.5), (325 + 275j))
self.assertAlmostEqual(path2.point(0.9), (395.8 + 227j))
self.assertAlmostEqual(path2.point(1), (400 + 200j))
# M100,200 C100,100 400,100 400,200
path3 = CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j)
self.assertAlmostEqual(path3.point(0), (100 + 200j))
self.assertAlmostEqual(path3.point(0.3), (164.8 + 137j))
self.assertAlmostEqual(path3.point(0.5), (250 + 125j))
self.assertAlmostEqual(path3.point(0.9), (391.6 + 173j))
self.assertAlmostEqual(path3.point(1), (400 + 200j))
# M100,500 C25,400 475,400 400,500
path4 = CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j)
self.assertAlmostEqual(path4.point(0), (100 + 500j))
self.assertAlmostEqual(path4.point(0.3), (145.9 + 437j))
self.assertAlmostEqual(path4.point(0.5), (250 + 425j))
self.assertAlmostEqual(path4.point(0.9), (407.8 + 473j))
self.assertAlmostEqual(path4.point(1), (400 + 500j))
# M100,800 C175,700 325,700 400,800
path5 = CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j)
self.assertAlmostEqual(path5.point(0), (100 + 800j))
self.assertAlmostEqual(path5.point(0.3), (183.7 + 737j))
self.assertAlmostEqual(path5.point(0.5), (250 + 725j))
self.assertAlmostEqual(path5.point(0.9), (375.4 + 773j))
self.assertAlmostEqual(path5.point(1), (400 + 800j))
# M600,200 C675,100 975,100 900,200
path6 = CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j)
self.assertAlmostEqual(path6.point(0), (600 + 200j))
self.assertAlmostEqual(path6.point(0.3), (712.05 + 137j))
self.assertAlmostEqual(path6.point(0.5), (806.25 + 125j))
self.assertAlmostEqual(path6.point(0.9), (911.85 + 173j))
self.assertAlmostEqual(path6.point(1), (900 + 200j))
# M600,500 C600,350 900,650 900,500
path7 = CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)
self.assertAlmostEqual(path7.point(0), (600 + 500j))
self.assertAlmostEqual(path7.point(0.3), (664.8 + 462.2j))
self.assertAlmostEqual(path7.point(0.5), (750 + 500j))
self.assertAlmostEqual(path7.point(0.9), (891.6 + 532.4j))
self.assertAlmostEqual(path7.point(1), (900 + 500j))
# M600,800 C625,700 725,700 750,800
path8 = CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j)
self.assertAlmostEqual(path8.point(0), (600 + 800j))
self.assertAlmostEqual(path8.point(0.3), (638.7 + 737j))
self.assertAlmostEqual(path8.point(0.5), (675 + 725j))
self.assertAlmostEqual(path8.point(0.9), (740.4 + 773j))
self.assertAlmostEqual(path8.point(1), (750 + 800j))
# S875,900 900,800
inversion = (750 + 800j) + (750 + 800j) - (725 + 700j)
path9 = CubicBezier(750 + 800j, inversion, 875 + 900j, 900 + 800j)
self.assertAlmostEqual(path9.point(0), (750 + 800j))
self.assertAlmostEqual(path9.point(0.3), (788.7 + 863j))
self.assertAlmostEqual(path9.point(0.5), (825 + 875j))
self.assertAlmostEqual(path9.point(0.9), (890.4 + 827j))
self.assertAlmostEqual(path9.point(1), (900 + 800j))
def test_length(self):
# A straight line:
cub = CubicBezier(
complex(0, 0),
complex(0, 0),
complex(0, 100),
complex(0, 100)
)
self.assertAlmostEqual(cub.length(), 100)
# A diagonal line:
cub = CubicBezier(
complex(0, 0),
complex(0, 0),
complex(100, 100),
complex(100, 100)
)
self.assertAlmostEqual(cub.length(), sqrt(2 * 100 * 100))
# A quarter circle large_arc with radius 100:
kappa = 4 * (sqrt(2) - 1) / 3 # http://www.whizkidtech.redprince.net/bezier/circle/
cub = CubicBezier(
complex(0, 0),
complex(0, kappa * 100),
complex(100 - kappa * 100, 100),
complex(100, 100)
)
# We can't compare with pi*50 here, because this is just an
# approximation of a circle large_arc. pi*50 is 157.079632679
# So this is just yet another "warn if this changes" test.
# This value is not verified to be correct.
self.assertAlmostEqual(cub.length(), 157.1016698)
# A recursive solution has also been suggested, but for CubicBezier
# curves it could get a false solution on curves where the midpoint is
# on a straight line between the start and end. For example, the
# following curve would get solved as a straight line and get the
# length 300.
# Make sure this is not the case.
cub = CubicBezier(
complex(600, 500),
complex(600, 350),
complex(900, 650),
complex(900, 500)
)
self.assertTrue(cub.length() > 300.0)
def test_equality(self):
# This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual
segment = CubicBezier(complex(600, 500), complex(600, 350),
complex(900, 650), complex(900, 500))
self.assertTrue(segment ==
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))
self.assertTrue(segment !=
CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j))
self.assertTrue(segment != Line(0, 400))
class QuadraticBezierTest(unittest.TestCase):
def test_svg_examples(self):
"""These is the path in the SVG specs"""
# M200,300 Q400,50 600,300 T1000,300
path1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
self.assertAlmostEqual(path1.point(0), (200 + 300j))
self.assertAlmostEqual(path1.point(0.3), (320 + 195j))
self.assertAlmostEqual(path1.point(0.5), (400 + 175j))
self.assertAlmostEqual(path1.point(0.9), (560 + 255j))
self.assertAlmostEqual(path1.point(1), (600 + 300j))
# T1000, 300
inversion = (600 + 300j) + (600 + 300j) - (400 + 50j)
path2 = QuadraticBezier(600 + 300j, inversion, 1000 + 300j)
self.assertAlmostEqual(path2.point(0), (600 + 300j))
self.assertAlmostEqual(path2.point(0.3), (720 + 405j))
self.assertAlmostEqual(path2.point(0.5), (800 + 425j))
self.assertAlmostEqual(path2.point(0.9), (960 + 345j))
self.assertAlmostEqual(path2.point(1), (1000 + 300j))
def test_length(self):
# expected results calculated with
# svg.path.segment_length(q, 0, 1, q.start, q.end, 1e-14, 20, 0)
q1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j)
closedq = QuadraticBezier(6+2j, 5-1j, 6+2j)
linq1 = QuadraticBezier(1, 2, 3)
linq2 = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
nodalq = QuadraticBezier(1, 1, 1)
tests = [(q1, 487.77109389525975),
(q2, 379.90458193489155),
(closedq, 3.1622776601683795),
(linq1, 2),
(linq2, 22.73335777124786),
(nodalq, 0)]
for q, exp_res in tests:
self.assertAlmostEqual(q.length(), exp_res)
# partial length tests
tests = [(q1, 212.34775387566032),
(q2, 166.22170622052397),
(closedq, 0.7905694150420949),
(linq1, 1.0),
(nodalq, 0.0)]
t0 = 0.25
t1 = 0.75
for q, exp_res in tests:
self.assertAlmostEqual(q.length(t0=t0, t1=t1), exp_res)
# linear partial cases
linq2 = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
tests = [(0, 1/24, 0.13975424859373725),
(0, 1/12, 0.1863389981249823),
(0, 0.5, 4.844813951249543),
(0, 1, 22.73335777124786),
(1/24, 1/12, 0.04658474953124506),
(1/24, 0.5, 4.705059702655722),
(1/24, 1, 22.59360352265412),
(1/12, 0.5, 4.658474953124562),
(1/12, 1, 22.54701877312288),
(0.5, 1, 17.88854381999832)]
for t0, t1, exp_s in tests:
self.assertAlmostEqual(linq2.length(t0=t0, t1=t1), exp_s)
def test_equality(self):
# This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual
segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j))
self.assertTrue(segment != QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j))
self.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment)
class ArcTest(unittest.TestCase):
def test_points(self):
arc1 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
self.assertAlmostEqual(arc1.center, 100 + 0j)
self.assertAlmostEqual(arc1.theta, 180.0)
self.assertAlmostEqual(arc1.delta, -90.0)
self.assertAlmostEqual(arc1.point(0.0), 0j)
self.assertAlmostEqual(arc1.point(0.1), (1.23116594049 + 7.82172325201j))
self.assertAlmostEqual(arc1.point(0.2), (4.89434837048 + 15.4508497187j))
self.assertAlmostEqual(arc1.point(0.3), (10.8993475812 + 22.699524987j))
self.assertAlmostEqual(arc1.point(0.4), (19.0983005625 + 29.3892626146j))
self.assertAlmostEqual(arc1.point(0.5), (29.2893218813 + 35.3553390593j))
self.assertAlmostEqual(arc1.point(0.6), (41.2214747708 + 40.4508497187j))
self.assertAlmostEqual(arc1.point(0.7), (54.6009500260 + 44.5503262094j))
self.assertAlmostEqual(arc1.point(0.8), (69.0983005625 + 47.5528258148j))
self.assertAlmostEqual(arc1.point(0.9), (84.3565534960 + 49.3844170298j))
self.assertAlmostEqual(arc1.point(1.0), (100 + 50j))
arc2 = Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j)
self.assertAlmostEqual(arc2.center, 50j)
self.assertAlmostEqual(arc2.theta, -90.0)
self.assertAlmostEqual(arc2.delta, -270.0)
self.assertAlmostEqual(arc2.point(0.0), 0j)
self.assertAlmostEqual(arc2.point(0.1), (-45.399049974 + 5.44967379058j))
self.assertAlmostEqual(arc2.point(0.2), (-80.9016994375 + 20.6107373854j))
self.assertAlmostEqual(arc2.point(0.3), (-98.7688340595 + 42.178276748j))
self.assertAlmostEqual(arc2.point(0.4), (-95.1056516295 + 65.4508497187j))
self.assertAlmostEqual(arc2.point(0.5), (-70.7106781187 + 85.3553390593j))
self.assertAlmostEqual(arc2.point(0.6), (-30.9016994375 + 97.5528258148j))
self.assertAlmostEqual(arc2.point(0.7), (15.643446504 + 99.3844170298j))
self.assertAlmostEqual(arc2.point(0.8), (58.7785252292 + 90.4508497187j))
self.assertAlmostEqual(arc2.point(0.9), (89.1006524188 + 72.699524987j))
self.assertAlmostEqual(arc2.point(1.0), (100 + 50j))
arc3 = Arc(0j, 100 + 50j, 0, 0, 1, 100 + 50j)
self.assertAlmostEqual(arc3.center, 50j)
self.assertAlmostEqual(arc3.theta, -90.0)
self.assertAlmostEqual(arc3.delta, 90.0)
self.assertAlmostEqual(arc3.point(0.0), 0j)
self.assertAlmostEqual(arc3.point(0.1), (15.643446504 + 0.615582970243j))
self.assertAlmostEqual(arc3.point(0.2), (30.9016994375 + 2.44717418524j))
self.assertAlmostEqual(arc3.point(0.3), (45.399049974 + 5.44967379058j))
self.assertAlmostEqual(arc3.point(0.4), (58.7785252292 + 9.54915028125j))
self.assertAlmostEqual(arc3.point(0.5), (70.7106781187 + 14.6446609407j))
self.assertAlmostEqual(arc3.point(0.6), (80.9016994375 + 20.6107373854j))
self.assertAlmostEqual(arc3.point(0.7), (89.1006524188 + 27.300475013j))
self.assertAlmostEqual(arc3.point(0.8), (95.1056516295 + 34.5491502813j))
self.assertAlmostEqual(arc3.point(0.9), (98.7688340595 + 42.178276748j))
self.assertAlmostEqual(arc3.point(1.0), (100 + 50j))
arc4 = Arc(0j, 100 + 50j, 0, 1, 1, 100 + 50j)
self.assertAlmostEqual(arc4.center, 100 + 0j)
self.assertAlmostEqual(arc4.theta, 180.0)
self.assertAlmostEqual(arc4.delta, 270.0)
self.assertAlmostEqual(arc4.point(0.0), 0j)
self.assertAlmostEqual(arc4.point(0.1), (10.8993475812 - 22.699524987j))
self.assertAlmostEqual(arc4.point(0.2), (41.2214747708 - 40.4508497187j))
self.assertAlmostEqual(arc4.point(0.3), (84.3565534960 - 49.3844170298j))
self.assertAlmostEqual(arc4.point(0.4), (130.901699437 - 47.5528258148j))
self.assertAlmostEqual(arc4.point(0.5), (170.710678119 - 35.3553390593j))
self.assertAlmostEqual(arc4.point(0.6), (195.105651630 - 15.4508497187j))
self.assertAlmostEqual(arc4.point(0.7), (198.768834060 + 7.82172325201j))
self.assertAlmostEqual(arc4.point(0.8), (180.901699437 + 29.3892626146j))
self.assertAlmostEqual(arc4.point(0.9), (145.399049974 + 44.5503262094j))
self.assertAlmostEqual(arc4.point(1.0), (100 + 50j))
arc5 = Arc((725.307482225571-915.5548199281527j),
(202.79421639137703+148.77294617167183j),
225.6910319606926, 1, 1,
(-624.6375539637027+896.5483089399895j))
self.assertAlmostEqual(arc5.point(0.0), (725.307482226-915.554819928j))
self.assertAlmostEqual(arc5.point(0.0909090909091), (1023.47397369-597.730444283j))
self.assertAlmostEqual(arc5.point(0.181818181818), (1242.80253007-232.251400124j))
self.assertAlmostEqual(arc5.point(0.272727272727), (1365.52445614+151.273373978j))
self.assertAlmostEqual(arc5.point(0.363636363636), (1381.69755131+521.772981736j))
self.assertAlmostEqual(arc5.point(0.454545454545), (1290.01156757+849.231748376j))
self.assertAlmostEqual(arc5.point(0.545454545455), (1097.89435807+1107.12091209j))
self.assertAlmostEqual(arc5.point(0.636363636364), (820.910116547+1274.54782658j))
self.assertAlmostEqual(arc5.point(0.727272727273), (481.49845896+1337.94855893j))
self.assertAlmostEqual(arc5.point(0.818181818182), (107.156499251+1292.18675889j))
self.assertAlmostEqual(arc5.point(0.909090909091), (-271.788803303+1140.96977533j))
def test_length(self):
# I'll test the length calculations by making a circle, in two parts.
arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j)
self.assertAlmostEqual(arc1.length(), pi * 100)
self.assertAlmostEqual(arc2.length(), pi * 100)
def test_equality(self):
# This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual
segment = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
self.assertTrue(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
self.assertTrue(segment != Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j))
class TestPath(unittest.TestCase):
def test_circle(self):
arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j)
path = Path(arc1, arc2)
self.assertAlmostEqual(path.point(0.0), 0j)
self.assertAlmostEqual(path.point(0.25), (100 + 100j))
self.assertAlmostEqual(path.point(0.5), (200 + 0j))
self.assertAlmostEqual(path.point(0.75), (100 - 100j))
self.assertAlmostEqual(path.point(1.0), 0j)
self.assertAlmostEqual(path.length(), pi * 200)
def test_svg_specs(self):
"""The paths that are in the SVG specs"""
# Big pie: M300,200 h-150 a150,150 0 1,0 150,-150 z
path = Path(Line(300 + 200j, 150 + 200j),
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
Line(300 + 50j, 300 + 200j))
# The points and length for this path are calculated and not regression tests.
self.assertAlmostEqual(path.point(0.0), (300 + 200j))
self.assertAlmostEqual(path.point(0.14897825542), (150 + 200j))
self.assertAlmostEqual(path.point(0.5), (406.066017177 + 306.066017177j))
self.assertAlmostEqual(path.point(1 - 0.14897825542), (300 + 50j))
self.assertAlmostEqual(path.point(1.0), (300 + 200j))
# The errors seem to accumulate. Still 6 decimal places is more than good enough.
self.assertAlmostEqual(path.length(), pi * 225 + 300, places=6)
# Little pie: M275,175 v-150 a150,150 0 0,0 -150,150 z
path = Path(Line(275 + 175j, 275 + 25j),
Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j),
Line(125 + 175j, 275 + 175j))
# The points and length for this path are calculated and not regression tests.
self.assertAlmostEqual(path.point(0.0), (275 + 175j))
self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j))
self.assertAlmostEqual(path.point(0.5), (168.93398282201787 + 68.93398282201787j))
self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j))
self.assertAlmostEqual(path.point(1.0), (275 + 175j))
# The errors seem to accumulate. Still 6 decimal places is more than good enough.
self.assertAlmostEqual(path.length(), pi * 75 + 300, places=6)
# Bumpy path: M600,350 l 50,-25
# a25,25 -30 0,1 50,-25 l 50,-25
# a25,50 -30 0,1 50,-25 l 50,-25
# a25,75 -30 0,1 50,-25 l 50,-25
# a25,100 -30 0,1 50,-25 l 50,-25
# Commented out because by Andy cause I was skeptical of path.point
# ground truth values
# path = Path(Line(600 + 350j, 650 + 325j),
# Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j),
# Line(700 + 300j, 750 + 275j),
# Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j),
# Line(800 + 250j, 850 + 225j),
# Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j),
# Line(900 + 200j, 950 + 175j),
# Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j),
# Line(1000 + 150j, 1050 + 125j),
# )
# # These are *not* calculated, but just regression tests. Be skeptical.
# self.assertAlmostEqual(path.point(0), (600+350j))
# self.assertAlmostEqual(path.point(0.3), (755.239799276+212.182020958j))
# self.assertAlmostEqual(path.point(0.5), (827.730749264+147.824157418j))
# self.assertAlmostEqual(path.point(0.9), (971.284357806+106.302352605j))
# self.assertAlmostEqual(path.point(1), (1050+125j))
# # The errors seem to accumulate. Still 6 decimal places is more than good enough.
# self.assertAlmostEqual(path.length(), 928.3886394081095)
def test_repr(self):
path = Path(
Line(start=600 + 350j, end=650 + 325j),
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j),
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j),
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
self.assertEqual(eval(repr(path)), path)
def test_equality(self):
# This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual
path1 = Path(
Line(start=600 + 350j, end=650 + 325j),
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j),
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j),
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
path2 = Path(
Line(start=600 + 350j, end=650 + 325j),
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j),
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j),
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
self.assertTrue(path1 == path2)
# Modify path2:
path2[0].start = 601 + 350j
self.assertTrue(path1 != path2)
# Modify back:
path2[0].start = 600 + 350j
self.assertFalse(path1 != path2)
# Get rid of the last segment:
del path2[-1]
self.assertFalse(path1 == path2)
# It's not equal to a list of it's segments
self.assertTrue(path1 != path1[:])
self.assertFalse(path1 == path1[:])
def test_continuous_subpaths(self):
"""Test the Path.continuous_subpaths() method."""
# Continuous and open example
q = Path(Line(1, 2))
a = [Path(Line(1, 2))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
# # Continuous and closed example
q = Path(Line(1, 2), Line(2, 1))
a = [Path(Line(1, 2), Line(2, 1))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = q == Path(*[seg for subpath in subpaths for seg in subpath])
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
# Continuous and open example
q = Path(Line(1, 2), Line(2, 3), Line(3, 4))
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
# Continuous and closed example
q = Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1))
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
# Discontinuous example
q = Path(Line(1, 2), Line(2, 3), Line(3, 4),
Line(10, 11))
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4)),
Path(Line(10, 11))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
# Discontinuous closed example
q = Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1),
Line(10, 11), Line(11, 12))
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1)),
Path(Line(10, 11), Line(11, 12))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
# Discontinuous example
q = Path(Line(1, 2),
Line(1, 2), Line(2, 3),
Line(10, 11), Line(11, 12), Line(12, 13),
Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 14))
a = [Path(Line(1, 2)),
Path(Line(1, 2), Line(2, 3)),
Path(Line(10, 11), Line(11, 12), Line(12, 13)),
Path(Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 14))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
# Discontinuous example with overlapping end
q = Path(Line(1, 2),
Line(5, 6), Line(6, 7),
Line(10, 11), Line(11, 12), Line(12, 13),
Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 1))
a = [Path(Line(1, 2)),
Path(Line(5, 6), Line(6, 7)),
Path(Line(10, 11), Line(11, 12), Line(12, 13)),
Path(Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 1))]
subpaths = q.continuous_subpaths()
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
self.assertTrue(subpaths == a)
self.assertTrue(chk1)
self.assertTrue(chk2)
class Test_ilength(unittest.TestCase):
def test_ilength(self):
# See svgpathtools.notes.inv_arclength.py for information on how these
# test values were generated (using the .length() method).
##############################################################
# Lines
l = Line(1, 3-1j)
nodall = Line(1+1j, 1+1j)
tests = [(l, 0.01, 0.022360679774997897),
(l, 0.1, 0.223606797749979),
(l, 0.5, 1.118033988749895),
(l, 0.9, 2.012461179749811),
(l, 0.99, 2.213707297724792)]
for (l, t, s) in tests:
self.assertAlmostEqual(l.ilength(s), t)
###############################################################
# Quadratics
q1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j)
closedq = QuadraticBezier(6 + 2j, 5 - 1j, 6 + 2j)
linq = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
nodalq = QuadraticBezier(1, 1, 1)
tests = [(q1, 0.01, 6.364183310105577),
(q1, 0.1, 60.23857499635088),
(q1, 0.5, 243.8855469477619),
(q1, 0.9, 427.53251889917294),
(q1, 0.99, 481.40691058541813),
(q2, 0.01, 6.365673533661836),
(q2, 0.1, 60.31675895732397),
(q2, 0.5, 233.24592830045907),
(q2, 0.9, 346.42891253298706),
(q2, 0.99, 376.32659156736844),
(closedq, 0.01, 0.06261309767133393),
(closedq, 0.1, 0.5692099788303084),
(closedq, 0.5, 1.5811388300841898),
(closedq, 0.9, 2.5930676813380713),
(closedq, 0.99, 3.0996645624970456),
(linq, 0.01, 0.04203807797699605),
(linq, 0.1, 0.19379255804998186),
(linq, 0.5, 4.844813951249544),
(linq, 0.9, 18.0823363780483),
(linq, 0.99, 22.24410609777091)]
for q, t, s in tests:
try:
self.assertAlmostEqual(q.ilength(s), t)
except:
print(q)
print(s)
print(t)
raise
###############################################################
# Cubics
c1 = CubicBezier(200 + 300j, 400 + 50j, 600+100j, -200)
symc = CubicBezier(1-2j, 10-1j, 10+1j, 1+2j)
closedc = CubicBezier(1-2j, 10-1j, 10+1j, 1-2j)
tests = [(c1, 0.01, 9.53434737943073),
(c1, 0.1, 88.89941848775852),
(c1, 0.5, 278.5750942713189),
(c1, 0.9, 651.4957786584646),
(c1, 0.99, 840.2010603832538),
(symc, 0.01, 0.2690118556702902),
(symc, 0.1, 2.45230693868727),
(symc, 0.5, 7.256147083644424),
(symc, 0.9, 12.059987228602886),
(symc, 0.99, 14.243282311619401),
(closedc, 0.01, 0.26901140075538765),
(closedc, 0.1, 2.451722765460998),
(closedc, 0.5, 6.974058969750422),
(closedc, 0.9, 11.41781741489913),
(closedc, 0.99, 13.681324783697782)]
for (c, t, s) in tests:
self.assertAlmostEqual(c.ilength(s), t)
###############################################################
# Arcs
arc1 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
arc2 = Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j)
arc3 = Arc(0j, 100 + 50j, 0, 0, 1, 100 + 50j)
arc4 = Arc(0j, 100 + 50j, 0, 1, 1, 100 + 50j)
arc5 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
arc6 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j)
arc7 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
tests = [(arc1, 0.01, 0.785495042476231),
(arc1, 0.1, 7.949362877455911),
(arc1, 0.5, 48.28318721111137),
(arc1, 0.9, 105.44598206942156),
(arc1, 0.99, 119.53485487631241),
(arc2, 0.01, 4.71108115728524),
(arc2, 0.1, 45.84152747676626),
(arc2, 0.5, 169.38878996795734),
(arc2, 0.9, 337.44707303579696),
(arc2, 0.99, 360.95800139278765),
(arc3, 0.01, 1.5707478805335624),
(arc3, 0.1, 15.659620687424416),
(arc3, 0.5, 72.82241554573457),
(arc3, 0.9, 113.15623987939003),
(arc3, 0.99, 120.3201077143697),
(arc4, 0.01, 2.3588068777503897),
(arc4, 0.1, 25.869735234740887),
(arc4, 0.5, 193.9280183025816),
(arc4, 0.9, 317.4752807937718),
(arc4, 0.99, 358.6057271132536),
(arc5, 0.01, 3.141592653589793),
(arc5, 0.1, 31.415926535897935),
(arc5, 0.5, 157.07963267948966),
(arc5, 0.9, 282.7433388230814),
(arc5, 0.99, 311.01767270538954),
(arc6, 0.01, 3.141592653589793),
(arc6, 0.1, 31.415926535897928),
(arc6, 0.5, 157.07963267948966),
(arc6, 0.9, 282.7433388230814),
(arc6, 0.99, 311.01767270538954),
(arc7, 0.01, 0.785495042476231),
(arc7, 0.1, 7.949362877455911),
(arc7, 0.5, 48.28318721111137),
(arc7, 0.9, 105.44598206942156),
(arc7, 0.99, 119.53485487631241)]
for (c, t, s) in tests:
self.assertAlmostEqual(c.ilength(s), t)
###############################################################
# Paths
line1 = Line(600 + 350j, 650 + 325j)
arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j)
cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j)
cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j)
quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j)
linez = Line(600 + 300j, 600 + 350j)
bezpath = Path(line1, cub1, cub2, quad3)
bezpathz = Path(line1, cub1, cub2, quad3, linez)
path = Path(line1, arc1, cub2, quad3)
pathz = Path(line1, arc1, cub2, quad3, linez)
lpath = Path(linez)
qpath = Path(quad3)
cpath = Path(cub1)
apath = Path(arc1)
tests = [(bezpath, 0.0, 0.0),
(bezpath, 0.1111111111111111, 286.2533595149515),
(bezpath, 0.2222222222222222, 503.8620222915423),
(bezpath, 0.3333333333333333, 592.6337135346268),
(bezpath, 0.4444444444444444, 644.3880677233315),
(bezpath, 0.5555555555555556, 835.0384185011363),
(bezpath, 0.6666666666666666, 1172.8729938994575),
(bezpath, 0.7777777777777778, 1308.6205983178952),
(bezpath, 0.8888888888888888, 1532.8473168900994),
(bezpath, 1.0, 1758.2427369258733),
(bezpathz, 0.0, 0.0),
(bezpathz, 0.1111111111111111, 294.15942308605435),
(bezpathz, 0.2222222222222222, 512.4295461513882),
(bezpathz, 0.3333333333333333, 594.0779370040138),
(bezpathz, 0.4444444444444444, 658.7361976564598),
(bezpathz, 0.5555555555555556, 874.1674336581542),
(bezpathz, 0.6666666666666666, 1204.2371344392693),
(bezpathz, 0.7777777777777778, 1356.773042865213),
(bezpathz, 0.8888888888888888, 1541.808492602876),
(bezpathz, 1.0, 1808.2427369258733),
(path, 0.0, 0.0),
(path, 0.1111111111111111, 81.44016397108298),
(path, 0.2222222222222222, 164.72556816469307),
(path, 0.3333333333333333, 206.71343564679154),
(path, 0.4444444444444444, 265.4898349999353),
(path, 0.5555555555555556, 367.5420981413199),
(path, 0.6666666666666666, 487.29863861165995),
(path, 0.7777777777777778, 511.84069655405284),
(path, 0.8888888888888888, 579.9530841780238),
(path, 1.0, 732.9614757397469),
(pathz, 0.0, 0.0),
(pathz, 0.1111111111111111, 86.99571952663854),
(pathz, 0.2222222222222222, 174.33662608180325),
(pathz, 0.3333333333333333, 214.42194393858466),
(pathz, 0.4444444444444444, 289.94661033436205),
(pathz, 0.5555555555555556, 408.38391100702125),
(pathz, 0.6666666666666666, 504.4309373835351),
(pathz, 0.7777777777777778, 533.774834546298),
(pathz, 0.8888888888888888, 652.931321760894),
(pathz, 1.0, 782.9614757397469),
(lpath, 0.0, 0.0),
(lpath, 0.1111111111111111, 5.555555555555555),
(lpath, 0.2222222222222222, 11.11111111111111),
(lpath, 0.3333333333333333, 16.666666666666664),
(lpath, 0.4444444444444444, 22.22222222222222),
(lpath, 0.5555555555555556, 27.77777777777778),
(lpath, 0.6666666666666666, 33.33333333333333),
(lpath, 0.7777777777777778, 38.88888888888889),
(lpath, 0.8888888888888888, 44.44444444444444),
(lpath, 1.0, 50.0),
(qpath, 0.0, 0.0),
(qpath, 0.1111111111111111, 17.28395061728395),
(qpath, 0.2222222222222222, 24.69135802469136),
(qpath, 0.3333333333333333, 27.777777777777786),
(qpath, 0.4444444444444444, 40.12345679012344),
(qpath, 0.5555555555555556, 62.3456790123457),
(qpath, 0.6666666666666666, 94.44444444444446),
(qpath, 0.7777777777777778, 136.41975308641975),
(qpath, 0.8888888888888888, 188.27160493827154),
(qpath, 1.0, 250.0),
(cpath, 0.0, 0.0),
(cpath, 0.1111111111111111, 207.35525375551356),
(cpath, 0.2222222222222222, 366.0583590267552),
(cpath, 0.3333333333333333, 474.34064293812787),
(cpath, 0.4444444444444444, 530.467036317684),
(cpath, 0.5555555555555556, 545.0444351253911),
(cpath, 0.6666666666666666, 598.9767847757622),
(cpath, 0.7777777777777778, 710.4080903390646),
(cpath, 0.8888888888888888, 881.1796899225557),
(cpath, 1.0, 1113.0914444911352),
(apath, 0.0, 0.0),
(apath, 0.1111111111111111, 9.756687033889872),
(apath, 0.2222222222222222, 19.51337406777974),
(apath, 0.3333333333333333, 29.27006110166961),
(apath, 0.4444444444444444, 39.02674813555948),
(apath, 0.5555555555555556, 48.783435169449355),
(apath, 0.6666666666666666, 58.54012220333922),
(apath, 0.7777777777777778, 68.2968092372291),
(apath, 0.8888888888888888, 78.05349627111896),
(apath, 1.0, 87.81018330500885)]
for (c, t, s) in tests:
self.assertAlmostEqual(c.ilength(s), t)
###############################################################
# Exception Cases
nodalq = QuadraticBezier(1, 1, 1)
with self.assertRaises(AssertionError):
nodalq.ilength(1)
lin = Line(0, 0.5j)
with self.assertRaises(ValueError):
lin.ilength(1)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,244 @@
# External dependencies
from __future__ import division, absolute_import, print_function
import unittest
from numpy import poly1d
# Internal dependencies
from svgpathtools import *
class TestPathTools(unittest.TestCase):
def setUp(self):
self.arc1 = Arc(650+325j, 25+25j, -30.0, False, True, 700+300j)
self.line1 = Line(0, 100+100j)
self.quadratic1 = QuadraticBezier(100+100j, 150+150j, 300+200j)
self.cubic1 = CubicBezier(300+200j, 350+400j, 400+425j, 650+325j)
self.path_of_all_seg_types = Path(self.line1, self.quadratic1,
self.cubic1, self.arc1)
self.path_of_bezier_seg_types = Path(self.line1, self.quadratic1,
self.cubic1)
def test_is_bezier_segment(self):
# False
self.assertFalse(is_bezier_segment(self.arc1))
self.assertFalse(is_bezier_segment(self.path_of_bezier_seg_types))
# True
self.assertTrue(is_bezier_segment(self.line1))
self.assertTrue(is_bezier_segment(self.quadratic1))
self.assertTrue(is_bezier_segment(self.cubic1))
def test_is_bezier_path(self):
# False
self.assertFalse(is_bezier_path(self.path_of_all_seg_types))
self.assertFalse(is_bezier_path(self.line1))
self.assertFalse(is_bezier_path(self.quadratic1))
self.assertFalse(is_bezier_path(self.cubic1))
self.assertFalse(is_bezier_path(self.arc1))
# True
self.assertTrue(is_bezier_path(self.path_of_bezier_seg_types))
self.assertTrue(is_bezier_path(Path()))
def test_polynomial2bezier(self):
def distfcn(tup1, tup2):
assert len(tup1) == len(tup2)
return sum((tup1[i]-tup2[i])**2 for i in range(len(tup1)))**0.5
# Case: Line
pcoeffs = [(-1.7-2j), (6+2j)]
p = poly1d(pcoeffs)
correct_bpoints = [(6+2j), (4.3+0j)]
# Input poly1d object
bez = poly2bez(p)
bpoints = bez.bpoints()
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
# Input list of coefficients
bpoints = poly2bez(pcoeffs, return_bpoints=True)
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
# Case: Quadratic
pcoeffs = [(29.5+15.5j), (-31-19j), (7.5+5.5j)]
p = poly1d(pcoeffs)
correct_bpoints = [(7.5+5.5j), (-8-4j), (6+2j)]
# Input poly1d object
bez = poly2bez(p)
bpoints = bez.bpoints()
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
# Input list of coefficients
bpoints = poly2bez(pcoeffs, return_bpoints=True)
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
# Case: Cubic
pcoeffs = [(-18.5-12.5j), (34.5+16.5j), (-18-6j), (6+2j)]
p = poly1d(pcoeffs)
correct_bpoints = [(6+2j), 0j, (5.5+3.5j), (4+0j)]
# Input poly1d object
bez = poly2bez(p)
bpoints = bez.bpoints()
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
# Input list of coefficients object
bpoints = poly2bez(pcoeffs, return_bpoints=True)
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
def test_bpoints2bezier(self):
cubic_bpoints = [(6+2j), 0, (5.5+3.5j), (4+0j)]
quadratic_bpoints = [(6+2j), 0, (5.5+3.5j)]
line_bpoints = [(6+2j), 0]
self.assertTrue(isinstance(bpoints2bezier(cubic_bpoints), CubicBezier))
self.assertTrue(isinstance(bpoints2bezier(quadratic_bpoints),
QuadraticBezier))
self.assertTrue(isinstance(bpoints2bezier(line_bpoints), Line))
self.assertSequenceEqual(bpoints2bezier(cubic_bpoints).bpoints(),
cubic_bpoints)
self.assertSequenceEqual(bpoints2bezier(quadratic_bpoints).bpoints(),
quadratic_bpoints)
self.assertSequenceEqual(bpoints2bezier(line_bpoints).bpoints(),
line_bpoints)
# def test_line2pathd(self):
# bpoints = (0+1.5j, 100+10j)
# line = Line(*bpoints)
#
# # from Line object
# pathd = line2pathd(line)
# path = parse_path(pathd)
# self.assertTrue(path[0] == line)
#
# # from list of bpoints
# pathd = line2pathd(bpoints)
# path = parse_path(pathd)
# self.assertTrue(path[0] == line)
#
# def test_cubic2pathd(self):
# bpoints = (0+1.5j, 100+10j, 150-155.3j, 0)
# cubic = CubicBezier(*bpoints)
#
# # from Line object
# pathd = cubic2pathd(cubic)
# path = parse_path(pathd)
# self.assertTrue(path[0] == cubic)
#
# # from list of bpoints
# pathd = cubic2pathd(bpoints)
# path = parse_path(pathd)
# self.assertTrue(path[0] == cubic)
def test_closest_point_in_path(self):
def distfcn(tup1, tup2):
assert len(tup1) == len(tup2)
return sum((tup1[i]-tup2[i])**2 for i in range(len(tup1)))**0.5
# Note: currently the radiialrange method is not implemented for Arc
# objects
# test_path = self.path_of_all_seg_types
# origin = -123 - 123j
# expected_result = ???
# self.assertAlmostEqual(min_radius(origin, test_path),
# expected_result)
# generic case (where is_bezier_path(test_path) == True)
test_path = self.path_of_bezier_seg_types
pt = 300+300j
expected_result = (29.382522853493143, 0.17477067969145446, 2)
result = closest_point_in_path(pt, test_path)
err = distfcn(expected_result, result)
self.assertAlmostEqual(err, 0)
# cubic test with multiple valid solutions
test_path = Path(CubicBezier(1-2j, 10-1j, 10+1j, 1+2j))
pt = 3
expected_results = [(1.7191878932122302, 0.90731678233211366, 0),
(1.7191878932122304, 0.092683217667886342, 0)]
result = closest_point_in_path(pt, test_path)
err = min(distfcn(e_res, result) for e_res in expected_results)
self.assertAlmostEqual(err, 0)
def test_farthest_point_in_path(self):
def distfcn(tup1, tup2):
assert len(tup1) == len(tup2)
return sum((tup1[i]-tup2[i])**2 for i in range(len(tup1)))**0.5
# Note: currently the radiialrange method is not implemented for Arc
# objects
# test_path = self.path_of_all_seg_types
# origin = -123 - 123j
# expected_result = ???
# self.assertAlmostEqual(min_radius(origin, test_path),
# expected_result)
# boundary test
test_path = self.path_of_bezier_seg_types
pt = 300+300j
expected_result = (424.26406871192853, 0, 0)
result = farthest_point_in_path(pt, test_path)
err = distfcn(expected_result, result)
self.assertAlmostEqual(err, 0)
# non-boundary test
test_path = Path(CubicBezier(1-2j, 10-1j, 10+1j, 1+2j))
pt = 3
expected_result = (4.75, 0.5, 0)
result = farthest_point_in_path(pt, test_path)
err = distfcn(expected_result, result)
self.assertAlmostEqual(err, 0)
def test_path_encloses_pt(self):
line1 = Line(0, 100+100j)
quadratic1 = QuadraticBezier(100+100j, 150+150j, 300+200j)
cubic1 = CubicBezier(300+200j, 350+400j, 400+425j, 650+325j)
line2 = Line(650+325j, 650+10j)
line3 = Line(650+10j, 0)
open_bez_path = Path(line1, quadratic1, cubic1)
closed_bez_path = Path(line1, quadratic1, cubic1, line2, line3)
inside_pt = 200+20j
outside_pt1 = 1000+1000j
outside_pt2 = 800+800j
boundary_pt = 50+50j
# Note: currently the intersect() method is not implemented for Arc
# objects
# arc1 = Arc(650+325j, 25+25j, -30.0, False, True, 700+300j)
# closed_path_with_arc = Path(line1, quadratic1, cubic1, arc1)
# self.assertTrue(
# path_encloses_pt(inside_pt, outside_pt2, closed_path_with_arc))
# True cases
self.assertTrue(
path_encloses_pt(inside_pt, outside_pt2, closed_bez_path))
self.assertTrue(
path_encloses_pt(boundary_pt, outside_pt2, closed_bez_path))
# False cases
self.assertFalse(
path_encloses_pt(outside_pt1, outside_pt2, closed_bez_path))
# Exception Cases
with self.assertRaises(AssertionError):
path_encloses_pt(inside_pt, outside_pt2, open_bez_path)
# Display test paths and points
# ns2d = [inside_pt, outside_pt1, outside_pt2, boundary_pt]
# ncolors = ['green', 'red', 'orange', 'purple']
# disvg(closed_path_with_arc, nodes=ns2d, node_colors=ncolors,
# openinbrowser=True)
# disvg(open_bez_path, nodes=ns2d, node_colors=ncolors,
# openinbrowser=True)
# disvg(closed_bez_path, nodes=ns2d, node_colors=ncolors,
# openinbrowser=True)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,30 @@
# External dependencies
from __future__ import division, absolute_import, print_function
import unittest
import numpy as np
# Internal dependencies
from svgpathtools import *
class Test_polytools(unittest.TestCase):
# def test_poly_roots(self):
# self.fail()
def test_rational_limit(self):
# (3x^3 + x)/(4x^2 - 2x) -> -1/2 as x->0
f = np.poly1d([3, 0, 1, 0])
g = np.poly1d([4, -2, 0])
lim = rational_limit(f, g, 0)
self.assertAlmostEqual(lim, -0.5)
# (3x^2)/(4x^2 - 2x) -> 0 as x->0
f = np.poly1d([3, 0, 0])
g = np.poly1d([4, -2, 0])
lim = rational_limit(f, g, 0)
self.assertAlmostEqual(lim, 0)
if __name__ == '__main__':
unittest.main()

25
test.svg Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" width="600px" version="1.1"
viewBox="-20.6525 -20.6525 366.305 366.305"
xmlns="http://www.w3.org/2000/svg">
<defs/>
<path d="M 10.5,80.0
C 40.0,10.0 65.0,10.0 95.0,80.0
C 125.0,150.0 150.0,150.0 180.0,80.0"
fill="none"
stroke="red"/>
<path d="m 84,92 c 10,10 30,10 40,0" fill="none" stroke="purple"/>
<path d="M 40.0,100.0
Q 100.0,50.0 65.0,20.0"
fill="none"
stroke="blue"/>
<path d="M 10.0,315.0
L 110.0,215.0
A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45
L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9
L 315.0,10.0"
fill="green"
fill-opacity="0.5"
stroke="black"
stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

21
vectorframes.svg Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="368px" version="1.1" viewBox="138.46883649 65.274611702 420.978169975 257.645495611" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M 175.0,162.5 L 175.0,236.805280985" id="tp0"/>
<path d="M 397.915842954,162.5 L 397.915842954,88.1947190152" id="tp1"/>
</defs>
<path d="M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0" fill="none" stroke="blue" stroke-width="0.347915842954"/>
<path d="M 175.0,162.5 L 175.0,236.805280985" fill="none" stroke="green" stroke-width="0.347915842954"/>
<path d="M 175.0,162.5 L 249.305280985,162.5" fill="none" stroke="pink" stroke-width="0.347915842954"/>
<path d="M 422.915842954,300.0 C 422.915842954,200.0 322.915842954,100.0 522.915842954,100.0" fill="none" stroke="black" stroke-width="0.347915842954"/>
<path d="M 397.915842954,162.5 L 397.915842954,88.1947190152" fill="none" stroke="green" stroke-width="0.347915842954"/>
<path d="M 397.915842954,162.5 L 323.61056197,162.5" fill="none" stroke="pink" stroke-width="0.347915842954"/>
<circle cx="175.0" cy="162.5" fill="#ff0000" r="1.73957921477"/>
<circle cx="397.915842954" cy="162.5" fill="#ff0000" r="1.73957921477"/>
<text font-size="12">
<textPath xlink:href="#tp0">b's tangent</textPath>
</text>
<text font-size="12">
<textPath xlink:href="#tp1">br's tangent</textPath>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB