diff --git a/.github/workflows/github-ci.yml b/.github/workflows/github-ci.yml index e4876c92..bf4fd1b3 100644 --- a/.github/workflows/github-ci.yml +++ b/.github/workflows/github-ci.yml @@ -29,6 +29,14 @@ jobs: pip install -r requirements.txt pip install scipy + # Lint: black and flake8 + - name: Install lint dependencies + run: pip install .[lint] + - name: Run black (check only) + run: black --check . + - name: Run flake8 + run: flake8 . + # find and run all unit tests - name: Run unit tests run: python -m unittest discover test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b35c6d7e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/psf/black + rev: 24.4.2 # Use the latest stable version or pin as needed + hooks: + - id: black + language_version: python3 + exclude: '(^|/)(README\.md|.*\.svg|.*\.html)$' + args: ["--line-length=120", "--target-version=py38"] + - repo: https://github.com/pycqa/flake8 + rev: 7.0.0 # Use the latest stable version or pin as needed + hooks: + - id: flake8 + language_version: python3 + exclude: '(^|/)(README\.md|.*\.svg|.*\.html)$' + args: ["--max-line-length=120", "--extend-ignore=E203,W503"] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a8b059e..8599afab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,65 +1,65 @@ # Contributing to svgpathtools -The following is a few and guidelines regarding the current philosophy, style, -flaws, and the future directions of svgpathtools. These guidelines are meant +The following is a few and guidelines regarding the current philosophy, style, +flaws, and the future directions of svgpathtools. These guidelines are meant to make it easy to contribute. ## Being a Hero -We need better automated testing coverage. Please, submit unittests! See the +We need better automated testing coverage. Please, submit unittests! See the Testing Style section below for info. Here's a list of things that need (more) unittests: * TBA (feel free to help) ## Submitting Bugs -If you find a bug, please submit an issue along with an **easily reproducible +If you find a bug, please submit an issue along with an **easily reproducible example**. Feel free to make a pull-request too (see relevant section below). -## Submitting Pull-Requests +## Submitting Pull-Requests #### New features come with unittests and docstrings. -If you want to add a cool/useful feature to svgpathtools, that's great! Just -make sure your pull-request includes both thorough unittests and well-written -docstrings. See relevant sections below on "Testing Style" and +If you want to add a cool/useful feature to svgpathtools, that's great! Just +make sure your pull-request includes both thorough unittests and well-written +docstrings. See relevant sections below on "Testing Style" and "Docstring Style" below. #### Modifications to old code may require additional unittests. -Certain submodules of svgpathtools are poorly covered by the current set of -unittests. That said, most functionality in svgpathtools has been tested quite +Certain submodules of svgpathtools are poorly covered by the current set of +unittests. That said, most functionality in svgpathtools has been tested quite a bit through use. -The point being, if you're working on functionality not currently covered by -unittests (and your changes replace more than a few lines), then please include +The point being, if you're working on functionality not currently covered by +unittests (and your changes replace more than a few lines), then please include unittests designed to verify that any affected functionary still works. -## Style +## Style ### Coding Style -* Follow the PEP8 guidelines unless you have good reason to violate them (e.g. -you want your code's variable names to match some official documentation, or +* Follow the PEP8 guidelines unless you have good reason to violate them (e.g. +you want your code's variable names to match some official documentation, or PEP8 guidelines contradict those present in this document). -* Include docstrings and in-line comments where appropriate. See +* Include docstrings and in-line comments where appropriate. See "Docstring Style" section below for more info. -* Use explicit, uncontracted names (e.g. `parse_transform` instead of -`parse_trafo`). Maybe the most important feature for a name is how easy it is +* Use explicit, uncontracted names (e.g. `parse_transform` instead of +`parse_trafo`). Maybe the most important feature for a name is how easy it is for a user to guess (after having seen other names used in `svgpathtools`). -* Use a capital 'T' denote a Path object's parameter, use a lower case 't' to -denote a Path segment's parameter. See the methods `Path.t2T` and `Path.T2t` -if you're unsure what I mean. In the ambiguous case, use either 't' or another -appropriate option (e.g. "tau"). +* Use a capital 'T' denote a Path object's parameter, use a lower case 't' to +denote a Path segment's parameter. See the methods `Path.t2T` and `Path.T2t` +if you're unsure what I mean. In the ambiguous case, use either 't' or another +appropriate option (e.g. "tau"). ### Testing Style -You want to submit unittests?! Yes! Please see the svgpathtools/test folder +You want to submit unittests?! Yes! Please see the svgpathtools/test folder for examples. ### Docstring Style -All docstrings in svgpathtools should (roughly) adhere to the Google Python -Style Guide. Currently, this is not the case... but for the sake of -consistency, Google Style is the officially preferred docstring style of -svgpathtools. +All docstrings in svgpathtools should (roughly) adhere to the Google Python +Style Guide. Currently, this is not the case... but for the sake of +consistency, Google Style is the officially preferred docstring style of +svgpathtools. [Some nice examples of Google Python Style docstrings]( https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) diff --git a/LICENSE.txt b/LICENSE.txt index 5c7c4382..247a8660 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -18,4 +18,4 @@ 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. \ No newline at end of file +SOFTWARE. diff --git a/LICENSE2.txt b/LICENSE2.txt index 6a9dbcf0..a46b4b02 100644 --- a/LICENSE2.txt +++ b/LICENSE2.txt @@ -18,4 +18,4 @@ 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. \ No newline at end of file +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index b7d0cbfa..3649022d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include *.svg LICENSE* -recursive-include test *.svg \ No newline at end of file +recursive-include test *.svg diff --git a/README.md b/README.md index 8cee731f..5b68d893 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Some included tools: ```bash $ pip install svgpathtools -``` - -### Alternative Setup +``` + +### Alternative Setup You can download the source from Github and install by using the command (from inside the folder containing setup.py): ```bash @@ -130,7 +130,7 @@ 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 +# 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) @@ -141,7 +141,7 @@ print(spath) # The following commands will open two browser windows to display path and spaths from svgpathtools import disvg from time import sleep -disvg(path) +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." @@ -169,12 +169,12 @@ print("Notice that path contains {} segments and spath contains {} 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. +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. ```python -# Read SVG into a list of path objects and list of dictionaries of attributes +# Read SVG into a list of path objects and list of dictionaries of attributes from svgpathtools import svg2paths, wsvg paths, attributes = svg2paths('test.svg') @@ -184,7 +184,7 @@ from svgpathtools import svg2paths2 paths, attributes, svg_attributes = svg2paths2('test.svg') # Let's print out the first path object and the color it was in the SVG -# We'll see it is composed of two CubicBezier objects and, in the SVG file it +# 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] @@ -217,27 +217,27 @@ 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)``. +**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. ```python # Example: -# Let's check that the first segment of redpath starts +# Let's check that the first segment of redpath starts # at the same point as redpath -firstseg = redpath[0] +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] +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 +# 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)) @@ -250,21 +250,21 @@ print(redpath[k].point(t) == redpath.point(0.5)) ### 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. +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 $$\mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3$$ -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 +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 $$\mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3$$ where -$$\begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} = +$$\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}$$ +\begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix}$$ `QuadraticBezier.poly()` and `Line.poly()` are [defined similarly](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#General_definition). @@ -277,34 +277,34 @@ 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. +# 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" + +print("The CubicBezier, b.point(x) = \n\n" + + bpretty + "\n\n" + "can be rewritten in standard form as \n\n" + str(p).replace('x','t')) ``` True - The CubicBezier, b.point(x) = - + 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 - + + can be rewritten in standard form as + 3 2 (-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j) -The ability to convert between Bezier objects to NumPy polynomial objects is very useful. For starters, we can take turn a list of Bézier segments into a NumPy array +The ability to convert between Bezier objects to NumPy polynomial objects is very useful. For starters, we can take turn a list of Bézier segments into a NumPy array -### Numpy Array operations on Bézier path segments +### Numpy Array operations on Bézier path segments -[Example available here](https://github.com/mathandy/svgpathtools/blob/master/examples/compute-many-points-quickly-using-numpy-arrays.py) +[Example available here](https://github.com/mathandy/svgpathtools/blob/master/examples/compute-many-points-quickly-using-numpy-arrays.py) -To further illustrate the power of being able to convert our Bezier curve objects to numpy.poly1d objects and back, lets compute the unit tangent vector of the above CubicBezier object, b, at t=0.5 in four different ways. +To further illustrate the power of being able to convert our Bezier curve objects to numpy.poly1d objects and back, lets compute the unit tangent vector of the above CubicBezier object, b, at t=0.5 in four different ways. -### Tangent vectors (and more on NumPy polynomials) +### Tangent vectors (and more on NumPy polynomials) ```python @@ -312,22 +312,22 @@ t = 0.5 ### Method 1: the easy way u1 = b.unit_tangent(t) -### Method 2: another easy way +### 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 +### Method 2: a third easy way # Note: This way will also fail if it encounters a removable singularity. -dp = p.deriv() +dp = p.deriv() u3 = dp(t)/abs(dp(t)) -### Method 4: the removable-singularity-proof numpy.poly1d way +### 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 +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))] == +# 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)) @@ -352,19 +352,19 @@ 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! +# 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 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()] # +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', +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]) ``` @@ -385,7 +385,7 @@ 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) @@ -404,8 +404,8 @@ Here we'll create an SVG that shows off the parametric and geometric midpoints o 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 +# 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 @@ -429,7 +429,7 @@ for path in paths: nradii += [5] * 2 # In 'output2.svg' the paths will retain their original attributes -wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, +wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, attributes=attributes, filename='output2.svg') ``` @@ -439,14 +439,14 @@ wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, ```python -# Let's find all intersections between redpath and the other +# 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)) ``` @@ -461,7 +461,7 @@ Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) from svgpathtools import parse_path, Line, Path, wsvg def offset_curve(path, offset_distance, steps=1000): """Takes in a Path object, `path`, and a distance, - `offset_distance`, and outputs an piecewise-linear approximation + `offset_distance`, and outputs an piecewise-linear approximation of the 'parallel' offset curve.""" nls = [] for seg in path: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..4f677ef4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "svgpathtools" +description = "A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves." +readme = "README.md" +authors = [ + { name = "Andy Port", email = "AndyAPort@gmail.com" } +] +license = { file = "LICENSE.txt" } +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Image Recognition", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: Libraries :: Python Modules" +] +keywords = ["svg", "svg path", "svg.path", "bezier", "parse svg path", "display svg"] +urls = { "Homepage" = "https://github.com/mathandy/svgpathtools" } +dependencies = [ + "numpy", + "svgwrite", + "scipy" +] +dynamic = ["version"] + +[project.optional-dependencies] +dev = [ + "black~=24.4", + "flake8~=7.0", + "pre-commit~=3.7" +] +lint = [ + "black~=24.4", + "flake8~=7.0" +] + +[tool.setuptools.packages.find] +where = ["svgpathtools"] +include = ["svgpathtools*"] + +[tool.setuptools] +package-dir = {"" = "."} + +[tool.setuptools.dynamic] +version = {attr = "svgpathtools.__version__"} + +[tool.black] +line-length = 120 +target-version = ['py38'] +exclude = ''' +( + (?x) + (^|/)README\.md$ + | \.svg$ + | \.html$ +) +''' + +[tool.flake8] +max-line-length = 120 +extend-ignore = ["E203", "W503"] +exclude = [ + "README.md", + "*.svg", + "*.html", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 7a38d72c..00000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bdist_wheel] -universal = 1 - -[metadata] -license_file = LICENSE.txt \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index ddb2f39a..00000000 --- a/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -from setuptools import setup -import codecs -import os - - -VERSION = '1.7.0' -AUTHOR_NAME = 'Andy Port' -AUTHOR_EMAIL = 'AndyAPort@gmail.com' -GITHUB = 'https://github.com/mathandy/svgpathtools' - -_here = os.path.abspath(os.path.dirname(__file__)) - - -def read(relative_path): - """Reads file at relative path, returning contents as string.""" - with codecs.open(os.path.join(_here, relative_path), "rb", "utf-8") as f: - return f.read() - - -setup(name='svgpathtools', - packages=['svgpathtools'], - version=VERSION, - description=('A collection of tools for manipulating and analyzing SVG ' - 'Path objects and Bezier curves.'), - long_description=read("README.md"), - long_description_content_type='text/markdown', - author=AUTHOR_NAME, - author_email=AUTHOR_EMAIL, - url=GITHUB, - download_url='{}/releases/download/{}/svgpathtools-{}-py3-none-any.whl' - ''.format(GITHUB, VERSION, VERSION), - license='MIT', - install_requires=['numpy', 'svgwrite', 'scipy'], - python_requires='>=3.8', - platforms="OS Independent", - keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'], - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Image Recognition", - "Topic :: Scientific/Engineering :: Information Analysis", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Scientific/Engineering :: Visualization", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - ) diff --git a/svgpathtools/__init__.py b/svgpathtools/__init__.py index 7e5da654..19fc9023 100644 --- a/svgpathtools/__init__.py +++ b/svgpathtools/__init__.py @@ -1,19 +1,40 @@ -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, polygon, polyline) +# flake8: noqa +__version__ = "1.7.0" +from .bezier import ( + bezier_point, + bezier2polynomial, + polynomial2bezier, + split_bezier, + bezier_bounding_box, + bezier_intersections, + bezier_by_line_intersections, +) +from .path import ( + Path, + Line, + QuadraticBezier, + CubicBezier, + Arc, + bezier_segment, + is_bezier_segment, + is_path_segment, + is_bezier_path, + concatpaths, + poly2bez, + bpoints2bezier, + closest_point_in_path, + farthest_point_in_path, + path_encloses_pt, + bbox2path, + polygon, + polyline, +) from .parser import parse_path from .paths2svg import disvg, wsvg, paths2Drawing from .polytools import polyroots, polyroots01, rational_limit, real, imag from .misctools import hex2rgb, rgb2hex from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks -from .document import (Document, CONVERSIONS, CONVERT_ONLY_PATHS, - SVG_GROUP_TAG, SVG_NAMESPACE) +from .document import Document, CONVERSIONS, CONVERT_ONLY_PATHS, SVG_GROUP_TAG, SVG_NAMESPACE from .svg_io_sax import SaxDocument try: diff --git a/test/test_svg2paths.py b/test/test_svg2paths.py index 2caae616..fc0ba304 100644 --- a/test/test_svg2paths.py +++ b/test/test_svg2paths.py @@ -15,64 +15,67 @@ class TestSVG2Paths(unittest.TestCase): def test_svg2paths_polygons(self): - paths, _ = svg2paths(join(dirname(__file__), 'polygons.svg')) + paths, _ = svg2paths(join(dirname(__file__), "polygons.svg")) # triangular polygon test path = paths[0] - path_correct = Path(Line(55.5+0j, 55.5+50j), - Line(55.5+50j, 105.5+50j), - Line(105.5+50j, 55.5+0j) - ) + path_correct = Path(Line(55.5 + 0j, 55.5 + 50j), Line(55.5 + 50j, 105.5 + 50j), Line(105.5 + 50j, 55.5 + 0j)) self.assertTrue(path.isclosed()) - self.assertTrue(len(path)==3) - self.assertTrue(path==path_correct) + self.assertTrue(len(path) == 3) + self.assertTrue(path == path_correct) # triangular quadrilateral (with a redundant 4th "closure" point) path = paths[1] - path_correct = Path(Line(0+0j, 0-100j), - Line(0-100j, 0.1-100j), - Line(0.1-100j, 0+0j), - Line(0+0j, 0+0j) # result of redundant point - ) + path_correct = Path( + Line(0 + 0j, 0 - 100j), + Line(0 - 100j, 0.1 - 100j), + Line(0.1 - 100j, 0 + 0j), + Line(0 + 0j, 0 + 0j), # result of redundant point + ) self.assertTrue(path.isclosed()) - self.assertTrue(len(path)==4) - self.assertTrue(path==path_correct) + self.assertTrue(len(path) == 4) + self.assertTrue(path == path_correct) def test_svg2paths_ellipses(self): - paths, _ = svg2paths(join(dirname(__file__), 'ellipse.svg')) + paths, _ = svg2paths(join(dirname(__file__), "ellipse.svg")) # ellipse tests path_ellipse = paths[0] - path_ellipse_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j), - Arc(150+100j, 50+50j, 0.0, True, False, 50+100j)) - self.assertTrue(len(path_ellipse)==2) - self.assertTrue(path_ellipse==path_ellipse_correct) + path_ellipse_correct = Path( + Arc(50 + 100j, 50 + 50j, 0.0, True, False, 150 + 100j), + Arc(150 + 100j, 50 + 50j, 0.0, True, False, 50 + 100j), + ) + self.assertTrue(len(path_ellipse) == 2) + self.assertTrue(path_ellipse == path_ellipse_correct) self.assertTrue(path_ellipse.isclosed()) # circle tests - paths, _ = svg2paths(join(dirname(__file__), 'circle.svg')) + paths, _ = svg2paths(join(dirname(__file__), "circle.svg")) path_circle = paths[0] - path_circle_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j), - Arc(150+100j, 50+50j, 0.0, True, False, 50+100j)) - self.assertTrue(len(path_circle)==2) - self.assertTrue(path_circle==path_circle_correct) + path_circle_correct = Path( + Arc(50 + 100j, 50 + 50j, 0.0, True, False, 150 + 100j), + Arc(150 + 100j, 50 + 50j, 0.0, True, False, 50 + 100j), + ) + self.assertTrue(len(path_circle) == 2) + self.assertTrue(path_circle == path_circle_correct) self.assertTrue(path_circle.isclosed()) # test for issue #198 (circles not being closed) - svg = u""" - + - + - """ + """ # noqa: E501 tmpdir = tempfile.mkdtemp() - svgfile = os.path.join(tmpdir, 'test.svg') - with open(svgfile, 'w') as f: + svgfile = os.path.join(tmpdir, "test.svg") + with open(svgfile, "w") as f: f.write(svg) paths, _ = svg2paths(svgfile) self.assertEqual(len(paths), 2) @@ -86,7 +89,7 @@ def test_rect2pathd(self): rect2pathd(non_rounded_dict), "M10.0 10.0 L 110.0 10.0 L 110.0 110.0 L 10.0 110.0 z", ) - + # flake8: noqa: E501 non_rounded_svg = """ @@ -104,10 +107,17 @@ def test_rect2pathd(self): "M 10.0,10.0 L 110.0,10.0 L 110.0,110.0 L 10.0,110.0 L 10.0,10.0", ) - rounded_dict = {"x": "10", "y": "10", "width": "100","height": "100", "rx": "15", "ry": "12"} + rounded_dict = {"x": "10", "y": "10", "width": "100", "height": "100", "rx": "15", "ry": "12"} self.assertEqual( rect2pathd(rounded_dict), - "M 25.0 10.0 L 95.0 10.0 A 15.0 12.0 0 0 1 110.0 22.0 L 110.0 98.0 A 15.0 12.0 0 0 1 95.0 110.0 L 25.0 110.0 A 15.0 12.0 0 0 1 10.0 98.0 L 10.0 22.0 A 15.0 12.0 0 0 1 25.0 10.0 z", + "M 25.0 10.0 L 95.0 10.0 " + "A 15.0 12.0 0 0 1 110.0 22.0 " + "L 110.0 98.0 " + "A 15.0 12.0 0 0 1 95.0 110.0 " + "L 25.0 110.0 " + "A 15.0 12.0 0 0 1 10.0 98.0 " + "L 10.0 22.0 " + "A 15.0 12.0 0 0 1 25.0 10.0 z", ) rounded_svg = """ @@ -120,12 +130,20 @@ def test_rect2pathd(self): self.assertTrue(paths[0].isclosed()) self.assertEqual( paths[0].d(), - "M 25.0,10.0 L 95.0,10.0 A 15.0,12.0 0.0 0,1 110.0,22.0 L 110.0,98.0 A 15.0,12.0 0.0 0,1 95.0,110.0 L 25.0,110.0 A 15.0,12.0 0.0 0,1 10.0,98.0 L 10.0,22.0 A 15.0,12.0 0.0 0,1 25.0,10.0", + "M 25.0,10.0 " + "L 95.0,10.0 " + "A 15.0,12.0 0.0 0,1 110.0,22.0 " + "L 110.0,98.0 " + "A 15.0,12.0 0.0 0,1 95.0,110.0 " + "L 25.0,110.0 " + "A 15.0,12.0 0.0 0,1 10.0,98.0 " + "L 10.0,22.0 " + "A 15.0,12.0 0.0 0,1 25.0,10.0", ) def test_from_file_path_string(self): """Test reading svg from file provided as path""" - paths, _ = svg2paths(join(dirname(__file__), 'polygons.svg')) + paths, _ = svg2paths(join(dirname(__file__), "polygons.svg")) self.assertEqual(len(paths), 2) @@ -133,21 +151,21 @@ def test_from_file_path(self): """Test reading svg from file provided as pathlib POSIXPath""" if version_info >= (3, 6): import pathlib - paths, _ = svg2paths(pathlib.Path(__file__).parent / 'polygons.svg') + + paths, _ = svg2paths(pathlib.Path(__file__).parent / "polygons.svg") self.assertEqual(len(paths), 2) def test_from_file_object(self): """Test reading svg from file object that has already been opened""" - with open(join(dirname(__file__), 'polygons.svg'), 'r') as file: + with open(join(dirname(__file__), "polygons.svg"), "r") as file: paths, _ = svg2paths(file) self.assertEqual(len(paths), 2) def test_from_stringio(self): """Test reading svg object contained in a StringIO object""" - with open(join(dirname(__file__), 'polygons.svg'), - 'r', encoding='utf-8') as file: + with open(join(dirname(__file__), "polygons.svg"), "r", encoding="utf-8") as file: # read entire file into string file_content = file.read() # prepare stringio object @@ -159,8 +177,7 @@ def test_from_stringio(self): def test_from_string(self): """Test reading svg object contained in a string""" - with open(join(dirname(__file__), 'polygons.svg'), - 'r', encoding='utf-8') as file: + with open(join(dirname(__file__), "polygons.svg"), "r", encoding="utf-8") as file: # read entire file into string file_content = file.read() @@ -169,5 +186,5 @@ def test_from_string(self): self.assertEqual(len(paths), 2) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()