Compare commits

..

3 Commits

Author SHA1 Message Date
Andrew Port 026b1dc6e6 updated from master 2020-12-08 19:57:11 -08:00
Andrew Port 2824a26a6c Merge remote-tracking branch 'origin/master' into vectorize-path-point 2020-12-08 19:56:46 -08:00
Andrew Port 8fd4fd73b8 added vectorized implementation of Path.point 2020-12-02 00:34:22 -08:00
55 changed files with 1827 additions and 1504 deletions

View File

@ -1,14 +0,0 @@
name: Codacy
on: ["push"]
jobs:
codacy-analysis-cli:
name: Codacy Analysis CLI
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@main
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@master

View File

@ -1,65 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
pull_request:
schedule:
- cron: '30 2 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

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

@ -1,34 +0,0 @@
name: Github CI Unit Testing
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
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

@ -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,42 +0,0 @@
name: Publish to PyPI if new version
on:
push:
tags:
- 'v*'
jobs:
build-n-publish:
name: Build and publish to TestPyPI and PyPI
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/
- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}

1
.gitignore vendored
View File

@ -5,4 +5,3 @@ build
svgpathtools.egg-info
!.travis.yml
!/.gitignore
!/.github

15
.travis.yml Normal file
View File

@ -0,0 +1,15 @@
language: python
python:
- '2.7'
- '3.6'
install:
- pip install numpy svgwrite scipy
script:
- python -m unittest discover test
deploy:
provider: pypi
username: __token__
password:
secure: DaP6S0yj9KlXICtYCWkd54f+yulPYMqr01giOFs2xv5kvMHF1GV160XvYdeLDfwLwAsjKr7438vhgeik99T9iWVb9SYQQsOj8zx+uKcNvNxY/+cHxN9joVOMbhowiqELtKbyNSf1fzax8CGv5+L6k7HfiFV6Zxy4r/4a7JJnavqlsyFn2fixD5FmY+gXjMlnWAer3Q9/1GCWhoUEkON9TRVUcZkYvvBGG16A3m/5o9brCiilaNH5lMiHjOQfbEjxYRgIy6wLGOT9u7EwYAhbnzKhqiaR2jQ5ZxFn52CVoy3r8+T9zvVtToDcjdn9bI+CEFiU37FR1sdJhANFozsoVT3LZid97e7BuMa5+UfFZWZIU7SOcPkzOYxEGwrwqrBWfSeQ3/R76fCcVyy4mNh7YJ5q/89y+NbIpWl6LxVJIwkfix9SUv0z1w4jFzOk5anzrCAJKtJxVFZSUC70ERPaDRK3t9jrZZNS9LIh7wcWGHuGyD4h/8gZr9y3STmNImDq1KdJnldgnOefIS2OXwM5IRkxKvtGeWgn123mp/1XoKDGX8ZhoWp0W0y1A9nZtOkWFjTUjmmj8SeD2R98KBQACsTUqTg3jtZR5dh3VHIPvUc2GeMXQVTZJspi9nyDhJbTLluMk+694S1nRb6Qv0UgIcz651gnv4s8qa6lGXKxEEg=
on:
tags: true

View File

@ -4,10 +4,6 @@
"cell_type": "markdown",
"metadata": {},
"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",
"\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",
"- **numpy**\n",
"- **svgwrite**\n",
"- **scipy** (optional but recommended for performance)\n",
"\n",
"## Setup\n",
"\n",
"If not already installed, you can **install the prerequisites** using pip.\n",
"\n",
"```bash\n",
"$ pip install numpy\n",
"```\n",
"\n",
"```bash\n",
"$ pip install svgwrite\n",
"```\n",
"\n",
"Then **install svgpathtools**:\n",
"```bash\n",
"$ pip install svgpathtools\n",
"``` \n",
" \n",
"### Alternative Setup\n",
"### Alternative Setup \n",
"You can download the source from Github and install by using the command (from inside the folder containing setup.py):\n",
"\n",
"```bash\n",

View File

