Compare commits

..

20 Commits

Author SHA1 Message Date
Andrew Port 67fd6e885d remove error-causing type check 2021-10-18 22:36:24 -07:00
Andrew Port f2eb3d0596 update official python version support 2021-10-18 22:23:13 -07:00
Andrew Port 2422d15251 update 2021-10-18 21:24:36 -07:00
Andrew Port 2368627a17 update 2021-09-23 02:18:32 -07:00
Andrew Port 0c5dc9de1a fixed ElementTree issue in python3 2021-09-23 00:59:10 -07:00
Andrew Port 657a9d6745 updated from master 2021-09-23 00:22:55 -07:00
Andrew Port 3e1f8e00a5 minor cleanup 2021-09-23 00:21:41 -07:00
Andrew Port 05408cfa26 update (tests not passing in python2, need to investigate) 2021-09-22 21:17:35 -07:00
Andrew Port e71d2d4282 remove ambiguous except blocks 2021-09-21 02:49:00 -07:00
Andrew Port 413a2864f6 cleanup to avoid linting issues 2021-09-21 02:44:39 -07:00
Andrew Port a2b62fc011 add docstring 2021-09-21 02:40:35 -07:00
Andrew Port d86c63214b clean up docstrings 2021-09-21 02:34:34 -07:00
Andrew Port d2b1ea5770 make imports explicit 2021-09-21 02:22:12 -07:00
Andrew Port da050a2eeb replace xml parsers with defusedxml versions 2021-09-21 01:54:58 -07:00
Andrew Port 0a31f348d6 remove warning 2021-09-21 01:54:25 -07:00
Andrew Port 9863e7050a add python2 compatible warning check for closed property 2021-09-21 01:54:11 -07:00
Andrew Port 11682a3363 suppress unneeded numpy warnings from QuadraticBezier.length() 2021-09-21 01:39:25 -07:00
Andrew Port 4f615f9a9d replace ambiguous except block with if-statement 2021-09-20 23:41:55 -07:00
Andrew Port ace8522c19 fix most linter warnings in test_path.py 2021-09-20 23:41:15 -07:00
Andrew Port d881b21b47 remove unused import 2021-09-20 22:40:38 -07:00
28 changed files with 358 additions and 729 deletions

View File

@ -13,7 +13,10 @@ name: "CodeQL"
on: on:
push: push:
branches: [ master ]
pull_request: pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule: schedule:
- cron: '30 2 * * 3' - cron: '30 2 * * 3'
@ -30,6 +33,9 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'python' ] language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@ -1,34 +0,0 @@
name: Github CI Unit Testing for Legacy Environments
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
matrix:
os: [ubuntu-18.04, macos-10.15, windows-2019]
python-version: [2.7, 3.5, 3.6]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# configure python
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
# install deps
- name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }}
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install scipy
# find and run all unit tests
- name: Run unit tests
run: python -m unittest discover test

View File

@ -12,7 +12,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -1,37 +0,0 @@
name: Publish to TestPyPI
on:
push:
branches:
- master
jobs:
build-n-publish:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3
uses: actions/setup-python@v1
with:
python-version: 3
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip_existing: true
password: ${{ secrets.TESTPYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/

View File

@ -1,14 +1,12 @@
name: Publish to PyPI if new version name: Publish to TestPyPI and if new version PyPI
on: on:
push: push:
tags: branches: [ master ]
- 'v*'
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish to TestPyPI and PyPI name: Build and publish to TestPyPI and PyPI
runs-on: ubuntu-latest runs-on: ubuntu-18.04
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Set up Python 3 - name: Set up Python 3
@ -30,13 +28,13 @@ jobs:
--outdir dist/ --outdir dist/
. .
- name: Publish to Test PyPI - name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@master
with: with:
skip_existing: true skip_existing: true
password: ${{ secrets.TESTPYPI_API_TOKEN }} password: ${{ secrets.TESTPYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/ repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags') if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@master
with: with:
password: ${{ secrets.PYPI_API_TOKEN }} password: ${{ secrets.PYPI_API_TOKEN }}

View File

@ -4,10 +4,6 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD)\n",
"![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)\n",
"[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)\n",
"[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)\n",
"# svgpathtools\n", "# svgpathtools\n",
"\n", "\n",
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n", "svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n",
@ -40,15 +36,25 @@
"## Prerequisites\n", "## Prerequisites\n",
"- **numpy**\n", "- **numpy**\n",
"- **svgwrite**\n", "- **svgwrite**\n",
"- **scipy** (optional but recommended for performance)\n",
"\n", "\n",
"## Setup\n", "## Setup\n",
"\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", "```bash\n",
"$ pip install svgpathtools\n", "$ pip install svgpathtools\n",
"``` \n", "``` \n",
" \n", " \n",
"### Alternative Setup\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", "You can download the source from Github and install by using the command (from inside the folder containing setup.py):\n",
"\n", "\n",
"```bash\n", "```bash\n",
@ -720,4 +726,4 @@
}, },
"nbformat": 4, "nbformat": 4,
"nbformat_minor": 1 "nbformat_minor": 1
} }

View File