@ -1,10 +1,5 @@
[![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)
[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)
# svgpathtools
svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.
## Features
@ -35,10 +30,20 @@ Some included tools:
## Prerequisites
- **numpy**
- **svgwrite**
- **scipy** (optional, but recommended for performance)
## Setup
If not already installed, you can **install the prerequisites** using pip.
```bash
$ pip install numpy
```
```bash
$ pip install svgwrite
```
Then **install svgpathtools**:
```bash
$ pip install svgpathtools
```

View File

@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
To report any security vulnerability, email andyaport@gmail.com

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/svgpathtools-1.4.1-py3.7.egg vendored Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -1,16 +0,0 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" baseProfile="full" height="50px" version="1.1" viewBox="-15.075 -5.075 180.15 60.15" width="150px">
<defs>
<path d="M 10.0,24.0 L 140.0,24.0" id="tp0"/>
<path d="M 10.0,40.0 L 140.0,40.0" id="tp1"/>
</defs>
<a href="https://www.paypal.com/donate?business=4SKJ27AM4EYYA&amp;no_recurring=0&amp;item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&amp;currency_code=USD">
<path d="M 0.0,25.0 C 0.0,0.0 0.0,0.0 75.0,0.0 C 150.0,0.0 150.0,0.0 150.0,25.0 C 150.0,50.0 150.0,50.0 75.0,50.0 C 0.0,50.0 0.0,50.0 0.0,25.0" fill="#34eb86" stroke="#000000" stroke-width="0.15"/>
<text font-size="15" font-weight="bold">
<textPath startOffset="50%" text-anchor="middle" xlink:href="#tp0">Donate to the creator</textPath>
</text>
<text font-size="16">
<textPath startOffset="50%" text-anchor="middle" xlink:href="#tp1">(He's a student.)</textPath>
</text>
</a>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -8,7 +8,7 @@ Note: The relevant matrix transformation for quadratics can be found in the
svgpathtools.bezier module."""
from __future__ import print_function
import numpy as np
from svgpathtools import bezier_point, Path, bpoints2bezier, polynomial2bezier
from svgpathtools import *
class HigherOrderBezier:

View File

@ -7,8 +7,7 @@ Path.continuous_subpaths() method to split a paths into a list of its
continuous subpaths.
"""
from svgpathtools import Path, Line
from svgpathtools import *
def path1_is_contained_in_path2(path1, path2):
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
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
ab_line = Path(Line(a, b))
number_of_intersections = len(ab_line.intersect(path2))
A = path1.start # pick an arbitrary point in path1
AB_line = Path(Line(A, B))
number_of_intersections = len(AB_line.intersect(path2))
if number_of_intersections % 2: # if number of intersections is odd
return True
else:

View File

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

View File

@ -1,62 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<meta charset="utf-8" />
<title>svgpathtools in JS!</title>
</head>
<body>
<button id="go_button" onclick="tick()" hidden>Click Me!</button>
<br />
<br />
<div>Output:</div>
<label for="output"></label>
<textarea id="output" style="width: 100%;" rows="6" disabled></textarea>
<svg height="100" width="100">
<circle cx="50" cy="50" r="40" stroke-width="2" stroke="black" fill="blue"/>
<path id="ticker" d="M 50 50 L 50 15" stroke-width="2" stroke="black"/>
<circle cx="50" cy="50" r="3" stroke-width="2" stroke="black" fill="green"/>
Sorry, your browser does not support inline SVG.
</svg>
<script>
// init Pyodide environment and install svgpathtools
async function main() {
let pyodide = await loadPyodide({
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
});
await pyodide.loadPackage("micropip");
pyodide.runPythonAsync(`
import micropip
await micropip.install('svgpathtools')
`);
output.value += "svgpathtools is ready!\n";
return pyodide;
}
async function tick() {
let clock_hand = document.getElementById("ticker");
let pyodide = await pyodideReadyPromise;
try {
let result = pyodide.runPython(`
from svgpathtools import parse_path
parse_path('${clock_hand.getAttribute('d')}').rotated(45, origin=50+50j).d()
`);
clock_hand.setAttribute('d', result);
} catch (err) {
output.value += err;
}
}
let pyodideReadyPromise = main();
$(document).ready(function(){
const output = document.getElementById("output");
output.value = "Initializing...\n";
document.getElementById("go_button").removeAttribute('hidden');
});
</script>
</body>
</html>

View File

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

View File

@ -3,17 +3,18 @@ import codecs
import os
VERSION = '1.6.1'
VERSION = '1.4.1'
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:
def read(*parts):
"""
Build an absolute path from *parts* and and return the contents of the
resulting file. Assume UTF-8 encoding.
"""
HERE = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
return f.read()
@ -26,12 +27,12 @@ setup(name='svgpathtools',
long_description_content_type='text/markdown',
author=AUTHOR_NAME,
author_email=AUTHOR_EMAIL,
url=GITHUB,
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
''.format(GITHUB, VERSION, VERSION),
url='https://github.com/mathandy/svgpathtools',
# download_url = 'http://github.com/mathandy/svgpathtools/tarball/'+VERSION,
license='MIT',
install_requires=['numpy', 'svgwrite', 'scipy'],
install_requires=['numpy', 'svgwrite'],
platforms="OS Independent",
requires=['numpy', 'svgwrite'],
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
classifiers=[
"Development Status :: 4 - Beta",
@ -40,14 +41,6 @@ setup(name='svgpathtools',
"Operating System :: OS Independent",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
"Topic :: Scientific/Engineering",
"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
try:
from .svg_to_paths import svg2paths, svg2paths2, svgstr2paths
from .svg_to_paths import svg2paths, svg2paths2
except ImportError:
pass

View File

@ -13,7 +13,7 @@ An Historic Note:
Example:
Typical usage looks something like the following.
>> from svgpathtools import Document
>> from svgpathtools import *
>> doc = Document('my_file.html')
>> for path in doc.paths():
>> # Do something with the transformed Path object.
@ -41,10 +41,8 @@ import xml.etree.ElementTree as etree
from xml.etree.ElementTree import Element, SubElement, register_namespace
from xml.dom.minidom import parseString
import warnings
from io import StringIO
from tempfile import gettempdir
from time import time
import numpy as np
# Internal dependencies
from .parser import parse_path
@ -52,17 +50,13 @@ from .parser import parse_transform
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
polyline2pathd, polygon2pathd, rect2pathd)
from .misctools import open_in_browser
from .path import transform, Path, is_path_segment
from .path import *
# To maintain forward/backward compatibility
try:
string = basestring
str = basestring
except NameError:
string = str
try:
from os import PathLike
except ImportError:
PathLike = string
pass
# Let xml.etree.ElementTree know about the SVG namespace
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
@ -241,14 +235,13 @@ class Document:
The output Path objects will be transformed based on their parent groups.
Args:
filepath (str or file-like): The filepath of the
DOM-style object or a file-like object containing it.
filepath (str): The filepath of the DOM-style object.
"""
# strings are interpreted as file location everything else is treated as
# file-like object and passed to the xml parser directly
from_filepath = isinstance(filepath, string) or isinstance(filepath, PathLike)
self.original_filepath = os.path.abspath(filepath) if from_filepath else None
# remember location of original svg file
self.original_filepath = filepath
if filepath is not None and os.path.dirname(filepath) == '':
self.original_filepath = os.path.join(os.getcwd(), filepath)
if filepath is None:
self.tree = etree.ElementTree(Element('svg'))
@ -258,14 +251,6 @@ class Document:
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,
path_filter=lambda x: True, path_conversions=CONVERSIONS):
"""Returns a list of all paths in the document.
@ -278,7 +263,7 @@ class Document:
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
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
# nested sequence
group = self.get_group(group)
@ -304,7 +289,7 @@ class Document:
# If given a list of strings (one or more), assume it represents
# 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)
elif not isinstance(group, Element):
@ -323,7 +308,7 @@ class Document:
path_svg = path.d()
elif is_path_segment(path):
path_svg = Path(path).d()
elif isinstance(path, string):
elif isinstance(path, str):
# Assume this is a valid d-string.
# TODO: Should we sanity check the input string?
path_svg = path

View File

@ -12,14 +12,12 @@ except ImportError:
from warnings import warn
from operator import itemgetter
import numpy as np
from itertools import tee
from functools import reduce
# these imports were originally from math and cmath, now are from numpy
# in order to encourage code that generalizes to vector inputs
from numpy import sqrt, cos, sin, tan, arccos as acos, arcsin as asin, \
degrees, radians, log, pi, ceil
from numpy import exp, sqrt as csqrt, angle as phase, isnan
from numpy import exp, sqrt as csqrt, angle as phase
try:
from scipy.integrate import quad
@ -43,8 +41,8 @@ except NameError:
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
# Default Parameters ##########################################################
@ -80,14 +78,11 @@ _is_smooth_from_warning = \
def bezier_segment(*bpoints):
if len(bpoints) == 2:
start, end = bpoints
return Line(start, end)
return Line(*bpoints)
elif len(bpoints) == 4:
start, control1, control2, end = bpoints
return CubicBezier(start, control1, control2, end)
return CubicBezier(*bpoints)
elif len(bpoints) == 3:
start, control, end = bpoints
return QuadraticBezier(start, control, end)
return QuadraticBezier(*bpoints)
else:
assert len(bpoints) in (2, 3, 4)
@ -137,7 +132,6 @@ def polygon(*points):
return Path(*[Line(points[i], points[(i + 1) % len(points)])
for i in range(len(points))])
# Conversion###################################################################
def bpoints2bezier(bpoints):
@ -186,22 +180,13 @@ def bez2poly(bez, numpy_ordering=True, return_poly1d=False):
# Geometric####################################################################
def transform_segments_together(path, transformation):
"""Makes sure that, if joints were continuous, they're kept that way."""
transformed_segs = [transformation(seg) for seg in path]
for i, (sa, sb) in enumerate(path.joints()):
if sa.end == sb.start:
transformed_segs[i].end = transformed_segs[(i + 1) % len(path)].start
return Path(*transformed_segs)
def rotate(curve, degs, origin=None):
"""Returns curve rotated by `degs` degrees (CCW) around the point `origin`
(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`.
"""
def rotate_point(z):
def transform(z):
return exp(1j*radians(degs))*(z - origin) + origin
if origin is None:
@ -211,13 +196,12 @@ def rotate(curve, degs, origin=None):
origin = curve.point(0.5)
if isinstance(curve, Path):
transformation = lambda seg: rotate(seg, degs, origin=origin)
return transform_segments_together(curve, transformation)
return Path(*[rotate(seg, degs, origin=origin) for seg in 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):
new_start = rotate_point(curve.start)
new_end = rotate_point(curve.end)
new_start = transform(curve.start)
new_end = transform(curve.end)
new_rotation = curve.rotation + degs
return Arc(new_start, radius=curve.radius, rotation=new_rotation,
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end)
@ -230,8 +214,7 @@ def translate(curve, z0):
"""Shifts the curve by the complex quantity z such that
translate(curve, z0).point(t) = curve.point(t) + z0"""
if isinstance(curve, Path):
transformation = lambda seg: translate(seg, z0)
return transform_segments_together(curve, transformation)
return Path(*[translate(seg, z0) for seg in curve])
elif is_bezier_segment(curve):
return bpoints2bezier([bpt + z0 for bpt in curve.bpoints()])
elif isinstance(curve, Arc):
@ -272,8 +255,7 @@ def scale(curve, sx, sy=None, origin=0j):
return poly2bez(p)
if isinstance(curve, Path):
transformation = lambda seg: scale(seg, sx, sy, origin)
return transform_segments_together(curve, transformation)
return Path(*[scale(seg, sx, sy, origin) for seg in curve])
elif is_bezier_segment(curve):
return scale_bezier(curve)
elif isinstance(curve, Arc):
@ -294,10 +276,6 @@ def scale(curve, sx, sy=None, origin=0j):
def transform(curve, 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):
return np.array([[p.real], [p.imag], [1.0]])
@ -308,45 +286,20 @@ def transform(curve, tf):
return v.item(0) + 1j * v.item(1)
if isinstance(curve, Path):
transformation = lambda seg: transform(seg, tf)
return transform_segments_together(curve, transformation)
return Path(*[transform(segment, tf) for segment in curve])
elif is_bezier_segment(curve):
return bpoints2bezier([to_complex(tf.dot(to_point(p)))
for p in curve.bpoints()])
elif isinstance(curve, Arc):
new_start = to_complex(tf.dot(to_point(curve.start)))
new_end = to_complex(tf.dot(to_point(curve.end)))
# Based on https://math.stackexchange.com/questions/2349726/
rx2 = curve.radius.real ** 2
ry2 = curve.radius.imag ** 2
Q = np.array([[1/rx2, 0], [0, 1/ry2]])
invT = np.linalg.inv(tf[:2,:2])
D = reduce(np.matmul, [invT.T, Q, invT])
eigvals, eigvecs = np.linalg.eig(D)
rx = 1 / np.sqrt(eigvals[0])
ry = 1 / np.sqrt(eigvals[1])
new_radius = complex(rx, ry)
xeigvec = eigvecs[:, 0]
rot = np.degrees(np.arccos(xeigvec[0]))
if new_radius.real == 0 or new_radius.imag == 0 :
return Line(new_start, new_end)
new_radius = to_complex(tf.dot(to_vector(curve.radius)))
if tf[0][0] * tf[1][1] >= 0.0:
new_sweep = curve.sweep
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,
large_arc=curve.large_arc, sweep=new_sweep, end=new_end,
autoscale_radius=True)
new_sweep = not curve.sweep
return Arc(new_start, radius=new_radius, rotation=curve.rotation,
large_arc=curve.large_arc, sweep=new_sweep, end=new_end)
else:
raise TypeError("Input `curve` should be a Path, Line, "
"QuadraticBezier, CubicBezier, or Arc object.")
@ -575,7 +528,8 @@ def inv_arclength(curve, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
def crop_bezier(seg, t0, t1):
"""Crop a copy of this `self` from `self.point(t0)` to `self.point(t1)`."""
"""returns a cropped copy of this segment which starts at self.point(t0)
and ends at self.point(t1)."""
assert t0 < t1
if t0 == 0:
cropped_seg = seg.split(t1)[0]
@ -602,9 +556,6 @@ class Line(object):
self.start = start
self.end = end
def __hash__(self):
return hash((self.start, self.end))
def __repr__(self):
return 'Line(start=%s, end=%s)' % (self.start, self.end)
@ -644,7 +595,7 @@ class Line(object):
def points(self, ts):
"""Faster than running Path.point many times."""
return self.poly()(ts)
return self.poly(ts)
def length(self, t0=0, t1=1, error=None, min_depth=None):
"""returns the length of the line segment between t0 and t1."""
@ -715,19 +666,6 @@ class Line(object):
Note: This will fail if the two segments coincide for more than a
finite collection of points.
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):
assert other_seg.end != other_seg.start and self.end != self.start
assert self != other_seg
@ -874,9 +812,6 @@ class QuadraticBezier(object):
# used to know if self._length needs to be updated
self._length_info = {'length': None, 'bpoints': None}
def __hash__(self):
return hash((self.start, self.control, self.end))
def __repr__(self):
return 'QuadraticBezier(start=%s, control=%s, end=%s)' % (
self.start, self.control, self.end)
@ -934,7 +869,7 @@ class QuadraticBezier(object):
def points(self, ts):
"""Faster than running Path.point many times."""
return self.poly()(ts)
return self.poly(ts)
def length(self, t0=0, t1=1, error=None, min_depth=None):
if t0 == 1 and t1 == 0:
@ -946,30 +881,31 @@ class QuadraticBezier(object):
if abs(a) < 1e-12:
s = abs(b)*(t1 - t0)
elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12:
tstar = abs(b)/(2*abs(a))
if t1 < tstar:
return abs(a)*(t0**2 - t1**2) - abs(b)*(t0 - t1)
elif tstar < t0:
return abs(a)*(t1**2 - t0**2) - abs(b)*(t1 - t0)
else:
return abs(a)*(t1**2 + t0**2) - abs(b)*(t1 + t0) + \
abs(b)**2/(2*abs(a))
else:
c2 = 4 * (a.real ** 2 + a.imag ** 2)
c1 = 4 * a_dot_b
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)
gamma = c0 / c2 - beta ** 2
beta = c1/(2*c2)
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)
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):
tstar = abs(b) / (2 * abs(a))
if t1 < tstar:
return abs(a) * (t0 ** 2 - t1 ** 2) - abs(b) * (t0 - t1)
elif tstar < t0:
return abs(a) * (t1 ** 2 - t0 ** 2) - abs(b) * (t1 - t0)
else:
return abs(a) * (t1 ** 2 + t0 ** 2) - abs(b) * (t1 + t0) + \
abs(b) ** 2 / (2 * abs(a))
if t0 == 1 and t1 == 0:
self._length_info['length'] = s
@ -1055,19 +991,6 @@ class QuadraticBezier(object):
self.point(t1) == other_seg.point(t2).
Note: This will fail if the two segments coincide for more than a
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):
return bezier_by_line_intersections(self, other_seg)
elif isinstance(other_seg, QuadraticBezier):
@ -1145,9 +1068,6 @@ class CubicBezier(object):
self._length_info = {'length': None, 'bpoints': None, 'error': None,
'min_depth': None}
def __hash__(self):
return hash((self.start, self.control1, self.control2, self.end))
def __repr__(self):
return 'CubicBezier(start=%s, control1=%s, control2=%s, end=%s)' % (
self.start, self.control1, self.control2, self.end)
@ -1211,7 +1131,7 @@ class CubicBezier(object):
def points(self, ts):
"""Faster than running Path.point many times."""
return self.poly()(ts)
return self.poly(ts)
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
"""Calculate the length of the path up to a certain position"""
@ -1319,51 +1239,37 @@ class CubicBezier(object):
def intersect(self, other_seg, tol=1e-12):
"""Finds the intersections of two segments.
Returns:
(list[tuple[float]]) a list of tuples (t1, t2) such that
self.point(t1) == other_seg.point(t2).
Scope:
This will fail if the two segments coincide for more than a
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 []
returns a list of tuples (t1, t2) such that
self.point(t1) == other_seg.point(t2).
Note: This will fail if the two segments coincide for more than a
finite collection of points."""
if isinstance(other_seg, Line):
return bezier_by_line_intersections(self, other_seg)
elif (isinstance(other_seg, QuadraticBezier) or
isinstance(other_seg, CubicBezier)):
assert self != other_seg
longer_length = max(self.length(), other_seg.length())
return bezier_intersections(
self, other_seg, longer_length=longer_length, tol=tol, tol_deC=tol
)
return bezier_intersections(self, other_seg,
longer_length=longer_length,
tol=tol, tol_deC=tol)
elif isinstance(other_seg, Arc):
return [(t1, t2) for t2, t1 in other_seg.intersect(self)]
t2t1s = other_seg.intersect(self)
return [(t1, t2) for t2, t1 in t2t1s]
elif isinstance(other_seg, Path):
raise TypeError("`other_seg` must be a path segment, not a "
"`Path` object, use `Path.intersect()`.")
raise TypeError(
"other_seg must be a path segment, not a Path object, use "
"Path.intersect().")
else:
raise TypeError("`other_seg` must be a path segment.")
raise TypeError("other_seg must be a path segment.")
def bbox(self):
"""returns bounding box in format (xmin, xmax, ymin, ymax)."""
"""returns the bounding box for the segment in the form
(xmin, xmax, ymin, ymax)."""
return bezier_bounding_box(self)
def split(self, t):
"""Splits a copy of `self` at t and returns the two subsegments."""
"""returns two segments, whose union is this segment and which join at
self.point(t)."""
bpoints1, bpoints2 = split_bezier(self.bpoints(), t)
return CubicBezier(*bpoints1), CubicBezier(*bpoints2)
@ -1375,8 +1281,8 @@ class CubicBezier(object):
def radialrange(self, origin, return_all_global_extrema=False):
"""returns the tuples (d_min, t_min) and (d_max, t_max) which minimize
and maximize, respectively, the distance d = |self.point(t)-origin|."""
return bezier_radialrange(
self, origin, return_all_global_extrema=return_all_global_extrema)
return bezier_radialrange(self, origin,
return_all_global_extrema=return_all_global_extrema)
def rotated(self, degs, origin=None):
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
@ -1398,7 +1304,7 @@ class CubicBezier(object):
class Arc(object):
def __init__(self, start, radius, rotation, large_arc, sweep, end,
autoscale_radius=True):
r"""
"""
This should be thought of as a part of an ellipse connecting two
points on that ellipse, start and end.
Parameters
@ -1736,7 +1642,7 @@ class Arc(object):
if np.isclose(t_x_0, t_y_0):
t = (t_x_0 + t_y_0) / 2.0
elif np.isclose(t_x_0, t_y_1):
t = (t_x_0 + t_y_1) / 2.0
t= (t_x_0 + t_y_1) / 2.0
elif np.isclose(t_x_1, t_y_0):
t = (t_x_1 + t_y_0) / 2.0
elif np.isclose(t_x_1, t_y_1):
@ -1754,48 +1660,33 @@ class Arc(object):
return None
def centeriso(self, z):
"""Isometry to a centered aligned ellipse.
This is an isometry that shifts and rotates `self`'s underlying
ellipse so that it's centered on the origin and has its axes
aligned with the xy-axes.
Args:
z (:obj:`complex` or :obj:`numpy.ndarray[complex]`): a point
to send through the above-described isometry.
Returns:
(:obj:`complex` or :obj:`numpy.ndarray[complex]`) The point(s) f(z),
where f is the above described isometry of the xy-plane (i.e.
the one-dimensional complex plane).
"""
"""This is an isometry that translates and rotates self so that it
is centered on the origin and has its axes aligned with the xy axes."""
return (1/self.rot_matrix)*(z - self.center)
def icenteriso(self, zeta):
"""The inverse of the `centeriso()` method."""
"""This is an isometry, the inverse of standardiso()."""
return self.rot_matrix*zeta + self.center
def u1transform(self, z):
"""Similar to the `centeriso()` method, but maps to the unit circle."""
zeta = self.centeriso(z)
"""This is an affine transformation (same as used in
self._parameterize()) that sends self to the unit circle."""
zeta = (1/self.rot_matrix)*(z - self.center) # same as centeriso(z)
x, y = real(zeta), imag(zeta)
return x/self.radius.real + 1j*y/self.radius.imag
def iu1transform(self, zeta):
"""The inverse of the `u1transform()` method."""
"""This is an affine transformation, the inverse of
self.u1transform()."""
x = real(zeta)
y = imag(zeta)
z = x*self.radius.real + y*self.radius.imag
return self.rot_matrix*z + self.center
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
"""Computes the length of the Arc segment, `self`, from t0 to t1.
Notes:
* The length of an elliptical large_arc segment requires numerical
"""The length of an elliptical large_arc segment requires numerical
integration, and in that case it's simpler to just do a geometric
approximation, as for cubic bezier curves.
"""
approximation, as for cubic bezier curves."""
assert 0 <= t0 <= 1 and 0 <= t1 <= 1
if t0 == 0 and t1 == 1:
@ -1819,17 +1710,8 @@ class Arc(object):
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
"""Approximates the unique `t` such that self.length(0, t) = s.
Args:
s (float): A length between 0 and `self.length()`.
Returns:
(float) The t, such that self.length(0, t) is approximately s.
For more info:
See the inv_arclength() docstring.
"""
"""Returns a float, t, such that self.length(0, t) is approximately s.
See the inv_arclength() docstring for more details."""
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
min_depth=min_depth)
@ -1927,18 +1809,9 @@ class Arc(object):
not self.sweep, self.start)
def phase2t(self, psi):
"""Converts phase to t-value.
I.e. given phase, psi, such that -np.pi < psi <= np.pi, approximates
the unique t-value such that `self.u1transform(self.point(t))` equals
`np.exp(1j*psi)`.
Args:
psi (float): The phase in radians.
Returns:
(float): the corresponding t-value.
"""Given phase -pi < psi <= pi,
returns the t value such that
exp(1j*psi) = self.u1transform(self.point(t)).
"""
def _deg(rads, domain_lower_limit):
# Convert rads to degrees in [0, 360) domain
@ -1957,6 +1830,7 @@ class Arc(object):
degs = _deg(psi, domain_lower_limit=self.theta)
return (degs - self.theta)/self.delta
def intersect(self, other_seg, tol=1e-12):
"""NOT FULLY IMPLEMENTED. Finds the intersections of two segments.
returns a list of tuples (t1, t2) such that
@ -2086,19 +1960,11 @@ class Arc(object):
return intersections
elif is_bezier_segment(other_seg):
# if self and other_seg intersect, they will itersect at the
# same points after being passed through the `u1transform`
# isometry. Since this isometry maps self to the unit circle,
# the intersections will be easy to find (just look for any
# points where other_seg is a distance of one from the origin.
# Moreoever, the t-values that the intersection happen at will
# be unchanged by the isometry.
u1poly = np.poly1d(self.u1transform(other_seg.poly()))
u1poly = self.u1transform(other_seg.poly())
u1poly_mag2 = real(u1poly)**2 + imag(u1poly)**2
t2s = [t for t in polyroots01(u1poly_mag2 - 1) if 0 <= t <= 1]
t2s = polyroots01(u1poly_mag2 - 1)
t1s = [self.phase2t(phase(u1poly(t2))) for t2 in t2s]
return [(t1, t2) for t1, t2 in zip(t1s, t2s) if 0 <= t1 <= 1]
return list(zip(t1s, t2s))
elif isinstance(other_seg, Arc):
assert other_seg != self
@ -2135,23 +2001,19 @@ class Arc(object):
def point_in_seg_interior(point, seg):
t = seg.point_to_t(point)
if (not t or
np.isclose(t, 0.0, rtol=0.0, atol=1e-6) or
np.isclose(t, 1.0, rtol=0.0, atol=1e-6)):
return False
if t is None: return False
if np.isclose(t, 0.0, rtol=0.0, atol=1e-6): return False
if np.isclose(t, 1.0, rtol=0.0, atol=1e-6): return False
return True
# If either end of either segment is in the interior
# of the other segment, then the Arcs overlap
# in an infinite number of points, and we return
# "no intersections".
if (
point_in_seg_interior(self.start, other_seg) or
point_in_seg_interior(self.end, other_seg) or
point_in_seg_interior(other_seg.start, self) or
point_in_seg_interior(other_seg.end, self)
):
return []
if point_in_seg_interior(self.start, other_seg): return []
if point_in_seg_interior(self.end, other_seg): return []
if point_in_seg_interior(other_seg.start, self): return []
if point_in_seg_interior(other_seg.end, self): return []
# If they touch at their endpoint(s) and don't go
# in "overlapping directions", then we accept that
@ -2454,6 +2316,16 @@ class Arc(object):
current_t = next_t
def is_bezier_segment(x):
return (isinstance(x, Line) or
isinstance(x, QuadraticBezier) or
isinstance(x, CubicBezier))
def is_path_segment(x):
return is_bezier_segment(x) or isinstance(x, Arc)
class Path(MutableSequence):
"""A Path is a sequence of path segments"""
@ -2494,9 +2366,6 @@ class Path(MutableSequence):
if 'tree_element' in kw:
self._tree_element = kw['tree_element']
def __hash__(self):
return hash((tuple(self._segments), self._closed))
def __getitem__(self, index):
return self._segments[index]
@ -2509,12 +2378,8 @@ class Path(MutableSequence):
def __delitem__(self, index):
del self._segments[index]
self._length = None
if len(self._segments) > 0:
self._start = self._segments[0].start
self._end = self._segments[-1].end
else:
self._start = None
self._end = None
self._start = self._segments[0].start
self._end = self._segments[-1].end
def __iter__(self):
return self._segments.__iter__()
@ -2563,33 +2428,33 @@ class Path(MutableSequence):
lengths = [each.length(error=error, min_depth=min_depth) for each in
self._segments]
self._length = sum(lengths)
if self._length == 0:
self._lengths = lengths # all lengths are 0.
else:
self._lengths = [each / self._length for each in lengths]
self._lengths = [each/self._length for each in lengths]
def point(self, pos):
def point(self, T):
# Shortcuts
if len(self._segments) == 0:
raise ValueError("This path contains no segments!")
if pos == 0.0:
return self._segments[0].point(pos)
if pos == 1.0:
return self._segments[-1].point(pos)
if T == 0.0:
return self._segments[0].point(T)
if T == 1.0:
return self._segments[-1].point(T)
self._calc_lengths()
# Find which segment the point we search for is located on:
segment_start = 0
for index, segment in enumerate(self._segments):
segment_end = segment_start + self._lengths[index]
if segment_end >= pos:
# This is the segment! How far in on the segment is the point?
segment_pos = (pos - segment_start)/(
segment_end - segment_start)
return segment.point(segment_pos)
segment_start = segment_end
raise RuntimeError("Something has gone wrong. Could not compute Path.point({}) for path {}".format(pos, self))
cumulative_relative_lengths = np.cumsum(self._lengths)
if hasattr(T, '__iter__'):
T = np.array(T).reshape(1, len(T))
relevant_seg_indices = np.argmax(cumulative_relative_lengths[:, None] >= T, axis=0)
T0, T1 = cumulative_relative_lengths[relevant_seg_indices - 1],\
cumulative_relative_lengths[relevant_seg_indices]
t = (T - T0) / (T1 - T0)
return [self[i].point(tval) for i, tval in zip(relevant_seg_indices, t)]
else: # assume T is a scalar
relevant_seg_index = np.argmax(cumulative_relative_lengths >= T)
T0, T1 = cumulative_relative_lengths[relevant_seg_index - 1],\
cumulative_relative_lengths[relevant_seg_index]
t = (T - T0) / (T1 - T0)
return self[relevant_seg_index].point(t)
def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
self._calc_lengths(error=error, min_depth=min_depth)
@ -2646,10 +2511,7 @@ class Path(MutableSequence):
return self.start == self.end
def _is_closable(self):
try:
end = self[-1].end
except IndexError:
return True
end = self[-1].end
for segment in self:
if segment.start == end:
return True
@ -2677,34 +2539,31 @@ class Path(MutableSequence):
@property
def start(self):
if not self._start and len(self._segments)>0:
if not self._start:
self._start = self._segments[0].start
return self._start
@start.setter
def start(self, pt):
self._start = pt
if len(self._segments)>0:
self._segments[0].start = pt
self._segments[0].start = pt
@property
def end(self):
if not self._end and len(self._segments)>0:
if not self._end:
self._end = self._segments[-1].end
return self._end
@end.setter
def end(self, pt):
self._end = pt
if len(self._segments)>0:
self._segments[-1].end = pt
self._segments[-1].end = pt
def d(self, useSandT=False, use_closed_attrib=False, rel=False):
"""Returns a path d-string for the path object.
For an explanation of useSandT and use_closed_attrib, see the
compatibility notes in the README."""
if len(self) == 0:
return ''
if use_closed_attrib:
self_closed = self.iscontinuous() and self.isclosed()
if self_closed:
@ -2948,10 +2807,10 @@ class Path(MutableSequence):
area_enclosed += integral(1) - integral(0)
return area_enclosed
def seg2lines(seg_):
def seg2lines(seg):
"""Find piecewise-linear approximation of `seg`."""
num_lines = int(ceil(seg_.length() / chord_length))
pts = [seg_.point(t) for t in np.linspace(0, 1, num_lines+1)]
num_lines = int(ceil(seg.length() / chord_length))
pts = [seg.point(t) for t in np.linspace(0, 1, num_lines+1)]
return [Line(pts[i], pts[i+1]) for i in range(num_lines)]
assert self.isclosed()
@ -2965,29 +2824,20 @@ class Path(MutableSequence):
return area_without_arcs(Path(*bezier_path_approximation))
def intersect(self, other_curve, justonemode=False, tol=1e-12):
"""Finds intersections of `self` with `other_curve`
Args:
other_curve: the path or path segment to check for intersections
with `self`
justonemode (bool): if true, returns only the first
intersection found.
tol (float): A tolerance used to check for redundant intersections
(see comment above the code block where tol is used).
Returns:
(list[tuple[float, Curve, float]]): list of intersections, each
in the format ((T1, seg1, t1), (T2, seg2, t2)), where
self.point(T1) == seg1.point(t1) == seg2.point(t2) == other_curve.point(T2)
Scope:
If the two path objects coincide for more than a finite set of
points, this code will iterate to max depth and/or raise an error.
"""
"""returns list of pairs of pairs ((T1, seg1, t1), (T2, seg2, t2))
giving the intersection points.
If justonemode==True, then returns just the first
intersection found.
tol is used to check for redundant intersections (see comment above
the code block where tol is used).
Note: If the two path objects coincide for more than a finite set of
points, this code will fail."""
path1 = self
path2 = other_curve if isinstance(other_curve, Path) else Path(other_curve)
if isinstance(other_curve, Path):
path2 = other_curve
else:
path2 = Path(other_curve)
assert path1 != path2
intersection_list = []
for seg1 in path1:
for seg2 in path2:
@ -2997,7 +2847,6 @@ class Path(MutableSequence):
T1 = path1.t2T(seg1, t1)
T2 = path2.t2T(seg2, t2)
intersection_list.append(((T1, seg1, t1), (T2, seg2, t2)))
if justonemode and intersection_list:
return intersection_list[0]
@ -3006,7 +2855,8 @@ class Path(MutableSequence):
# redundant intersection. This code block checks for and removes said
# redundancies.
if intersection_list:
pts = [_seg1.point(_t1) for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]]
pts = [seg1.point(_t1)
for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]]
indices2remove = []
for ind1 in range(len(pts)):
for ind2 in range(ind1 + 1, len(pts)):
@ -3019,7 +2869,8 @@ class Path(MutableSequence):
return intersection_list
def bbox(self):
"""returns bounding box in the form (xmin, xmax, ymin, ymax)."""
"""returns a bounding box for the input Path object in the form
(xmin, xmax, ymin, ymax)."""
bbs = [seg.bbox() for seg in self._segments]
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
xmin = min(xmins)
@ -3167,18 +3018,6 @@ class Path(MutableSequence):
arc_required = int(ceil(abs(segment.delta) / sweep_limit))
self[s:s+1] = list(segment.as_quad_curves(arc_required))
def joints(self):
"""returns generator of segment joints
I.e. Path(s0, s1, s2, ..., sn).joints() returns generator
(s0, s1), (s1, s2), ..., (sn, s0)
credit: https://docs.python.org/3/library/itertools.html#recipes
"""
a, b = tee(self)
next(b, None)
return zip(a, b)
def _tokenize_path(self, pathdef):
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:

View File

@ -100,7 +100,7 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
mindim=600, dimensions=None, viewbox=None, text=None,
text_path=None, font_size=None, attributes=None,
svg_attributes=None, svgwrite_debug=False,
paths2Drawing=False, baseunit='px'):
paths2Drawing=False):
"""Creates (and optionally displays) an SVG file.
REQUIRED INPUTS:
@ -214,13 +214,10 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
timestamp = True if timestamp is None else timestamp
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
if timestamp:
fbname, fext = os_path.splitext(filename)
dirname = os_path.dirname(filename)
tstamp = str(time()).replace('.', '')
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
filename = os_path.join(dirname, stfilename)
@ -316,16 +313,12 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
dy += 2*margin_size*dy + extra_space_for_style
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
if mindim is None:
szx = "{}{}".format(dx, baseunit)
szy = "{}{}".format(dy, baseunit)
if dx > dy:
szx = str(mindim) + 'px'
szy = str(int(ceil(mindim * dy / dx))) + 'px'
else:
if dx > dy:
szx = str(mindim) + baseunit
szy = str(int(ceil(mindim * dy / dx))) + baseunit
else:
szx = str(int(ceil(mindim * dx / dy))) + baseunit
szy = str(mindim) + baseunit
szx = str(int(ceil(mindim * dx / dy))) + 'px'
szy = str(mindim) + 'px'
dimensions = szx, szy
# Create an SVG file
@ -410,6 +403,9 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
if paths2Drawing:
return dwg
# save svg
if not os_path.exists(os_path.dirname(filename)):
makedirs(os_path.dirname(filename))
dwg.save()
# re-open the svg, make the xml pretty, and save it again
@ -432,7 +428,7 @@ def wsvg(paths=None, colors=None, filename=None, stroke_widths=None,
mindim=600, dimensions=None, viewbox=None, text=None,
text_path=None, font_size=None, attributes=None,
svg_attributes=None, svgwrite_debug=False,
paths2Drawing=False, baseunit='px'):
paths2Drawing=False):
"""Create SVG and write to disk.
Note: This is identical to `disvg()` except that `openinbrowser`
@ -451,7 +447,7 @@ def wsvg(paths=None, colors=None, filename=None, stroke_widths=None,
text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes,
svgwrite_debug=svgwrite_debug,
paths2Drawing=paths2Drawing, baseunit=baseunit)
paths2Drawing=paths2Drawing)
def paths2Drawing(paths=None, colors=None, filename=None,
@ -460,7 +456,7 @@ def paths2Drawing(paths=None, colors=None, filename=None,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None,
font_size=None, attributes=None, svg_attributes=None,
svgwrite_debug=False, paths2Drawing=True, baseunit='px'):
svgwrite_debug=False, paths2Drawing=True):
"""Create and return `svg.Drawing` object.
Note: This is identical to `disvg()` except that `paths2Drawing`
@ -478,4 +474,4 @@ def paths2Drawing(paths=None, colors=None, filename=None,
text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes,
svgwrite_debug=svgwrite_debug,
paths2Drawing=paths2Drawing, baseunit=baseunit)
paths2Drawing=paths2Drawing)

View File

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

View File

@ -4,13 +4,8 @@ The main tool being the svg2paths() function."""
# External dependencies
from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse
import os
from io import StringIO
from os import path as os_path, getcwd
import re
try:
from os import PathLike as FilePathLike
except ImportError:
FilePathLike = str
# Internal dependencies
from .parser import parse_path
@ -22,11 +17,9 @@ COORD_PAIR_TMPLT = re.compile(
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
)
def path2pathd(path):
return path.get('d', '')
def ellipse2pathd(ellipse):
"""converts the parameters from an ellipse or a circle to a string for a
Path object d-attribute"""
@ -51,7 +44,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'
return d + 'z'
return d
def polyline2pathd(polyline, is_polygon=False):
@ -91,39 +84,14 @@ def rect2pathd(rect):
The rectangle will start at the (x,y) coordinate specified by the
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))
if 'rx' in rect or 'ry' in rect:
# if only one, rx or ry, is present, use that value for both
# 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
x1, y1 = x0 + w, y0
x2, y2 = x0 + w, y0 + h
x3, y3 = x0, y0 + h
d = ("M{} {} L {} {} L {} {} L {} {} z"
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
return d
@ -149,9 +117,7 @@ def svg2paths(svg_file_location,
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
Args:
svg_file_location (string or file-like object): the location of the
svg file on disk or a file-like object containing the content of a
svg file
svg_file_location (string): the location of the svg file
return_svg_attributes (bool): Set to True and a dictionary of
svg-attributes will be extracted and returned. See also the
`svg2paths2()` function.
@ -175,10 +141,8 @@ def svg2paths(svg_file_location,
list: The list of corresponding path attribute dictionaries.
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
"""
# strings are interpreted as file location everything else is treated as
# file-like object and passed to the xml parser directly
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
if os_path.dirname(svg_file_location) == '':
svg_file_location = os_path.join(getcwd(), svg_file_location)
doc = parse(svg_file_location)
@ -258,26 +222,3 @@ def svg2paths2(svg_file_location,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_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)

1221
tags Normal file

File diff suppressed because it is too large Load Diff

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

@ -1,7 +1,7 @@
from __future__ import division, absolute_import, print_function
import numpy as np
import unittest
from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier
from svgpathtools.bezier import *
from svgpathtools.path import bpoints2bezier

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

@ -2,7 +2,7 @@
#------------------------------------------------------------------------------
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import parse_path
from svgpathtools import *
class TestGeneration(unittest.TestCase):

View File

@ -5,15 +5,11 @@ $ python -m unittest test.test_groups.TestGroups.test_group_flatten
"""
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import Document, SVG_NAMESPACE, parse_path, Line, Arc
from svgpathtools import *
from os.path import join, dirname
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):
return next(p for p in paths
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_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):
# Test the Document.paths() function against the
# groups.svg test file.
@ -256,10 +236,3 @@ class TestGroups(unittest.TestCase):
path = parse_path(path_d)
svg_path = doc.add_path(path, group=new_leaf)
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)
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, parse_path
from svgpathtools import *
import svgpathtools
import numpy as np

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import unittest
import numpy as np
# Internal dependencies
from svgpathtools import rational_limit
from svgpathtools import *
class Test_polytools(unittest.TestCase):

View File

@ -1,6 +1,6 @@
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import SaxDocument
from svgpathtools import *
from os.path import join, dirname

View File

@ -1,16 +1,7 @@
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import Path, Line, Arc, svg2paths, svgstr2paths
from io import StringIO
from io import open # overrides build-in open for compatibility with python2
import os
from svgpathtools import *
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):
def test_svg2paths_polygons(self):
@ -59,78 +50,3 @@ class TestSVG2Paths(unittest.TestCase):
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"""<?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()