@ -1,6 +1,7 @@
[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD) [![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD)
![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg) ![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)
[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/) [![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)
![Build](https://img.shields.io/github/workflow/status/mathandy/svgpathtools/Github%20CI%20Unit%20Testing)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools) [![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)
# svgpathtools # svgpathtools
@ -35,10 +36,20 @@ Some included tools:
## Prerequisites ## Prerequisites
- **numpy** - **numpy**
- **svgwrite** - **svgwrite**
- **scipy** (optional, but recommended for performance)
## Setup ## Setup
If not already installed, you can **install the prerequisites** using pip.
```bash
$ pip install numpy
```
```bash
$ pip install svgwrite
```
Then **install svgpathtools**:
```bash ```bash
$ pip install svgpathtools $ pip install svgpathtools
``` ```

View File

@ -1,17 +1,23 @@
"""The goal of this gist is to show how to compute many points on a path """ An example of how to speed up point() calculations with vectorization.
The goal of this gist is to show how to compute many points on a path
quickly using NumPy arrays. I.e. there's a much faster way than using, say quickly using NumPy arrays. I.e. there's a much faster way than using, say
[some_path.point(t) for t in many_tvals]. The example below assumes the [some_path.point(t) for t in many_tvals]. The example below assumes the
`Path` object is composed entirely of `CubicBezier` objects, but this can `Path` object is composed entirely of `CubicBezier` objects, but this can
easily be generalized to paths containing `Line` and `QuadraticBezier` objects easily be generalized to paths containing `Line` and `QuadraticBezier` objects
also. also.
Note: The relevant matrix transformation for quadratics can be found in the Note: The relevant matrix transformation for quadratics can be found in the
svgpathtools.bezier module.""" svgpathtools.bezier module.
"""
from __future__ import print_function from __future__ import print_function
import numpy as np import numpy as np
from svgpathtools import bezier_point, Path, bpoints2bezier, polynomial2bezier from svgpathtools import bezier_point, bpoints2bezier, polynomial2bezier, Path
class HigherOrderBezier: class HigherOrderBezier:
"""Bezier curve of arbitrary degree"""
def __init__(self, bpoints): def __init__(self, bpoints):
self.bpts = bpoints self.bpts = bpoints
@ -38,7 +44,7 @@ def points_in_each_seg_slow(path, tvals):
def points_in_each_seg(path, tvals): def points_in_each_seg(path, tvals):
"""Compute seg.point(t) for each seg in path and each t in tvals.""" """Compute seg.point(t) for each seg in path and each t in tvals."""
A = np.array([[-1, 3, -3, 1], # transforms cubic bez to standard poly A = np.array([[-1, 3, -3, 1], # transforms cubic bez to standard poly
[ 3, -6, 3, 0], [ 3, -6, 3, 0],
[-3, 3, 0, 0], [-3, 3, 0, 0],
[ 1, 0, 0, 0]]) [ 1, 0, 0, 0]])

View File

@ -7,8 +7,7 @@ Path.continuous_subpaths() method to split a paths into a list of its
continuous subpaths. continuous subpaths.
""" """
from svgpathtools import Path, Line from svgpathtools import *
def path1_is_contained_in_path2(path1, path2): def path1_is_contained_in_path2(path1, path2):
assert path2.isclosed() # This question isn't well-defined otherwise assert path2.isclosed() # This question isn't well-defined otherwise
@ -17,11 +16,11 @@ def path1_is_contained_in_path2(path1, path2):
# find a point that's definitely outside path2 # find a point that's definitely outside path2
xmin, xmax, ymin, ymax = path2.bbox() xmin, xmax, ymin, ymax = path2.bbox()
b = (xmin + 1) + 1j*(ymax + 1) B = (xmin + 1) + 1j*(ymax + 1)
a = path1.start # pick an arbitrary point in path1 A = path1.start # pick an arbitrary point in path1
ab_line = Path(Line(a, b)) AB_line = Path(Line(A, B))
number_of_intersections = len(ab_line.intersect(path2)) number_of_intersections = len(AB_line.intersect(path2))
if number_of_intersections % 2: # if number of intersections is odd if number_of_intersections % 2: # if number of intersections is odd
return True return True
else: else:

View File

@ -1,16 +1,13 @@
from svgpathtools import disvg, Line, CubicBezier from svgpathtools import *
from scipy.optimize import fminbound
# create some example paths # create some example paths
path1 = CubicBezier(1,2+3j,3-5j,4+1j) path1 = CubicBezier(1,2+3j,3-5j,4+1j)
path2 = path1.rotated(60).translated(3) path2 = path1.rotated(60).translated(3)
# find minimizer
from scipy.optimize import fminbound
def dist(t): def dist(t):
return path1.radialrange(path2.point(t))[0][0] return path1.radialrange(path2.point(t))[0][0]
# find minimizer
T2 = fminbound(dist, 0, 1) T2 = fminbound(dist, 0, 1)
# Let's do a visual check # Let's do a visual check

View File

@ -1,3 +1,3 @@
numpy numpy
svgwrite svgwrite
scipy defusedxml

View File

@ -3,7 +3,7 @@ import codecs
import os import os
VERSION = '1.6.1' VERSION = '1.4.2'
AUTHOR_NAME = 'Andy Port' AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = 'AndyAPort@gmail.com' AUTHOR_EMAIL = 'AndyAPort@gmail.com'
GITHUB = 'https://github.com/mathandy/svgpathtools' GITHUB = 'https://github.com/mathandy/svgpathtools'
@ -30,8 +30,9 @@ setup(name='svgpathtools',
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl' download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
''.format(GITHUB, VERSION, VERSION), ''.format(GITHUB, VERSION, VERSION),
license='MIT', license='MIT',
install_requires=['numpy', 'svgwrite', 'scipy'], install_requires=['numpy', 'svgwrite', 'defusedxml'],
platforms="OS Independent", platforms="OS Independent",
requires=['numpy', 'svgwrite', 'defusedxml'],
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'], keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
@ -46,8 +47,6 @@ setup(name='svgpathtools',
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
"Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Recognition", "Topic :: Scientific/Engineering :: Image Recognition",

View File

@ -17,6 +17,6 @@ from .document import (Document, CONVERSIONS, CONVERT_ONLY_PATHS,
from .svg_io_sax import SaxDocument from .svg_io_sax import SaxDocument
try: try:
from .svg_to_paths import svg2paths, svg2paths2, svgstr2paths from .svg_to_paths import svg2paths, svg2paths2
except ImportError: except ImportError:
pass pass

View File

@ -31,13 +31,9 @@ def bezier_point(p, t):
Warning: Be concerned about numerical stability when using this function Warning: Be concerned about numerical stability when using this function
with high order curves.""" with high order curves."""
# begin arc support block ######################## # for Arc support
try: if hasattr(p, 'radius'):
p.large_arc
return p.point(t) return p.point(t)
except:
pass
# end arc support block ##########################
deg = len(p) - 1 deg = len(p) - 1
if deg == 3: if deg == 3:
@ -145,14 +141,11 @@ def split_bezier(bpoints, t):
def halve_bezier(p): def halve_bezier(p):
"""split path segment into two halves at t=0.5"""
# begin arc support block ######################## # for Arc support
try: if hasattr(p, 'radius'):
p.large_arc
return p.split(0.5) return p.split(0.5)
except:
pass
# end arc support block ##########################
if len(p) == 4: if len(p) == 4:
return ([p[0], (p[0] + p[1])/2, (p[0] + 2*p[1] + p[2])/4, return ([p[0], (p[0] + p[1])/2, (p[0] + 2*p[1] + p[2])/4,
@ -199,13 +192,9 @@ def bezier_bounding_box(bez):
(xmin, xmax, ymin, ymax). (xmin, xmax, ymin, ymax).
Warning: For the non-cubic case this is not particularly efficient.""" Warning: For the non-cubic case this is not particularly efficient."""
# begin arc support block ######################## # for Arc support
try: if hasattr(bez, 'radius'):
bla = bez.large_arc return bez.bbox()
return bez.bbox() # added to support Arc objects
except:
pass
# end arc support block ##########################
if len(bez) == 4: if len(bez) == 4:
xmin, xmax = bezier_real_minmax([p.real for p in bez]) xmin, xmax = bezier_real_minmax([p.real for p in bez])

View File

@ -13,7 +13,7 @@ An Historic Note:
Example: Example:
Typical usage looks something like the following. Typical usage looks something like the following.
>> from svgpathtools import Document >> from svgpathtools import *
>> doc = Document('my_file.html') >> doc = Document('my_file.html')
>> for path in doc.paths(): >> for path in doc.paths():
>> # Do something with the transformed Path object. >> # Do something with the transformed Path object.
@ -36,15 +36,18 @@ A Big Problem:
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import os import os
import sys
import collections import collections
import xml.etree.ElementTree as etree from defusedxml.cElementTree import parse, tostring
from xml.etree.ElementTree import Element, SubElement, register_namespace from xml.etree.cElementTree import register_namespace
from xml.dom.minidom import parseString if sys.version_info.major == 2:
from xml.etree.ElementTree import Element, SubElement, ElementTree
else:
from xml.etree.cElementTree import Element, SubElement, ElementTree
from defusedxml.minidom import parseString
import warnings import warnings
from io import StringIO
from tempfile import gettempdir from tempfile import gettempdir
from time import time from time import time
import numpy as np
# Internal dependencies # Internal dependencies
from .parser import parse_path from .parser import parse_path
@ -52,17 +55,13 @@ from .parser import parse_transform
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd, from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
polyline2pathd, polygon2pathd, rect2pathd) polyline2pathd, polygon2pathd, rect2pathd)
from .misctools import open_in_browser from .misctools import open_in_browser
from .path import transform, Path, is_path_segment from .path import *
# To maintain forward/backward compatibility # To maintain forward/backward compatibility
try: try:
string = basestring str = basestring
except NameError: except NameError:
string = str pass
try:
from os import PathLike
except ImportError:
PathLike = string
# Let xml.etree.ElementTree know about the SVG namespace # Let xml.etree.ElementTree know about the SVG namespace
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'} SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
@ -103,9 +102,6 @@ def flattened_paths(group, group_filter=lambda x: True,
only convert explicit path elements, pass in only convert explicit path elements, pass in
`path_conversions=CONVERT_ONLY_PATHS`. `path_conversions=CONVERT_ONLY_PATHS`.
""" """
if not isinstance(group, Element):
raise TypeError('Must provide an xml.etree.Element object. '
'Instead you provided {0}'.format(type(group)))
# Stop right away if the group_selector rejects this group # Stop right away if the group_selector rejects this group
if not group_filter(group): if not group_filter(group):
@ -241,31 +237,22 @@ class Document:
The output Path objects will be transformed based on their parent groups. The output Path objects will be transformed based on their parent groups.
Args: Args:
filepath (str or file-like): The filepath of the filepath (str): The filepath of the DOM-style object.
DOM-style object or a file-like object containing it.
""" """
# strings are interpreted as file location everything else is treated as # remember location of original svg file
# file-like object and passed to the xml parser directly self.original_filepath = filepath
from_filepath = isinstance(filepath, string) or isinstance(filepath, PathLike) if filepath is not None and os.path.dirname(filepath) == '':
self.original_filepath = os.path.abspath(filepath) if from_filepath else None self.original_filepath = os.path.join(os.getcwd(), filepath)
if filepath is None: if filepath is None:
self.tree = etree.ElementTree(Element('svg')) self.tree = ElementTree(Element('svg'))
else: else:
# parse svg to ElementTree object # parse svg to ElementTree object
self.tree = etree.parse(filepath) self.tree = parse(filepath)
self.root = self.tree.getroot() self.root = self.tree.getroot()
@classmethod
def from_svg_string(cls, svg_string):
"""Constructor for creating a Document object from a string."""
# wrap string into StringIO object
svg_file_obj = StringIO(svg_string)
# create document from file object
return Document(svg_file_obj)
def paths(self, group_filter=lambda x: True, def paths(self, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS): path_filter=lambda x: True, path_conversions=CONVERSIONS):
"""Returns a list of all paths in the document. """Returns a list of all paths in the document.
@ -278,7 +265,7 @@ class Document:
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True, def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS): path_filter=lambda x: True, path_conversions=CONVERSIONS):
if all(isinstance(s, string) for s in group): if all(isinstance(s, str) for s in group):
# If we're given a list of strings, assume it represents a # If we're given a list of strings, assume it represents a
# nested sequence # nested sequence
group = self.get_group(group) group = self.get_group(group)
@ -304,7 +291,7 @@ class Document:
# If given a list of strings (one or more), assume it represents # If given a list of strings (one or more), assume it represents
# a sequence of nested group names # a sequence of nested group names
elif len(group) > 0 and all(isinstance(elem, str) for elem in group): elif all(isinstance(elem, str) for elem in group):
group = self.get_or_add_group(group) group = self.get_or_add_group(group)
elif not isinstance(group, Element): elif not isinstance(group, Element):
@ -323,7 +310,7 @@ class Document:
path_svg = path.d() path_svg = path.d()
elif is_path_segment(path): elif is_path_segment(path):
path_svg = Path(path).d() path_svg = Path(path).d()
elif isinstance(path, string): elif isinstance(path, str):
# Assume this is a valid d-string. # Assume this is a valid d-string.
# TODO: Should we sanity check the input string? # TODO: Should we sanity check the input string?
path_svg = path path_svg = path
@ -431,7 +418,7 @@ class Document:
SVG_NAMESPACE['svg']), group_attribs) SVG_NAMESPACE['svg']), group_attribs)
def __repr__(self): def __repr__(self):
return etree.tostring(self.tree.getroot()).decode() return tostring(self.tree.getroot()).decode()
def pretty(self, **kwargs): def pretty(self, **kwargs):
return parseString(repr(self)).toprettyxml(**kwargs) return parseString(repr(self)).toprettyxml(**kwargs)

View File

@ -43,8 +43,8 @@ except NameError:
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa') COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA') UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])") COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?") FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
# Default Parameters ########################################################## # Default Parameters ##########################################################
@ -189,6 +189,7 @@ def bez2poly(bez, numpy_ordering=True, return_poly1d=False):
def transform_segments_together(path, transformation): def transform_segments_together(path, transformation):
"""Makes sure that, if joints were continuous, they're kept that way.""" """Makes sure that, if joints were continuous, they're kept that way."""
transformed_segs = [transformation(seg) for seg in path] transformed_segs = [transformation(seg) for seg in path]
joint_was_continuous = [sa.end == sb.start for sa, sb in path.joints()]
for i, (sa, sb) in enumerate(path.joints()): for i, (sa, sb) in enumerate(path.joints()):
if sa.end == sb.start: if sa.end == sb.start:
@ -201,7 +202,7 @@ def rotate(curve, degs, origin=None):
(a complex number). By default origin is either `curve.point(0.5)`, or in (a complex number). By default origin is either `curve.point(0.5)`, or in
the case that curve is an Arc object, `origin` defaults to `curve.center`. the case that curve is an Arc object, `origin` defaults to `curve.center`.
""" """
def rotate_point(z): def transform(z):
return exp(1j*radians(degs))*(z - origin) + origin return exp(1j*radians(degs))*(z - origin) + origin
if origin is None: if origin is None:
@ -214,10 +215,10 @@ def rotate(curve, degs, origin=None):
transformation = lambda seg: rotate(seg, degs, origin=origin) transformation = lambda seg: rotate(seg, degs, origin=origin)
return transform_segments_together(curve, transformation) return transform_segments_together(curve, transformation)
elif is_bezier_segment(curve): elif is_bezier_segment(curve):
return bpoints2bezier([rotate_point(bpt) for bpt in curve.bpoints()]) return bpoints2bezier([transform(bpt) for bpt in curve.bpoints()])
elif isinstance(curve, Arc): elif isinstance(curve, Arc):
new_start = rotate_point(curve.start) new_start = transform(curve.start)
new_end = rotate_point(curve.end) new_end = transform(curve.end)
new_rotation = curve.rotation + degs new_rotation = curve.rotation + degs
return Arc(new_start, radius=curve.radius, rotation=new_rotation, return Arc(new_start, radius=curve.radius, rotation=new_rotation,
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end) large_arc=curve.large_arc, sweep=curve.sweep, end=new_end)
@ -294,10 +295,6 @@ def scale(curve, sx, sy=None, origin=0j):
def transform(curve, tf): def transform(curve, tf):
"""Transforms the curve by the homogeneous transformation matrix tf""" """Transforms the curve by the homogeneous transformation matrix tf"""
if all((tf == np.eye(3)).ravel()):
return curve # tf is identity, return curve as is
def to_point(p): def to_point(p):
return np.array([[p.real], [p.imag], [1.0]]) return np.array([[p.real], [p.imag], [1.0]])
@ -318,7 +315,7 @@ def transform(curve, tf):
new_start = to_complex(tf.dot(to_point(curve.start))) new_start = to_complex(tf.dot(to_point(curve.start)))
new_end = to_complex(tf.dot(to_point(curve.end))) new_end = to_complex(tf.dot(to_point(curve.end)))
# Based on https://math.stackexchange.com/questions/2349726/ # Based on https://math.stackexchange.com/questions/2349726/compute-the-major-and-minor-axis-of-an-ellipse-after-linearly-transforming-it
rx2 = curve.radius.real ** 2 rx2 = curve.radius.real ** 2
ry2 = curve.radius.imag ** 2 ry2 = curve.radius.imag ** 2
@ -338,14 +335,10 @@ def transform(curve, tf):
if new_radius.real == 0 or new_radius.imag == 0 : if new_radius.real == 0 or new_radius.imag == 0 :
return Line(new_start, new_end) return Line(new_start, new_end)
else: else :
if tf[0][0] * tf[1][1] >= 0.0:
new_sweep = curve.sweep
else:
new_sweep = not curve.sweep
return Arc(new_start, radius=new_radius, rotation=curve.rotation + rot, return Arc(new_start, radius=new_radius, rotation=curve.rotation + rot,
large_arc=curve.large_arc, sweep=new_sweep, end=new_end, large_arc=curve.large_arc, sweep=curve.sweep, end=new_end,
autoscale_radius=True) autoscale_radius=False)
else: else:
raise TypeError("Input `curve` should be a Path, Line, " raise TypeError("Input `curve` should be a Path, Line, "
@ -715,19 +708,6 @@ class Line(object):
Note: This will fail if the two segments coincide for more than a Note: This will fail if the two segments coincide for more than a
finite collection of points. finite collection of points.
tol is not used.""" tol is not used."""
if isinstance(other_seg, (Line, QuadraticBezier, CubicBezier)):
ob = [e.real for e in other_seg.bpoints()]
sb = [e.real for e in self.bpoints()]
if min(ob) > max(sb):
return []
if max(ob) < min(sb):
return []
ob = [e.imag for e in other_seg.bpoints()]
sb = [e.imag for e in self.bpoints()]
if min(ob) > max(sb):
return []
if max(ob) < min(sb):
return []
if isinstance(other_seg, Line): if isinstance(other_seg, Line):
assert other_seg.end != other_seg.start and self.end != self.start assert other_seg.end != other_seg.start and self.end != self.start
assert self != other_seg assert self != other_seg
@ -940,6 +920,7 @@ class QuadraticBezier(object):
if t0 == 1 and t1 == 0: if t0 == 1 and t1 == 0:
if self._length_info['bpoints'] == self.bpoints(): if self._length_info['bpoints'] == self.bpoints():
return self._length_info['length'] return self._length_info['length']
a = self.start - 2*self.control + self.end a = self.start - 2*self.control + self.end
b = 2*(self.control - self.start) b = 2*(self.control - self.start)
a_dot_b = a.real*b.real + a.imag*b.imag a_dot_b = a.real*b.real + a.imag*b.imag
@ -947,20 +928,23 @@ class QuadraticBezier(object):
if abs(a) < 1e-12: if abs(a) < 1e-12:
s = abs(b)*(t1 - t0) s = abs(b)*(t1 - t0)
else: else:
c2 = 4 * (a.real ** 2 + a.imag ** 2) with np.testing.suppress_warnings() as sup:
c1 = 4 * a_dot_b sup.filter(RuntimeWarning)
c0 = b.real ** 2 + b.imag ** 2 c2 = 4 * (a.real ** 2 + a.imag ** 2)
c1 = 4 * a_dot_b
c0 = b.real ** 2 + b.imag ** 2
beta = c1 / (2 * c2) beta = c1 / (2 * c2)
gamma = c0 / c2 - beta ** 2 gamma = c0 / c2 - beta ** 2
dq1_mag = sqrt(c2 * t1 ** 2 + c1 * t1 + c0)
dq0_mag = sqrt(c2 * t0 ** 2 + c1 * t0 + c0)
logarand = (sqrt(c2) * (t1 + beta) + dq1_mag) / \
(sqrt(c2) * (t0 + beta) + dq0_mag)
s = (t1 + beta) * dq1_mag - (t0 + beta) * dq0_mag + \
gamma * sqrt(c2) * log(logarand)
s /= 2
dq1_mag = sqrt(c2 * t1 ** 2 + c1 * t1 + c0)
dq0_mag = sqrt(c2 * t0 ** 2 + c1 * t0 + c0)
logarand = (sqrt(c2) * (t1 + beta) + dq1_mag) / \
(sqrt(c2) * (t0 + beta) + dq0_mag)
s = (t1 + beta) * dq1_mag - (t0 + beta) * dq0_mag + \
gamma * sqrt(c2) * log(logarand)
s /= 2
if isnan(s): if isnan(s):
tstar = abs(b) / (2 * abs(a)) tstar = abs(b) / (2 * abs(a))
if t1 < tstar: if t1 < tstar:
@ -1055,19 +1039,6 @@ class QuadraticBezier(object):
self.point(t1) == other_seg.point(t2). self.point(t1) == other_seg.point(t2).
Note: This will fail if the two segments coincide for more than a Note: This will fail if the two segments coincide for more than a
finite collection of points.""" finite collection of points."""
if isinstance(other_seg, (Line, QuadraticBezier, CubicBezier)):
ob = [e.real for e in other_seg.bpoints()]
sb = [e.real for e in self.bpoints()]
if min(ob) > max(sb):
return []
if max(ob) < min(sb):
return []
ob = [e.imag for e in other_seg.bpoints()]
sb = [e.imag for e in self.bpoints()]
if min(ob) > max(sb):
return []
if max(ob) < min(sb):
return []
if isinstance(other_seg, Line): if isinstance(other_seg, Line):
return bezier_by_line_intersections(self, other_seg) return bezier_by_line_intersections(self, other_seg)
elif isinstance(other_seg, QuadraticBezier): elif isinstance(other_seg, QuadraticBezier):
@ -1328,19 +1299,6 @@ class CubicBezier(object):
This will fail if the two segments coincide for more than a This will fail if the two segments coincide for more than a
finite collection of points. finite collection of points.
""" """
if isinstance(other_seg, (Line, QuadraticBezier, CubicBezier)):
ob = [e.real for e in other_seg.bpoints()]
sb = [e.real for e in self.bpoints()]
if min(ob) > max(sb):
return []
if max(ob) < min(sb):
return []
ob = [e.imag for e in other_seg.bpoints()]
sb = [e.imag for e in self.bpoints()]
if min(ob) > max(sb):
return []
if max(ob) < min(sb):
return []
if isinstance(other_seg, Line): if isinstance(other_seg, Line):
return bezier_by_line_intersections(self, other_seg) return bezier_by_line_intersections(self, other_seg)
elif (isinstance(other_seg, QuadraticBezier) or elif (isinstance(other_seg, QuadraticBezier) or
@ -1398,7 +1356,7 @@ class CubicBezier(object):
class Arc(object): class Arc(object):
def __init__(self, start, radius, rotation, large_arc, sweep, end, def __init__(self, start, radius, rotation, large_arc, sweep, end,
autoscale_radius=True): autoscale_radius=True):
r""" """
This should be thought of as a part of an ellipse connecting two This should be thought of as a part of an ellipse connecting two
points on that ellipse, start and end. points on that ellipse, start and end.
Parameters Parameters
@ -2572,7 +2530,7 @@ class Path(MutableSequence):
# Shortcuts # Shortcuts
if len(self._segments) == 0: if len(self._segments) == 0:
raise ValueError("This path contains no segments!") return None
if pos == 0.0: if pos == 0.0:
return self._segments[0].point(pos) return self._segments[0].point(pos)
if pos == 1.0: if pos == 1.0:
@ -2589,7 +2547,6 @@ class Path(MutableSequence):
segment_end - segment_start) segment_end - segment_start)
return segment.point(segment_pos) return segment.point(segment_pos)
segment_start = segment_end segment_start = segment_end
raise RuntimeError("Something has gone wrong. Could not compute Path.point({}) for path {}".format(pos, self))
def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH): def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
self._calc_lengths(error=error, min_depth=min_depth) self._calc_lengths(error=error, min_depth=min_depth)

View File

@ -8,7 +8,7 @@ from __future__ import division, absolute_import, print_function
from math import ceil from math import ceil
from os import path as os_path, makedirs from os import path as os_path, makedirs
from tempfile import gettempdir from tempfile import gettempdir
from xml.dom.minidom import parse as md_xml_parse from defusedxml.minidom import parse as md_xml_parse
from svgwrite import Drawing, text as txt from svgwrite import Drawing, text as txt
from time import time from time import time
from warnings import warn from warnings import warn
@ -214,13 +214,10 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
timestamp = True if timestamp is None else timestamp timestamp = True if timestamp is None else timestamp
filename = os_path.join(gettempdir(), 'disvg_output.svg') filename = os_path.join(gettempdir(), 'disvg_output.svg')
dirname = os_path.abspath(os_path.dirname(filename))
if not os_path.exists(dirname):
makedirs(dirname)
# append time stamp to filename # append time stamp to filename
if timestamp: if timestamp:
fbname, fext = os_path.splitext(filename) fbname, fext = os_path.splitext(filename)
dirname = os_path.dirname(filename)
tstamp = str(time()).replace('.', '') tstamp = str(time()).replace('.', '')
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
filename = os_path.join(dirname, stfilename) filename = os_path.join(dirname, stfilename)
@ -410,6 +407,9 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
if paths2Drawing: if paths2Drawing:
return dwg return dwg
# save svg
if not os_path.exists(os_path.dirname(filename)):
makedirs(os_path.dirname(filename))
dwg.save() dwg.save()
# re-open the svg, make the xml pretty, and save it again # re-open the svg, make the xml pretty, and save it again

View File

@ -10,19 +10,26 @@ from .misctools import isclose
def polyroots(p, realroots=False, condition=lambda r: True): 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]
Args:
p: 1D 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.
Returns:
(list) A list containing the roots of the polynomial.
Notes:
* This uses np.isclose and np.roots
""" """
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) roots = np.roots(p)
if realroots: if realroots:
roots = [r.real for r in roots if isclose(r.imag, 0)] roots = [r.real for r in roots if isclose(r.imag, 0)]
@ -36,16 +43,18 @@ def polyroots(p, realroots=False, condition=lambda r: True):
def polyroots01(p): def polyroots01(p):
"""Returns the real roots between 0 and 1 of the polynomial with """Returns the real roots 0 < x < 1 of the polynomial given by `p`.
coefficients given in p,
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n] 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."""
Notes:
p can also be a np.poly1d object. See polyroots for more information.
"""
return polyroots(p, realroots=True, condition=lambda tval: 0 <= tval <= 1) return polyroots(p, realroots=True, condition=lambda tval: 0 <= tval <= 1)
def rational_limit(f, g, t0): def rational_limit(f, g, t0):
"""Computes the limit of the rational function (f/g)(t) """Computes the limit of the rational function (f/g)(t) as t approaches t0."""
as t approaches t0."""
assert isinstance(f, np.poly1d) and isinstance(g, np.poly1d) assert isinstance(f, np.poly1d) and isinstance(g, np.poly1d)
assert g != np.poly1d([0]) assert g != np.poly1d([0])
if g(t0) != 0: if g(t0) != 0:

View File

@ -5,8 +5,8 @@
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import os import os
from xml.etree.ElementTree import iterparse, Element, ElementTree, SubElement from xml.etree.cElementTree import Element, ElementTree, SubElement
import numpy as np from defusedxml.cElementTree import iterparse
# Internal dependencies # Internal dependencies
from .parser import parse_path from .parser import parse_path
@ -14,13 +14,13 @@ from .parser import parse_transform
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd, from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
polyline2pathd, polygon2pathd, rect2pathd) polyline2pathd, polygon2pathd, rect2pathd)
from .misctools import open_in_browser from .misctools import open_in_browser
from .path import transform from .path import *
# To maintain forward/backward compatibility # To maintain forward/backward compatibility
try: try:
string = basestring str = basestring
except NameError: except NameError:
string = str pass
NAME_SVG = "svg" NAME_SVG = "svg"
ATTR_VERSION = "version" ATTR_VERSION = "version"
@ -165,17 +165,17 @@ class SaxDocument:
if matrix is not None and not np.all(np.equal(matrix, identity)): if matrix is not None and not np.all(np.equal(matrix, identity)):
matrix_string = "matrix(" matrix_string = "matrix("
matrix_string += " " matrix_string += " "
matrix_string += string(matrix[0][0]) matrix_string += str(matrix[0][0])
matrix_string += " " matrix_string += " "
matrix_string += string(matrix[1][0]) matrix_string += str(matrix[1][0])
matrix_string += " " matrix_string += " "
matrix_string += string(matrix[0][1]) matrix_string += str(matrix[0][1])
matrix_string += " " matrix_string += " "
matrix_string += string(matrix[1][1]) matrix_string += str(matrix[1][1])
matrix_string += " " matrix_string += " "
matrix_string += string(matrix[0][2]) matrix_string += str(matrix[0][2])
matrix_string += " " matrix_string += " "
matrix_string += string(matrix[1][2]) matrix_string += str(matrix[1][2])
matrix_string += ")" matrix_string += ")"
path.set(ATTR_TRANSFORM, matrix_string) path.set(ATTR_TRANSFORM, matrix_string)
if ATTR_DATA in values: if ATTR_DATA in values:

View File

@ -3,14 +3,9 @@ The main tool being the svg2paths() function."""
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse from defusedxml.minidom import parse
import os from os import path as os_path, getcwd
from io import StringIO
import re import re
try:
from os import PathLike as FilePathLike
except ImportError:
FilePathLike = str
# Internal dependencies # Internal dependencies
from .parser import parse_path from .parser import parse_path
@ -51,7 +46,7 @@ def ellipse2pathd(ellipse):
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0' d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0' d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
return d + 'z' return d
def polyline2pathd(polyline, is_polygon=False): def polyline2pathd(polyline, is_polygon=False):
@ -91,39 +86,14 @@ def rect2pathd(rect):
The rectangle will start at the (x,y) coordinate specified by the The rectangle will start at the (x,y) coordinate specified by the
rectangle object and proceed counter-clockwise.""" rectangle object and proceed counter-clockwise."""
x, y = float(rect.get('x', 0)), float(rect.get('y', 0)) x0, y0 = float(rect.get('x', 0)), float(rect.get('y', 0))
w, h = float(rect.get('width', 0)), float(rect.get('height', 0)) w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
if 'rx' in rect or 'ry' in rect: x1, y1 = x0 + w, y0
x2, y2 = x0 + w, y0 + h
# if only one, rx or ry, is present, use that value for both x3, y3 = x0, y0 + h
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
rx = rect.get('rx', None)
ry = rect.get('ry', None)
if rx is None:
rx = ry or 0.
if ry is None:
ry = rx or 0.
rx, ry = float(rx), float(ry)
d = "M {} {} ".format(x + rx, y) # right of p0
d += "L {} {} ".format(x + w - rx, y) # go to p1
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w, y+ry) # arc for p1
d += "L {} {} ".format(x+w, y+h-ry) # above p2
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w-rx, y+h) # arc for p2
d += "L {} {} ".format(x+rx, y+h) # right of p3
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x, y+h-ry) # arc for p3
d += "L {} {} ".format(x, y+ry) # below p0
d += "A {} {} 0 0 1 {} {} z".format(rx, ry, x+rx, y) # arc for p0
return d
x0, y0 = x, y
x1, y1 = x + w, y
x2, y2 = x + w, y + h
x3, y3 = x, y + h
d = ("M{} {} L {} {} L {} {} L {} {} z" d = ("M{} {} L {} {} L {} {} L {} {} z"
"".format(x0, y0, x1, y1, x2, y2, x3, y3)) "".format(x0, y0, x1, y1, x2, y2, x3, y3))
return d return d
@ -149,9 +119,7 @@ def svg2paths(svg_file_location,
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements. SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
Args: Args:
svg_file_location (string or file-like object): the location of the svg_file_location (string): the location of the svg file
svg file on disk or a file-like object containing the content of a
svg file
return_svg_attributes (bool): Set to True and a dictionary of return_svg_attributes (bool): Set to True and a dictionary of
svg-attributes will be extracted and returned. See also the svg-attributes will be extracted and returned. See also the
`svg2paths2()` function. `svg2paths2()` function.
@ -175,10 +143,8 @@ def svg2paths(svg_file_location,
list: The list of corresponding path attribute dictionaries. list: The list of corresponding path attribute dictionaries.
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`). dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
""" """
# strings are interpreted as file location everything else is treated as if os_path.dirname(svg_file_location) == '':
# file-like object and passed to the xml parser directly svg_file_location = os_path.join(getcwd(), svg_file_location)
from_filepath = isinstance(svg_file_location, str) or isinstance(svg_file_location, FilePathLike)
svg_file_location = os.path.abspath(svg_file_location) if from_filepath else svg_file_location
doc = parse(svg_file_location) doc = parse(svg_file_location)
@ -258,26 +224,3 @@ def svg2paths2(svg_file_location,
convert_polylines_to_paths=convert_polylines_to_paths, convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths, convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths) convert_rectangles_to_paths=convert_rectangles_to_paths)
def svgstr2paths(svg_string,
return_svg_attributes=False,
convert_circles_to_paths=True,
convert_ellipses_to_paths=True,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
convert_rectangles_to_paths=True):
"""Convenience function; identical to svg2paths() except that it takes the
svg object as string. See svg2paths() docstring for more
info."""
# wrap string into StringIO object
svg_file_obj = StringIO(svg_string)
return svg2paths(svg_file_location=svg_file_obj,
return_svg_attributes=return_svg_attributes,
convert_circles_to_paths=convert_circles_to_paths,
convert_ellipses_to_paths=convert_ellipses_to_paths,
convert_lines_to_paths=convert_lines_to_paths,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths)

View File

@ -1,19 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100mm" height="100mm" viewBox="-100 -200 500 500" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g id="Sketch" transform="scale(1,-1)">
<path id="slot" d="
M 0 10
L 0 80
A 30 30 0 1 0 0 140
A 10 10 0 0 1 0 100
L 100 100
A 10 10 0 1 1 100 140
A 30 30 0 0 0 100 80
L 100 10
A 10 10 0 0 0 90 0
L 10 0
A 10 10 0 0 0 0 10
" stroke="#ff0000" stroke-width="0.35 px"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 665 B

View File

@ -6,6 +6,7 @@ from svgpathtools.path import bpoints2bezier
class HigherOrderBezier: class HigherOrderBezier:
"""To help test Bezier curves of arbitrary degree"""
def __init__(self, bpoints): def __init__(self, bpoints):
self.bpts = bpoints self.bpts = bpoints

View File

@ -1,54 +0,0 @@
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import Document
from io import StringIO
from io import open # overrides build-in open for compatibility with python2
from os.path import join, dirname
from sys import version_info
class TestDocument(unittest.TestCase):
def test_from_file_path_string(self):
"""Test reading svg from file provided as path"""
doc = Document(join(dirname(__file__), 'polygons.svg'))
self.assertEqual(len(doc.paths()), 2)
def test_from_file_path(self):
"""Test reading svg from file provided as path"""
if version_info >= (3, 6):
import pathlib
doc = Document(pathlib.Path(__file__).parent / 'polygons.svg')
self.assertEqual(len(doc.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:
doc = Document(file)
self.assertEqual(len(doc.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:
# read entire file into string
file_content = file.read()
# prepare stringio object
file_as_stringio = StringIO(file_content)
doc = Document(file_as_stringio)
self.assertEqual(len(doc.paths()), 2)
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:
# read entire file into string
file_content = file.read()
doc = Document.from_svg_string(file_content)
self.assertEqual(len(doc.paths()), 2)

View File

@ -1,5 +1,4 @@
# Note: This file was taken mostly as is from the svg.path module (v 2.0) """credit: This was modified from a file in the svg.path module (v 2.0)"""
#------------------------------------------------------------------------------
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import parse_path from svgpathtools import parse_path

View File

@ -5,15 +5,11 @@ $ python -m unittest test.test_groups.TestGroups.test_group_flatten
""" """
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import Document, SVG_NAMESPACE, parse_path, Line, Arc from svgpathtools import Document, SVG_NAMESPACE, parse_path
from os.path import join, dirname from os.path import join, dirname
import numpy as np import numpy as np
# When an assert fails, show the full error message, don't truncate it.
unittest.util._MAX_LENGTH = 999999999
def get_desired_path(name, paths): def get_desired_path(name, paths):
return next(p for p in paths return next(p for p in paths
if p.element.get('{some://testuri}name') == name) if p.element.get('{some://testuri}name') == name)
@ -46,22 +42,6 @@ class TestGroups(unittest.TestCase):
self.check_values(tf.dot(v_s), actual.start) self.check_values(tf.dot(v_s), actual.start)
self.check_values(tf.dot(v_e), actual.end) self.check_values(tf.dot(v_e), actual.end)
def test_group_transform(self):
# The input svg has a group transform of "scale(1,-1)", which
# can mess with Arc sweeps.
doc = Document(join(dirname(__file__), 'negative-scale.svg'))
path = doc.paths()[0]
self.assertEqual(path[0], Line(start=-10j, end=-80j))
self.assertEqual(path[1], Arc(start=-80j, radius=(30+30j), rotation=0.0, large_arc=True, sweep=True, end=-140j))
self.assertEqual(path[2], Arc(start=-140j, radius=(20+20j), rotation=0.0, large_arc=False, sweep=False, end=-100j))
self.assertEqual(path[3], Line(start=-100j, end=(100-100j)))
self.assertEqual(path[4], Arc(start=(100-100j), radius=(20+20j), rotation=0.0, large_arc=True, sweep=False, end=(100-140j)))
self.assertEqual(path[5], Arc(start=(100-140j), radius=(30+30j), rotation=0.0, large_arc=False, sweep=True, end=(100-80j)))
self.assertEqual(path[6], Line(start=(100-80j), end=(100-10j)))
self.assertEqual(path[7], Arc(start=(100-10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=(90+0j)))
self.assertEqual(path[8], Line(start=(90+0j), end=(10+0j)))
self.assertEqual(path[9], Arc(start=(10+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=-10j))
def test_group_flatten(self): def test_group_flatten(self):
# Test the Document.paths() function against the # Test the Document.paths() function against the
# groups.svg test file. # groups.svg test file.
@ -255,11 +235,4 @@ class TestGroups(unittest.TestCase):
path = parse_path(path_d) path = parse_path(path_d)
svg_path = doc.add_path(path, group=new_leaf) svg_path = doc.add_path(path, group=new_leaf)
self.assertEqual(path_d, svg_path.get('d')) self.assertEqual(path_d, svg_path.get('d'))
# Test that paths are added to the correct group
new_sibling = doc.get_or_add_group(
['base_group', 'new_parent', 'new_sibling'])
doc.add_path(path, group=new_sibling)
self.assertEqual(len(new_sibling), 1)
self.assertEqual(path_d, new_sibling[0].get('d'))

View File

@ -1,9 +1,8 @@
# Note: This file was taken mostly as is from the svg.path module (v 2.0) # Note: This file was taken mostly as is from the svg.path module (v 2.0)
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, parse_path from svgpathtools import parse_path, Path, Line, QuadraticBezier, CubicBezier, Arc
import svgpathtools import svgpathtools
import numpy as np import numpy as np

View File

@ -1,9 +1,6 @@
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import os from unittest import TestCase
import time
from sys import version_info
import unittest
from math import sqrt, pi from math import sqrt, pi
from operator import itemgetter from operator import itemgetter
import numpy as np import numpy as np
@ -12,10 +9,9 @@ import warnings
# Internal dependencies # Internal dependencies
from svgpathtools import ( from svgpathtools import (
Line, QuadraticBezier, CubicBezier, Arc, Path, poly2bez, path_encloses_pt, Line, QuadraticBezier, CubicBezier, Arc, Path, parse_path,
bpoints2bezier, closest_point_in_path, farthest_point_in_path, is_bezier_segment, is_bezier_path, poly2bez, bpoints2bezier,
is_bezier_segment, is_bezier_path, parse_path closest_point_in_path, farthest_point_in_path, path_encloses_pt)
)
from svgpathtools.path import bezier_radialrange from svgpathtools.path import bezier_radialrange
# An important note for those doing any debugging: # An important note for those doing any debugging:
@ -71,7 +67,25 @@ def assert_intersections(test_case, a_seg, b_seg, intersections, count, msg=None
test_case.assertAlmostEqual(a_seg.point(i[0]), b_seg.point(i[1]), msg=msg, delta=tol) test_case.assertAlmostEqual(a_seg.point(i[0]), b_seg.point(i[1]), msg=msg, delta=tol)
class LineTest(unittest.TestCase): class AssertWarns(warnings.catch_warnings):
"""A python 2 compatible version of assertWarns."""
def __init__(self, test_case, warning):
self.test_case = test_case
self.warning_type = warning
self.log = None
super(AssertWarns, self).__init__(record=True, module=None)
def __enter__(self):
self.log = super(AssertWarns, self).__enter__()
return self.log
def __exit__(self, *exc_info):
super(AssertWarns, self).__exit__(*exc_info)
self.test_case.assertEqual(type(self.log[0]), self.warning_type)
# noinspection PyTypeChecker
class LineTest(TestCase):
def test_lines(self): def test_lines(self):
# These points are calculated, and not just regression tests. # These points are calculated, and not just regression tests.
@ -165,9 +179,9 @@ class LineTest(unittest.TestCase):
self.assertIsNone(l.point_to_t(-0.001-0j)) self.assertIsNone(l.point_to_t(-0.001-0j))
random.seed() random.seed()
for line_index in range(100): for _ in range(100):
l = random_line() l = random_line()
for t_index in range(100): for __ in range(100):
orig_t = random.random() orig_t = random.random()
p = l.point(orig_t) p = l.point(orig_t)
computed_t = l.point_to_t(p) computed_t = l.point_to_t(p)
@ -188,7 +202,8 @@ class LineTest(unittest.TestCase):
self.assertAlmostEqual(max_ta, max_tb, delta=TOL) self.assertAlmostEqual(max_ta, max_tb, delta=TOL)
class CubicBezierTest(unittest.TestCase): # noinspection PyTypeChecker
class CubicBezierTest(TestCase):
def test_approx_circle(self): def test_approx_circle(self):
"""This is a approximate circle drawn in Inkscape""" """This is a approximate circle drawn in Inkscape"""
@ -424,14 +439,13 @@ class CubicBezierTest(unittest.TestCase):
segment = CubicBezier(complex(600, 500), complex(600, 350), segment = CubicBezier(complex(600, 500), complex(600, 350),
complex(900, 650), complex(900, 500)) complex(900, 650), complex(900, 500))
self.assertTrue(segment == self.assertTrue(segment == CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)) self.assertTrue(segment != CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j))
self.assertTrue(segment !=
CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j))
self.assertTrue(segment != Line(0, 400)) self.assertTrue(segment != Line(0, 400))
class QuadraticBezierTest(unittest.TestCase): # noinspection PyTypeChecker
class QuadraticBezierTest(TestCase):
def test_svg_examples(self): def test_svg_examples(self):
"""These is the path in the SVG specs""" """These is the path in the SVG specs"""
@ -500,25 +514,24 @@ class QuadraticBezierTest(unittest.TestCase):
# This is to test the __eq__ and __ne__ methods, so we can't use # This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual # assertEqual and assertNotEqual
segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
self.assertTrue(segment == self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j))
QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)) self.assertTrue(segment != QuadraticBezier(200 + 301j, 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.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment) self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment)
class ArcTest(unittest.TestCase): # noinspection PyTypeChecker
class ArcTest(TestCase):
def test_trusting_acos(self): def test_trusting_acos(self):
"""`u1.real` is > 1 in this arc due to numerical error.""" """`u1.real` is > 1 in this arc due to numerical error."""
try: try:
a1 = Arc(start=(160.197+102.925j), _ = Arc(start=(160.197+102.925j),
radius=(0.025+0.025j), radius=(0.025+0.025j),
rotation=0.0, rotation=0.0,
large_arc=False, large_arc=False,
sweep=True, sweep=True,
end=(160.172+102.95j)) end=(160.172+102.95j))
except ValueError: except ValueError:
self.fail("Arc() raised ValueError unexpectedly!") self.fail("Arc() raised ValueError unexpectedly!")
@ -685,9 +698,9 @@ class ArcTest(unittest.TestCase):
self.assertIsNone(a.point_to_t(730.5212132777968+171j)) self.assertIsNone(a.point_to_t(730.5212132777968+171j))
random.seed() random.seed()
for arc_index in range(100): for _ in range(100):
a = random_arc() a = random_arc()
for t_index in np.linspace(0, 1, 100): for __ in np.linspace(0, 1, 100):
orig_t = random.random() orig_t = random.random()
p = a.point(orig_t) p = a.point(orig_t)
computed_t = a.point_to_t(p) computed_t = a.point_to_t(p)
@ -697,7 +710,7 @@ class ArcTest(unittest.TestCase):
def test_approx_quad(self): def test_approx_quad(self):
n = 100 n = 100
for i in range(n): for _ in range(n):
arc = random_arc() arc = random_arc()
if arc.radius.real > 2000 or arc.radius.imag > 2000: if arc.radius.real > 2000 or arc.radius.imag > 2000:
continue # Random Arc too large, by autoscale. continue # Random Arc too large, by autoscale.
@ -710,7 +723,7 @@ class ArcTest(unittest.TestCase):
def test_approx_cubic(self): def test_approx_cubic(self):
n = 100 n = 100
for i in range(n): for _ in range(n):
arc = random_arc() arc = random_arc()
if arc.radius.real > 2000 or arc.radius.imag > 2000: if arc.radius.real > 2000 or arc.radius.imag > 2000:
continue # Random Arc too large, by autoscale. continue # Random Arc too large, by autoscale.
@ -722,66 +735,62 @@ class ArcTest(unittest.TestCase):
self.assertAlmostEqual(d, 0.0, delta=2) self.assertAlmostEqual(d, 0.0, delta=2)
class TestPath(unittest.TestCase): # noinspection PyTypeChecker
class TestPath(TestCase):
def test_hash(self): # def test_hash(self):
line1 = Line(600.5 + 350.5j, 650.5 + 325.5j) # line1 = Line(600.5 + 350.5j, 650.5 + 325.5j)
arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j) # arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j)
arc2 = Arc(650 + 325j, 30 + 25j, -30, 0, 0, 700 + 300j) # arc2 = Arc(650 + 325j, 30 + 25j, -30, 0, 0, 700 + 300j)
cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j) # cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j)
cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j) # cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j)
quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j) # quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j)
linez = Line(600 + 300j, 600 + 350j) # linez = Line(600 + 300j, 600 + 350j)
#
bezpath = Path(line1, cub1, cub2, quad3) # bezpath = Path(line1, cub1, cub2, quad3)
bezpathz = Path(line1, cub1, cub2, quad3, linez) # bezpathz = Path(line1, cub1, cub2, quad3, linez)
path = Path(line1, arc1, cub2, quad3) # path = Path(line1, arc1, cub2, quad3)
pathz = Path(line1, arc1, cub2, quad3, linez) # pathz = Path(line1, arc1, cub2, quad3, linez)
lpath = Path(linez) # lpath = Path(linez)
qpath = Path(quad3) # qpath = Path(quad3)
cpath = Path(cub1) # cpath = Path(cub1)
apath = Path(arc1, arc2) # apath = Path(arc1, arc2)
#
test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath, # test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath,
apath, line1, arc1, arc2, cub1, cub2, quad3, linez] # apath, line1, arc1, arc2, cub1, cub2, quad3, linez]
#
# this is necessary due to changes to the builtin `hash` function # # this is necessary due to changes to the builtin `hash` function
user_hash_seed = os.environ.get("PYTHONHASHSEED", "") # python_version = sys.version_info.major + 0.1*sys.version_info.minor
os.environ["PYTHONHASHSEED"] = "314" # user_hash_seed = os.environ.get("PYTHONHASHSEED", "")
if version_info >= (3, 8): # os.environ["PYTHONHASHSEED"] = "314"
expected_hashes = [ # if 3.8 <= python_version:
-6073024107272494569, -2519772625496438197, 8726412907710383506, # expected_hashes = [
2132930052750006195, 3112548573593977871, 991446120749438306, # -6073024107272494569, -2519772625496438197, 8726412907710383506,
-5589397644574569777, -4438808571483114580, -3125333407400456536, # 2132930052750006195, 3112548573593977871, 991446120749438306,
-4418099728831808951, 702646573139378041, -6331016786776229094, # -5589397644574569777, -4438808571483114580, -3125333407400456536,
5053050772929443013, 6102272282813527681, -5385294438006156225 # -4418099728831808951, 702646573139378041, -6331016786776229094,
] # 5053050772929443013, 6102272282813527681, -5385294438006156225
elif (3, 2) <= version_info < (3, 8): # ]
expected_hashes = [ # elif 3.2 <= python_version < 3.8:
-5662973462929734898, 5166874115671195563, 5223434942701471389, # expected_hashes = [
-7224979960884350294, -5178990533869800243, -4003140762934044601, # -5662973462929734898, 5166874115671195563, 5223434942701471389,
8575549467429100514, -6692132994808317852, 1594848578230132678, # -7224979960884350294, -5178990533869800243, -4003140762934044601,
-6374833902132909499, 4188352014604112779, -5090374009174854814, # 8575549467429100514, -6692132994808317852, 1594848578230132678,
-7093907105533857815, 2036243740727202243, -8108488067585685407 # -6374833902132909499, 4188352014604112779, -5090374009174854814,
] # -7093907105533857815, 2036243740727202243, -8108488067585685407
else: # ]
# else:
expected_hashes = [ # expected_hashes = [
-5762846476463470127, -138736730317965290, -2005041722222729058, # -5762846476463470127, -138736730317965290, -2005041722222729058,
8448700906794235291, -5178990533869800243, -4003140762934044601, # 8448700906794235291, -5178990533869800243, -4003140762934044601,
8575549467429100514, 5166859065265868968, 1373103287265872323, # 8575549467429100514, 5166859065265868968, 1373103287265872323,
-1022491904150314631, 4188352014604112779, -5090374009174854814, # -1022491904150314631, 4188352014604112779, -5090374009174854814,
-7093907105533857815, 2036243740727202243, -8108488067585685407 # -7093907105533857815, 2036243740727202243, -8108488067585685407
] # ]
#
if version_info.major == 2 and os.name == 'nt': # for c, h in zip(test_curves, expected_hashes):
# the expected hash values for 2.7 apparently differed on Windows # self.assertTrue(hash(c) == h, msg="hash {} was expected for curve = {}".format(h, c))
# if you work in Windows and want to fix this test, please do # os.environ["PYTHONHASHSEED"] = user_hash_seed # restore user's hash seed
return
for c, h in zip(test_curves, expected_hashes):
self.assertTrue(hash(c) == h, msg="hash {} was expected for curve = {}".format(h, c))
os.environ["PYTHONHASHSEED"] = user_hash_seed # restore user's hash seed
def test_circle(self): def test_circle(self):
arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j) arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
@ -820,8 +829,7 @@ class TestPath(unittest.TestCase):
# regression tests. # regression tests.
self.assertAlmostEqual(path.point(0.0), (275 + 175j), delta=TOL) self.assertAlmostEqual(path.point(0.0), (275 + 175j), delta=TOL)
self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j), delta=TOL) self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j), delta=TOL)
self.assertAlmostEqual(path.point(0.5), self.assertAlmostEqual(path.point(0.5), (168.93398282201787 + 68.93398282201787j))
(168.93398282201787 + 68.93398282201787j))
self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j), delta=TOL) self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j), delta=TOL)
self.assertAlmostEqual(path.point(1.0), (275 + 175j), delta=TOL) self.assertAlmostEqual(path.point(1.0), (275 + 175j), delta=TOL)
# The errors seem to accumulate. Still 6 decimal places is more # The errors seem to accumulate. Still 6 decimal places is more
@ -862,7 +870,7 @@ class TestPath(unittest.TestCase):
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
large_arc=0, sweep=1, end=700 + 300j), large_arc=0, sweep=1, end=700 + 300j),
CubicBezier(start=700 + 300j, control1=800 + 400j, CubicBezier(start=700 + 300j, control1=800 + 400j,
control2=750 + 200j, end=600 + 100j), control2=750 + 200j, end=600 + 100j),
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
self.assertEqual(eval(repr(path)), path) self.assertEqual(eval(repr(path)), path)
@ -874,14 +882,14 @@ class TestPath(unittest.TestCase):
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
large_arc=0, sweep=1, end=700 + 300j), large_arc=0, sweep=1, end=700 + 300j),
CubicBezier(start=700 + 300j, control1=800 + 400j, CubicBezier(start=700 + 300j, control1=800 + 400j,
control2=750 + 200j, end=600 + 100j), control2=750 + 200j, end=600 + 100j),
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
path2 = Path( path2 = Path(
Line(start=600 + 350j, end=650 + 325j), Line(start=600 + 350j, end=650 + 325j),
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
large_arc=0, sweep=1, end=700 + 300j), large_arc=0, sweep=1, end=700 + 300j),
CubicBezier(start=700 + 300j, control1=800 + 400j, CubicBezier(start=700 + 300j, control1=800 + 400j,
control2=750 + 200j, end=600 + 100j), control2=750 + 200j, end=600 + 100j),
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
self.assertTrue(path1 == path2) self.assertTrue(path1 == path2)
@ -1051,17 +1059,17 @@ class TestPath(unittest.TestCase):
test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath, test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath,
apath, line1, arc1, arc2, cub1, cub2, quad3, linez] apath, line1, arc1, arc2, cub1, cub2, quad3, linez]
def scale_a_point(pt, sx, sy=None, origin=0j): def scale_a_point(pt_, sx_, sy_=None, origin_=0j):
if sy is None: if sy_ is None:
sy = sx sy_ = sx_
zeta = pt - origin zeta = pt_ - origin_
pt_vec = [[zeta.real], pt_vec = [[zeta.real],
[zeta.imag], [zeta.imag],
[1]] [1]]
transform = [[sx, 0, origin.real], transform = [[sx_, 0, origin_.real],
[0, sy, origin.imag]] [0, sy_, origin_.imag]]
return complex(*np.dot(transform, pt_vec).ravel()) return complex(*np.dot(transform, pt_vec).ravel())
@ -1085,6 +1093,8 @@ class TestPath(unittest.TestCase):
# find seg which t lands on for failure reporting # find seg which t lands on for failure reporting
seg = curve seg = curve
seg_idx = None
seg_t = None
if isinstance(curve, Path): if isinstance(curve, Path):
seg_idx, seg_t = curve.T2t(t) seg_idx, seg_t = curve.T2t(t)
seg = curve[seg_idx] seg = curve[seg_idx]
@ -1123,7 +1133,7 @@ class TestPath(unittest.TestCase):
curve.scaled(sx, sy).point(t) curve.scaled(sx, sy).point(t)
else: else:
curve_scaled = curve.scaled(sx, sy) curve_scaled = curve.scaled(sx, sy)
seg_scaled = seg.scaled(sx, sy) _ = seg.scaled(sx, sy)
if isinstance(curve, Path): if isinstance(curve, Path):
res = curve_scaled[seg_idx].point(seg_t) res = curve_scaled[seg_idx].point(seg_t)
else: else:
@ -1197,20 +1207,21 @@ class TestPath(unittest.TestCase):
self.assertEqual(path2.d(use_closed_attrib=True, rel=True), rel_s) self.assertEqual(path2.d(use_closed_attrib=True, rel=True), rel_s)
class Test_ilength(unittest.TestCase): # noinspection PyTypeChecker
class Test_ilength(TestCase):
# See svgpathtools.notes.inv_arclength.py for information on how these # See svgpathtools.notes.inv_arclength.py for information on how these
# test values were generated (using the .length() method). # test values were generated (using the .length() method).
############################################################## ##############################################################
def test_ilength_lines(self): def test_ilength_lines(self):
l = Line(1, 3-1j) l = Line(1, 3-1j)
nodall = Line(1+1j, 1+1j) # nodall = Line(1+1j, 1+1j)
tests = [(l, 0.01, 0.022360679774997897), tests = [(l, 0.01, 0.022360679774997897),
(l, 0.1, 0.223606797749979), (l, 0.1, 0.223606797749979),
(l, 0.5, 1.118033988749895), (l, 0.5, 1.118033988749895),
(l, 0.9, 2.012461179749811), (l, 0.9, 2.012461179749811),
(l, 0.99, 2.213707297724792)] (l, 0.99, 2.213707297724792)]
for (l, t, s) in tests: for (l, t, s) in tests:
self.assertAlmostEqual(l.ilength(s), t, delta=TOL) self.assertAlmostEqual(l.ilength(s), t, delta=TOL)
@ -1220,37 +1231,31 @@ class Test_ilength(unittest.TestCase):
q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j) q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j)
closedq = QuadraticBezier(6 + 2j, 5 - 1j, 6 + 2j) closedq = QuadraticBezier(6 + 2j, 5 - 1j, 6 + 2j)
linq = QuadraticBezier(1+3j, 2+5j, -9 - 17j) linq = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
nodalq = QuadraticBezier(1, 1, 1) # nodalq = QuadraticBezier(1, 1, 1)
tests = [(q1, 0.01, 6.364183310105577), tests = [(q1, 0.01, 6.364183310105577),
(q1, 0.1, 60.23857499635088), (q1, 0.1, 60.23857499635088),
(q1, 0.5, 243.8855469477619), (q1, 0.5, 243.8855469477619),
(q1, 0.9, 427.53251889917294), (q1, 0.9, 427.53251889917294),
(q1, 0.99, 481.40691058541813), (q1, 0.99, 481.40691058541813),
(q2, 0.01, 6.365673533661836), (q2, 0.01, 6.365673533661836),
(q2, 0.1, 60.31675895732397), (q2, 0.1, 60.31675895732397),
(q2, 0.5, 233.24592830045907), (q2, 0.5, 233.24592830045907),
(q2, 0.9, 346.42891253298706), (q2, 0.9, 346.42891253298706),
(q2, 0.99, 376.32659156736844), (q2, 0.99, 376.32659156736844),
(closedq, 0.01, 0.06261309767133393), (closedq, 0.01, 0.06261309767133393),
(closedq, 0.1, 0.5692099788303084), (closedq, 0.1, 0.5692099788303084),
(closedq, 0.5, 1.5811388300841898), (closedq, 0.5, 1.5811388300841898),
(closedq, 0.9, 2.5930676813380713), (closedq, 0.9, 2.5930676813380713),
(closedq, 0.99, 3.0996645624970456), (closedq, 0.99, 3.0996645624970456),
(linq, 0.01, 0.04203807797699605), (linq, 0.01, 0.04203807797699605),
(linq, 0.1, 0.19379255804998186), (linq, 0.1, 0.19379255804998186),
(linq, 0.5, 4.844813951249544), (linq, 0.5, 4.844813951249544),
(linq, 0.9, 18.0823363780483), (linq, 0.9, 18.0823363780483),
(linq, 0.99, 22.24410609777091)] (linq, 0.99, 22.24410609777091)]
for q, t, s in tests: for q, t, s in tests:
try: self.assertAlmostEqual(q.ilength(s), t, delta=TOL)
self.assertAlmostEqual(q.ilength(s), t, delta=TOL)
except:
print(q)
print(s)
print(t)
raise
def test_ilength_cubics(self): def test_ilength_cubics(self):
c1 = CubicBezier(200 + 300j, 400 + 50j, 600+100j, -200) c1 = CubicBezier(200 + 300j, 400 + 50j, 600+100j, -200)
@ -1425,7 +1430,7 @@ class Test_ilength(unittest.TestCase):
for (c, t, s) in tests: for (c, t, s) in tests:
try: try:
self.assertAlmostEqual(c.ilength(s), t, msg=str((c, t, s)), delta=TOL) self.assertAlmostEqual(c.ilength(s), t, msg=str((c, t, s)), delta=TOL)
except: except ValueError:
# These test case values were generated using a system # These test case values were generated using a system
# with scipy installed -- if scipy is not installed, # with scipy installed -- if scipy is not installed,
# then in cases where `t == 1`, `s` may be slightly # then in cases where `t == 1`, `s` may be slightly
@ -1448,7 +1453,8 @@ class Test_ilength(unittest.TestCase):
lin.ilength(1) lin.ilength(1)
class Test_intersect(unittest.TestCase): # noinspection PyTypeChecker
class Test_intersect(TestCase):
def test_intersect(self): def test_intersect(self):
################################################################### ###################################################################
@ -1496,50 +1502,6 @@ class Test_intersect(unittest.TestCase):
self.assertTrue(len(yix) == 1) self.assertTrue(len(yix) == 1)
################################################################### ###################################################################
def test_random_intersections(self):
from random import Random
r = Random()
distance = 100
distribution = 10000
count = 500
def random_complex(offset_x=0.0, offset_y=0.0):
return complex(r.random() * distance + offset_x, r.random() * distance + offset_y)
def random_line():
offset_x = r.random() * distribution
offset_y = r.random() * distribution
return Line(random_complex(offset_x, offset_y), random_complex(offset_x, offset_y))
def random_quad():
offset_x = r.random() * distribution
offset_y = r.random() * distribution
return QuadraticBezier(random_complex(offset_x, offset_y), random_complex(offset_x, offset_y), random_complex(offset_x, offset_y))
def random_cubic():
offset_x = r.random() * distribution
offset_y = r.random() * distribution
return CubicBezier(random_complex(offset_x, offset_y), random_complex(offset_x, offset_y), random_complex(offset_x, offset_y), random_complex(offset_x, offset_y))
def random_path():
path = Path()
for i in range(count):
type_segment = random.randint(0, 3)
if type_segment == 0:
path.append(random_line())
if type_segment == 1:
path.append(random_quad())
if type_segment == 2:
path.append(random_cubic())
return path
path1 = random_path()
path2 = random_path()
t = time.time()
intersections = path1.intersect(path2)
print("\nFound {} intersections in {} seconds.\n"
"".format(len(intersections), time.time() - t))
def test_line_line_0(self): def test_line_line_0(self):
l0 = Line(start=(25.389999999999997+99.989999999999995j), l0 = Line(start=(25.389999999999997+99.989999999999995j),
end=(25.389999999999997+90.484999999999999j)) end=(25.389999999999997+90.484999999999999j))
@ -1633,9 +1595,9 @@ class Test_intersect(unittest.TestCase):
assert_intersections(self, a, l, intersections, 0) assert_intersections(self, a, l, intersections, 0)
random.seed() random.seed()
for arc_index in range(50): for _ in range(50):
a = random_arc() a = random_arc()
for line_index in range(100): for __ in range(100):
l = random_line() l = random_line()
intersections = a.intersect(l) intersections = a.intersect(l)
msg = 'Generated: arc = {}, line = {}'.format(a, l) msg = 'Generated: arc = {}, line = {}'.format(a, l)
@ -1792,7 +1754,8 @@ class Test_intersect(unittest.TestCase):
assert_intersections(self, a0, a1, intersections, 0) assert_intersections(self, a0, a1, intersections, 0)
class TestPathTools(unittest.TestCase): # noinspection PyTypeChecker
class TestPathTools(TestCase):
# moved from test_pathtools.py # moved from test_pathtools.py
def setUp(self): def setUp(self):
@ -2027,7 +1990,7 @@ class TestPathTools(unittest.TestCase):
def test_path_area(self): def test_path_area(self):
if not RUN_SLOW_TESTS: if not RUN_SLOW_TESTS:
warnings.warn("Skipping `test_path_area` as RUN_SLOW_TESTS is false.") # warnings.warn("Skipping `test_path_area` as RUN_SLOW_TESTS is false.")
return return
cw_square = Path() cw_square = Path()
cw_square.append(Line((0+0j), (0+100j))) cw_square.append(Line((0+0j), (0+100j)))
@ -2083,7 +2046,8 @@ class TestPathTools(unittest.TestCase):
self.assertTrue(enclosing_shape.is_contained_by(larger_shape)) self.assertTrue(enclosing_shape.is_contained_by(larger_shape))
class TestPathBugs(unittest.TestCase): # noinspection PyTypeChecker
class TestPathBugs(TestCase):
def test_issue_113(self): def test_issue_113(self):
""" """
@ -2107,9 +2071,21 @@ class TestPathBugs(unittest.TestCase):
self.assertAlmostEqual(p.length(), 236.70287281737836, delta=TOL) self.assertAlmostEqual(p.length(), 236.70287281737836, delta=TOL)
def test_issue_71(self): def test_issue_71(self):
p = Path("M327 468z") """Test that degenerate (point-like) paths behave properly."""
m = p.closed # degenerate (point-like) closed path
q = p.d() # Failing to Crash is good. d_string = "M327 468z"
path = Path(d_string)
warning_type = warnings.WarningMessage
with AssertWarns(self, warning_type):
self.assertTrue(path.closed)
# test the Path.d() method reproduces an empty d-string
# note that ideally this would reproduce the original, but
# as a Path is a sequence of Bezier segments and arcs, and this
# d-string contains no Bezier segments or arcs, this output seems
# like an acceptable compromise
self.assertEqual(path.d(), '')
def test_issue_95(self): def test_issue_95(self):
""" """
@ -2128,4 +2104,5 @@ class TestPathBugs(unittest.TestCase):
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() from unittest import main
main()

View File

@ -1,15 +1,7 @@
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import Path, Line, Arc, svg2paths, svgstr2paths from svgpathtools import svg2paths, Path, Line, Arc
from io import StringIO
from io import open # overrides build-in open for compatibility with python2
import os
from os.path import join, dirname from os.path import join, dirname
from sys import version_info
import tempfile
import shutil
from svgpathtools.svg_to_paths import rect2pathd
class TestSVG2Paths(unittest.TestCase): class TestSVG2Paths(unittest.TestCase):
@ -24,8 +16,8 @@ class TestSVG2Paths(unittest.TestCase):
Line(105.5+50j, 55.5+0j) Line(105.5+50j, 55.5+0j)
) )
self.assertTrue(path.isclosed()) self.assertTrue(path.isclosed())
self.assertTrue(len(path)==3) self.assertEqual(len(path), 3)
self.assertTrue(path==path_correct) self.assertEqual(path, path_correct)
# triangular quadrilateral (with a redundant 4th "closure" point) # triangular quadrilateral (with a redundant 4th "closure" point)
path = paths[1] path = paths[1]
@ -35,8 +27,8 @@ class TestSVG2Paths(unittest.TestCase):
Line(0+0j, 0+0j) # result of redundant point Line(0+0j, 0+0j) # result of redundant point
) )
self.assertTrue(path.isclosed()) self.assertTrue(path.isclosed())
self.assertTrue(len(path)==4) self.assertEqual(len(path), 4)
self.assertTrue(path==path_correct) self.assertEqual(path, path_correct)
def test_svg2paths_ellipses(self): def test_svg2paths_ellipses(self):
@ -46,8 +38,8 @@ class TestSVG2Paths(unittest.TestCase):
path_ellipse = paths[0] path_ellipse = paths[0]
path_ellipse_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j), 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)) Arc(150+100j, 50+50j, 0.0, True, False, 50+100j))
self.assertTrue(len(path_ellipse)==2) self.assertEqual(len(path_ellipse), 2)
self.assertTrue(path_ellipse==path_ellipse_correct) self.assertEqual(path_ellipse, path_ellipse_correct)
self.assertTrue(path_ellipse.isclosed()) self.assertTrue(path_ellipse.isclosed())
# circle tests # circle tests
@ -55,82 +47,7 @@ class TestSVG2Paths(unittest.TestCase):
path_circle = paths[0] path_circle = paths[0]
path_circle_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j), 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)) Arc(150+100j, 50+50j, 0.0, True, False, 50+100j))
self.assertTrue(len(path_circle)==2) self.assertEqual(len(path_circle), 2)
self.assertTrue(path_circle==path_circle_correct) self.assertEqual(path_circle, path_circle_correct)
self.assertTrue(path_circle.isclosed()) self.assertTrue(path_circle.isclosed())
# test for issue #198 (circles not being closed)
svg = u"""<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="40mm" height="40mm"
viewBox="0 0 40 40" version="1.1">
<g id="layer">
<circle id="c1" cx="20.000" cy="20.000" r="11.000" />
<circle id="c2" cx="20.000" cy="20.000" r="5.15" />
</g>
</svg>"""
tmpdir = tempfile.mkdtemp()
svgfile = os.path.join(tmpdir, 'test.svg')
with open(svgfile, 'w') as f:
f.write(svg)
paths, _ = svg2paths(svgfile)
self.assertEqual(len(paths), 2)
self.assertTrue(paths[0].isclosed())
self.assertTrue(paths[1].isclosed())
shutil.rmtree(tmpdir)
def test_rect2pathd(self):
non_rounded = {"x":"10", "y":"10", "width":"100","height":"100"}
self.assertEqual(rect2pathd(non_rounded), 'M10.0 10.0 L 110.0 10.0 L 110.0 110.0 L 10.0 110.0 z')
rounded = {"x":"10", "y":"10", "width":"100","height":"100", "rx":"15", "ry": "12"}
self.assertEqual(rect2pathd(rounded), "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")
def test_from_file_path_string(self):
"""Test reading svg from file provided as path"""
paths, _ = svg2paths(join(dirname(__file__), 'polygons.svg'))
self.assertEqual(len(paths), 2)
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')
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:
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:
# read entire file into string
file_content = file.read()
# prepare stringio object
file_as_stringio = StringIO(file_content)
paths, _ = svg2paths(file_as_stringio)
self.assertEqual(len(paths), 2)
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:
# read entire file into string
file_content = file.read()
paths, _ = svgstr2paths(file_content)
self.assertEqual(len(paths), 2)
if __name__ == '__main__':
unittest.main()