Compare commits
20 Commits
master
...
security-u
Author | SHA1 | Date |
---|---|---|
Andrew Port | 67fd6e885d | |
Andrew Port | f2eb3d0596 | |
Andrew Port | 2422d15251 | |
Andrew Port | 2368627a17 | |
Andrew Port | 0c5dc9de1a | |
Andrew Port | 657a9d6745 | |
Andrew Port | 3e1f8e00a5 | |
Andrew Port | 05408cfa26 | |
Andrew Port | e71d2d4282 | |
Andrew Port | 413a2864f6 | |
Andrew Port | a2b62fc011 | |
Andrew Port | d86c63214b | |
Andrew Port | d2b1ea5770 | |
Andrew Port | da050a2eeb | |
Andrew Port | 0a31f348d6 | |
Andrew Port | 9863e7050a | |
Andrew Port | 11682a3363 | |
Andrew Port | 4f615f9a9d | |
Andrew Port | ace8522c19 | |
Andrew Port | d881b21b47 |
|
@ -13,7 +13,10 @@ name: "CodeQL"
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '30 2 * * 3'
|
||||
|
||||
|
@ -30,6 +33,9 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
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:
|
||||
- name: Checkout repository
|
||||
|
|
|
@ -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
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
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:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -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/
|
|
@ -1,14 +1,12 @@
|
|||
name: Publish to PyPI if new version
|
||||
name: Publish to TestPyPI and if new version PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
branches: [ master ]
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish to TestPyPI and PyPI
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Set up Python 3
|
||||
|
@ -30,13 +28,13 @@ jobs:
|
|||
--outdir dist/
|
||||
.
|
||||
- name: Publish to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
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
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
18
README.ipynb
18
README.ipynb
|
@ -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",
|
||||
|
|
13
README.md
13
README.md
|
@ -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)
|
||||
![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)
|
||||
[![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)
|
||||
# svgpathtools
|
||||
|
||||
|
@ -35,10 +36,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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
[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
|
||||
easily be generalized to paths containing `Line` and `QuadraticBezier` objects
|
||||
also.
|
||||
|
||||
Note: The relevant matrix transformation for quadratics can be found in the
|
||||
svgpathtools.bezier module."""
|
||||
svgpathtools.bezier module.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import numpy as np
|
||||
from svgpathtools import bezier_point, Path, bpoints2bezier, polynomial2bezier
|
||||
from svgpathtools import bezier_point, bpoints2bezier, polynomial2bezier, Path
|
||||
|
||||
|
||||
class HigherOrderBezier:
|
||||
"""Bezier curve of arbitrary degree"""
|
||||
def __init__(self, bpoints):
|
||||
self.bpts = bpoints
|
||||
|
||||
|
@ -38,7 +44,7 @@ def points_in_each_seg_slow(path, tvals):
|
|||
|
||||
def points_in_each_seg(path, 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, 3, 0, 0],
|
||||
[ 1, 0, 0, 0]])
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
numpy
|
||||
svgwrite
|
||||
scipy
|
||||
defusedxml
|
7
setup.py
7
setup.py
|
@ -3,7 +3,7 @@ import codecs
|
|||
import os
|
||||
|
||||
|
||||
VERSION = '1.6.1'
|
||||
VERSION = '1.4.2'
|
||||
AUTHOR_NAME = 'Andy Port'
|
||||
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
|
||||
GITHUB = 'https://github.com/mathandy/svgpathtools'
|
||||
|
@ -30,8 +30,9 @@ setup(name='svgpathtools',
|
|||
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
|
||||
''.format(GITHUB, VERSION, VERSION),
|
||||
license='MIT',
|
||||
install_requires=['numpy', 'svgwrite', 'scipy'],
|
||||
install_requires=['numpy', 'svgwrite', 'defusedxml'],
|
||||
platforms="OS Independent",
|
||||
requires=['numpy', 'svgwrite', 'defusedxml'],
|
||||
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
|
@ -46,8 +47,6 @@ setup(name='svgpathtools',
|
|||
"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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,13 +31,9 @@ def bezier_point(p, t):
|
|||
Warning: Be concerned about numerical stability when using this function
|
||||
with high order curves."""
|
||||
|
||||
# begin arc support block ########################
|
||||
try:
|
||||
p.large_arc
|
||||
# for Arc support
|
||||
if hasattr(p, 'radius'):
|
||||
return p.point(t)
|
||||
except:
|
||||
pass
|
||||
# end arc support block ##########################
|
||||
|
||||
deg = len(p) - 1
|
||||
if deg == 3:
|
||||
|
@ -145,14 +141,11 @@ def split_bezier(bpoints, t):
|
|||
|
||||
|
||||
def halve_bezier(p):
|
||||
"""split path segment into two halves at t=0.5"""
|
||||
|
||||
# begin arc support block ########################
|
||||
try:
|
||||
p.large_arc
|
||||
# for Arc support
|
||||
if hasattr(p, 'radius'):
|
||||
return p.split(0.5)
|
||||
except:
|
||||
pass
|
||||
# end arc support block ##########################
|
||||
|
||||
if len(p) == 4:
|
||||
return ([p[0], (p[0] + p[1])/2, (p[0] + 2*p[1] + p[2])/4,
|
||||
|
@ -199,13 +192,9 @@ def bezier_bounding_box(bez):
|
|||
(xmin, xmax, ymin, ymax).
|
||||
Warning: For the non-cubic case this is not particularly efficient."""
|
||||
|
||||
# begin arc support block ########################
|
||||
try:
|
||||
bla = bez.large_arc
|
||||
return bez.bbox() # added to support Arc objects
|
||||
except:
|
||||
pass
|
||||
# end arc support block ##########################
|
||||
# for Arc support
|
||||
if hasattr(bez, 'radius'):
|
||||
return bez.bbox()
|
||||
|
||||
if len(bez) == 4:
|
||||
xmin, xmax = bezier_real_minmax([p.real for p in bez])
|
||||
|
|
|
@ -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.
|
||||
|
@ -36,15 +36,18 @@ A Big Problem:
|
|||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import os
|
||||
import sys
|
||||
import collections
|
||||
import xml.etree.ElementTree as etree
|
||||
from xml.etree.ElementTree import Element, SubElement, register_namespace
|
||||
from xml.dom.minidom import parseString
|
||||
from defusedxml.cElementTree import parse, tostring
|
||||
from xml.etree.cElementTree import register_namespace
|
||||
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
|
||||
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 +55,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'}
|
||||
|
@ -103,9 +102,6 @@ def flattened_paths(group, group_filter=lambda x: True,
|
|||
only convert explicit path elements, pass in
|
||||
`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
|
||||
if not group_filter(group):
|
||||
|
@ -241,31 +237,22 @@ 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'))
|
||||
self.tree = ElementTree(Element('svg'))
|
||||
else:
|
||||
# parse svg to ElementTree object
|
||||
self.tree = etree.parse(filepath)
|
||||
self.tree = parse(filepath)
|
||||
|
||||
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 +265,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 +291,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 +310,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
|
||||
|
@ -431,7 +418,7 @@ class Document:
|
|||
SVG_NAMESPACE['svg']), group_attribs)
|
||||
|
||||
def __repr__(self):
|
||||
return etree.tostring(self.tree.getroot()).decode()
|
||||
return tostring(self.tree.getroot()).decode()
|
||||
|
||||
def pretty(self, **kwargs):
|
||||
return parseString(repr(self)).toprettyxml(**kwargs)
|
||||
|
|
|
@ -43,8 +43,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 ##########################################################
|
||||
|
||||
|
@ -189,6 +189,7 @@ def bez2poly(bez, numpy_ordering=True, return_poly1d=False):
|
|||
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]
|
||||
joint_was_continuous = [sa.end == sb.start for sa, sb in path.joints()]
|
||||
|
||||
for i, (sa, sb) in enumerate(path.joints()):
|
||||
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
|
||||
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:
|
||||
|
@ -214,10 +215,10 @@ def rotate(curve, degs, origin=None):
|
|||
transformation = lambda seg: rotate(seg, degs, origin=origin)
|
||||
return transform_segments_together(curve, transformation)
|
||||
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)
|
||||
|
@ -294,10 +295,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]])
|
||||
|
||||
|
@ -318,7 +315,7 @@ def transform(curve, tf):
|
|||
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/
|
||||
# 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
|
||||
ry2 = curve.radius.imag ** 2
|
||||
|
||||
|
@ -338,14 +335,10 @@ def transform(curve, tf):
|
|||
|
||||
if new_radius.real == 0 or new_radius.imag == 0 :
|
||||
return Line(new_start, new_end)
|
||||
else:
|
||||
if tf[0][0] * tf[1][1] >= 0.0:
|
||||
new_sweep = curve.sweep
|
||||
else:
|
||||
new_sweep = not curve.sweep
|
||||
else :
|
||||
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)
|
||||
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end,
|
||||
autoscale_radius=False)
|
||||
|
||||
else:
|
||||
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
|
||||
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
|
||||
|
@ -940,6 +920,7 @@ class QuadraticBezier(object):
|
|||
if t0 == 1 and t1 == 0:
|
||||
if self._length_info['bpoints'] == self.bpoints():
|
||||
return self._length_info['length']
|
||||
|
||||
a = self.start - 2*self.control + self.end
|
||||
b = 2*(self.control - self.start)
|
||||
a_dot_b = a.real*b.real + a.imag*b.imag
|
||||
|
@ -947,20 +928,23 @@ class QuadraticBezier(object):
|
|||
if abs(a) < 1e-12:
|
||||
s = abs(b)*(t1 - t0)
|
||||
else:
|
||||
c2 = 4 * (a.real ** 2 + a.imag ** 2)
|
||||
c1 = 4 * a_dot_b
|
||||
c0 = b.real ** 2 + b.imag ** 2
|
||||
with np.testing.suppress_warnings() as sup:
|
||||
sup.filter(RuntimeWarning)
|
||||
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)
|
||||
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):
|
||||
tstar = abs(b) / (2 * abs(a))
|
||||
if t1 < tstar:
|
||||
|
@ -1055,19 +1039,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):
|
||||
|
@ -1328,19 +1299,6 @@ class CubicBezier(object):
|
|||
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) or
|
||||
|
@ -1398,7 +1356,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
|
||||
|
@ -2572,7 +2530,7 @@ class Path(MutableSequence):
|
|||
|
||||
# Shortcuts
|
||||
if len(self._segments) == 0:
|
||||
raise ValueError("This path contains no segments!")
|
||||
return None
|
||||
if pos == 0.0:
|
||||
return self._segments[0].point(pos)
|
||||
if pos == 1.0:
|
||||
|
@ -2589,7 +2547,6 @@ class Path(MutableSequence):
|
|||
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))
|
||||
|
||||
def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
||||
self._calc_lengths(error=error, min_depth=min_depth)
|
||||
|
|
|
@ -8,7 +8,7 @@ from __future__ import division, absolute_import, print_function
|
|||
from math import ceil
|
||||
from os import path as os_path, makedirs
|
||||
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 time import time
|
||||
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
|
||||
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)
|
||||
|
@ -410,6 +407,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
|
||||
|
|
|
@ -10,19 +10,26 @@ from .misctools import isclose
|
|||
|
||||
|
||||
def polyroots(p, realroots=False, condition=lambda r: True):
|
||||
"""Returns the roots of a polynomial with coefficients given in p.
|
||||
|
||||
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
|
||||
|
||||
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)
|
||||
if realroots:
|
||||
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):
|
||||
"""Returns the real roots between 0 and 1 of the polynomial with
|
||||
coefficients given in p,
|
||||
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
|
||||
p can also be a np.poly1d object. See polyroots for more information."""
|
||||
"""Returns the real roots 0 < x < 1 of the polynomial given by `p`.
|
||||
|
||||
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def rational_limit(f, g, t0):
|
||||
"""Computes the limit of the rational function (f/g)(t)
|
||||
as t approaches t0."""
|
||||
"""Computes the limit of the rational function (f/g)(t) as t approaches t0."""
|
||||
assert isinstance(f, np.poly1d) and isinstance(g, np.poly1d)
|
||||
assert g != np.poly1d([0])
|
||||
if g(t0) != 0:
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import os
|
||||
from xml.etree.ElementTree import iterparse, Element, ElementTree, SubElement
|
||||
import numpy as np
|
||||
from xml.etree.cElementTree import Element, ElementTree, SubElement
|
||||
from defusedxml.cElementTree import iterparse
|
||||
|
||||
# Internal dependencies
|
||||
from .parser import parse_path
|
||||
|
@ -14,13 +14,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 +165,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:
|
||||
|
|
|
@ -3,14 +3,9 @@ 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 defusedxml.minidom import parse
|
||||
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
|
||||
|
@ -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'
|
||||
|
||||
return d + 'z'
|
||||
return d
|
||||
|
||||
|
||||
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
|
||||
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 +119,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 +143,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 +224,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)
|
||||
|
|
|
@ -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 |
|
@ -6,6 +6,7 @@ from svgpathtools.path import bpoints2bezier
|
|||
|
||||
|
||||
class HigherOrderBezier:
|
||||
"""To help test Bezier curves of arbitrary degree"""
|
||||
def __init__(self, bpoints):
|
||||
self.bpts = bpoints
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
import unittest
|
||||
from svgpathtools import parse_path
|
||||
|
|
|
@ -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 Document, SVG_NAMESPACE, parse_path
|
||||
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'))
|
||||
|
|
|
@ -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 parse_path, Path, Line, QuadraticBezier, CubicBezier, Arc
|
||||
import svgpathtools
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import os
|
||||
import time
|
||||
from sys import version_info
|
||||
import unittest
|
||||
from unittest import TestCase
|
||||
from math import sqrt, pi
|
||||
from operator import itemgetter
|
||||
import numpy as np
|
||||
|
@ -12,10 +9,9 @@ import warnings
|
|||
|
||||
# Internal dependencies
|
||||
from svgpathtools import (
|
||||
Line, QuadraticBezier, CubicBezier, Arc, Path, poly2bez, path_encloses_pt,
|
||||
bpoints2bezier, closest_point_in_path, farthest_point_in_path,
|
||||
is_bezier_segment, is_bezier_path, parse_path
|
||||
)
|
||||
Line, QuadraticBezier, CubicBezier, Arc, Path, parse_path,
|
||||
is_bezier_segment, is_bezier_path, poly2bez, bpoints2bezier,
|
||||
closest_point_in_path, farthest_point_in_path, path_encloses_pt)
|
||||
from svgpathtools.path import bezier_radialrange
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
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):
|
||||
# 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))
|
||||
|
||||
random.seed()
|
||||
for line_index in range(100):
|
||||
for _ in range(100):
|
||||
l = random_line()
|
||||
for t_index in range(100):
|
||||
for __ in range(100):
|
||||
orig_t = random.random()
|
||||
p = l.point(orig_t)
|
||||
computed_t = l.point_to_t(p)
|
||||
|
@ -188,7 +202,8 @@ class LineTest(unittest.TestCase):
|
|||
self.assertAlmostEqual(max_ta, max_tb, delta=TOL)
|
||||
|
||||
|
||||
class CubicBezierTest(unittest.TestCase):
|
||||
# noinspection PyTypeChecker
|
||||
class CubicBezierTest(TestCase):
|
||||
def test_approx_circle(self):
|
||||
"""This is a approximate circle drawn in Inkscape"""
|
||||
|
||||
|
@ -424,14 +439,13 @@ class CubicBezierTest(unittest.TestCase):
|
|||
segment = CubicBezier(complex(600, 500), complex(600, 350),
|
||||
complex(900, 650), complex(900, 500))
|
||||
|
||||
self.assertTrue(segment ==
|
||||
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))
|
||||
self.assertTrue(segment !=
|
||||
CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j))
|
||||
self.assertTrue(segment == CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))
|
||||
self.assertTrue(segment != CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j))
|
||||
self.assertTrue(segment != Line(0, 400))
|
||||
|
||||
|
||||
class QuadraticBezierTest(unittest.TestCase):
|
||||
# noinspection PyTypeChecker
|
||||
class QuadraticBezierTest(TestCase):
|
||||
|
||||
def test_svg_examples(self):
|
||||
"""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
|
||||
# assertEqual and assertNotEqual
|
||||
segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
|
||||
self.assertTrue(segment ==
|
||||
QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j))
|
||||
self.assertTrue(segment !=
|
||||
QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j))
|
||||
self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j))
|
||||
self.assertTrue(segment != QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j))
|
||||
self.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
|
||||
self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment)
|
||||
|
||||
|
||||
class ArcTest(unittest.TestCase):
|
||||
# noinspection PyTypeChecker
|
||||
class ArcTest(TestCase):
|
||||
|
||||
def test_trusting_acos(self):
|
||||
"""`u1.real` is > 1 in this arc due to numerical error."""
|
||||
try:
|
||||
a1 = Arc(start=(160.197+102.925j),
|
||||
radius=(0.025+0.025j),
|
||||
rotation=0.0,
|
||||
large_arc=False,
|
||||
sweep=True,
|
||||
end=(160.172+102.95j))
|
||||
_ = Arc(start=(160.197+102.925j),
|
||||
radius=(0.025+0.025j),
|
||||
rotation=0.0,
|
||||
large_arc=False,
|
||||
sweep=True,
|
||||
end=(160.172+102.95j))
|
||||
except ValueError:
|
||||
self.fail("Arc() raised ValueError unexpectedly!")
|
||||
|
||||
|
@ -685,9 +698,9 @@ class ArcTest(unittest.TestCase):
|
|||
self.assertIsNone(a.point_to_t(730.5212132777968+171j))
|
||||
|
||||
random.seed()
|
||||
for arc_index in range(100):
|
||||
for _ in range(100):
|
||||
a = random_arc()
|
||||
for t_index in np.linspace(0, 1, 100):
|
||||
for __ in np.linspace(0, 1, 100):
|
||||
orig_t = random.random()
|
||||
p = a.point(orig_t)
|
||||
computed_t = a.point_to_t(p)
|
||||
|
@ -697,7 +710,7 @@ class ArcTest(unittest.TestCase):
|
|||
|
||||
def test_approx_quad(self):
|
||||
n = 100
|
||||
for i in range(n):
|
||||
for _ in range(n):
|
||||
arc = random_arc()
|
||||
if arc.radius.real > 2000 or arc.radius.imag > 2000:
|
||||
continue # Random Arc too large, by autoscale.
|
||||
|
@ -710,7 +723,7 @@ class ArcTest(unittest.TestCase):
|
|||
|
||||
def test_approx_cubic(self):
|
||||
n = 100
|
||||
for i in range(n):
|
||||
for _ in range(n):
|
||||
arc = random_arc()
|
||||
if arc.radius.real > 2000 or arc.radius.imag > 2000:
|
||||
continue # Random Arc too large, by autoscale.
|
||||
|
@ -722,66 +735,62 @@ class ArcTest(unittest.TestCase):
|
|||
self.assertAlmostEqual(d, 0.0, delta=2)
|
||||
|
||||
|
||||
class TestPath(unittest.TestCase):
|
||||
# noinspection PyTypeChecker
|
||||
class TestPath(TestCase):
|
||||
|
||||
def test_hash(self):
|
||||
line1 = Line(600.5 + 350.5j, 650.5 + 325.5j)
|
||||
arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j)
|
||||
arc2 = Arc(650 + 325j, 30 + 25j, -30, 0, 0, 700 + 300j)
|
||||
cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j)
|
||||
cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j)
|
||||
quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j)
|
||||
linez = Line(600 + 300j, 600 + 350j)
|
||||
|
||||
bezpath = Path(line1, cub1, cub2, quad3)
|
||||
bezpathz = Path(line1, cub1, cub2, quad3, linez)
|
||||
path = Path(line1, arc1, cub2, quad3)
|
||||
pathz = Path(line1, arc1, cub2, quad3, linez)
|
||||
lpath = Path(linez)
|
||||
qpath = Path(quad3)
|
||||
cpath = Path(cub1)
|
||||
apath = Path(arc1, arc2)
|
||||
|
||||
test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath,
|
||||
apath, line1, arc1, arc2, cub1, cub2, quad3, linez]
|
||||
|
||||
# this is necessary due to changes to the builtin `hash` function
|
||||
user_hash_seed = os.environ.get("PYTHONHASHSEED", "")
|
||||
os.environ["PYTHONHASHSEED"] = "314"
|
||||
if version_info >= (3, 8):
|
||||
expected_hashes = [
|
||||
-6073024107272494569, -2519772625496438197, 8726412907710383506,
|
||||
2132930052750006195, 3112548573593977871, 991446120749438306,
|
||||
-5589397644574569777, -4438808571483114580, -3125333407400456536,
|
||||
-4418099728831808951, 702646573139378041, -6331016786776229094,
|
||||
5053050772929443013, 6102272282813527681, -5385294438006156225
|
||||
]
|
||||
elif (3, 2) <= version_info < (3, 8):
|
||||
expected_hashes = [
|
||||
-5662973462929734898, 5166874115671195563, 5223434942701471389,
|
||||
-7224979960884350294, -5178990533869800243, -4003140762934044601,
|
||||
8575549467429100514, -6692132994808317852, 1594848578230132678,
|
||||
-6374833902132909499, 4188352014604112779, -5090374009174854814,
|
||||
-7093907105533857815, 2036243740727202243, -8108488067585685407
|
||||
]
|
||||
else:
|
||||
|
||||
expected_hashes = [
|
||||
-5762846476463470127, -138736730317965290, -2005041722222729058,
|
||||
8448700906794235291, -5178990533869800243, -4003140762934044601,
|
||||
8575549467429100514, 5166859065265868968, 1373103287265872323,
|
||||
-1022491904150314631, 4188352014604112779, -5090374009174854814,
|
||||
-7093907105533857815, 2036243740727202243, -8108488067585685407
|
||||
]
|
||||
|
||||
if version_info.major == 2 and os.name == 'nt':
|
||||
# the expected hash values for 2.7 apparently differed on Windows
|
||||
# if you work in Windows and want to fix this test, please do
|
||||
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_hash(self):
|
||||
# line1 = Line(600.5 + 350.5j, 650.5 + 325.5j)
|
||||
# arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j)
|
||||
# arc2 = Arc(650 + 325j, 30 + 25j, -30, 0, 0, 700 + 300j)
|
||||
# cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j)
|
||||
# cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j)
|
||||
# quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j)
|
||||
# linez = Line(600 + 300j, 600 + 350j)
|
||||
#
|
||||
# bezpath = Path(line1, cub1, cub2, quad3)
|
||||
# bezpathz = Path(line1, cub1, cub2, quad3, linez)
|
||||
# path = Path(line1, arc1, cub2, quad3)
|
||||
# pathz = Path(line1, arc1, cub2, quad3, linez)
|
||||
# lpath = Path(linez)
|
||||
# qpath = Path(quad3)
|
||||
# cpath = Path(cub1)
|
||||
# apath = Path(arc1, arc2)
|
||||
#
|
||||
# test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath,
|
||||
# apath, line1, arc1, arc2, cub1, cub2, quad3, linez]
|
||||
#
|
||||
# # this is necessary due to changes to the builtin `hash` function
|
||||
# python_version = sys.version_info.major + 0.1*sys.version_info.minor
|
||||
# user_hash_seed = os.environ.get("PYTHONHASHSEED", "")
|
||||
# os.environ["PYTHONHASHSEED"] = "314"
|
||||
# if 3.8 <= python_version:
|
||||
# expected_hashes = [
|
||||
# -6073024107272494569, -2519772625496438197, 8726412907710383506,
|
||||
# 2132930052750006195, 3112548573593977871, 991446120749438306,
|
||||
# -5589397644574569777, -4438808571483114580, -3125333407400456536,
|
||||
# -4418099728831808951, 702646573139378041, -6331016786776229094,
|
||||
# 5053050772929443013, 6102272282813527681, -5385294438006156225
|
||||
# ]
|
||||
# elif 3.2 <= python_version < 3.8:
|
||||
# expected_hashes = [
|
||||
# -5662973462929734898, 5166874115671195563, 5223434942701471389,
|
||||
# -7224979960884350294, -5178990533869800243, -4003140762934044601,
|
||||
# 8575549467429100514, -6692132994808317852, 1594848578230132678,
|
||||
# -6374833902132909499, 4188352014604112779, -5090374009174854814,
|
||||
# -7093907105533857815, 2036243740727202243, -8108488067585685407
|
||||
# ]
|
||||
# else:
|
||||
# expected_hashes = [
|
||||
# -5762846476463470127, -138736730317965290, -2005041722222729058,
|
||||
# 8448700906794235291, -5178990533869800243, -4003140762934044601,
|
||||
# 8575549467429100514, 5166859065265868968, 1373103287265872323,
|
||||
# -1022491904150314631, 4188352014604112779, -5090374009174854814,
|
||||
# -7093907105533857815, 2036243740727202243, -8108488067585685407
|
||||
# ]
|
||||
#
|
||||
# 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):
|
||||
arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
|
||||
|
@ -820,8 +829,7 @@ class TestPath(unittest.TestCase):
|
|||
# regression tests.
|
||||
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.5),
|
||||
(168.93398282201787 + 68.93398282201787j))
|
||||
self.assertAlmostEqual(path.point(0.5), (168.93398282201787 + 68.93398282201787j))
|
||||
self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j), delta=TOL)
|
||||
self.assertAlmostEqual(path.point(1.0), (275 + 175j), delta=TOL)
|
||||
# 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,
|
||||
large_arc=0, sweep=1, end=700 + 300j),
|
||||
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))
|
||||
self.assertEqual(eval(repr(path)), path)
|
||||
|
||||
|
@ -874,14 +882,14 @@ class TestPath(unittest.TestCase):
|
|||
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
|
||||
large_arc=0, sweep=1, end=700 + 300j),
|
||||
CubicBezier(start=700 + 300j, control1=800 + 400j,
|
||||
control2=750 + 200j, end=600 + 100j),
|
||||
control2=750 + 200j, end=600 + 100j),
|
||||
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
|
||||
path2 = Path(
|
||||
Line(start=600 + 350j, end=650 + 325j),
|
||||
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
|
||||
large_arc=0, sweep=1, end=700 + 300j),
|
||||
CubicBezier(start=700 + 300j, control1=800 + 400j,
|
||||
control2=750 + 200j, end=600 + 100j),
|
||||
control2=750 + 200j, end=600 + 100j),
|
||||
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
|
||||
|
||||
self.assertTrue(path1 == path2)
|
||||
|
@ -1051,17 +1059,17 @@ class TestPath(unittest.TestCase):
|
|||
test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath,
|
||||
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:
|
||||
sy = sx
|
||||
if sy_ is None:
|
||||
sy_ = sx_
|
||||
|
||||
zeta = pt - origin
|
||||
zeta = pt_ - origin_
|
||||
pt_vec = [[zeta.real],
|
||||
[zeta.imag],
|
||||
[1]]
|
||||
transform = [[sx, 0, origin.real],
|
||||
[0, sy, origin.imag]]
|
||||
transform = [[sx_, 0, origin_.real],
|
||||
[0, sy_, origin_.imag]]
|
||||
|
||||
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
|
||||
seg = curve
|
||||
seg_idx = None
|
||||
seg_t = None
|
||||
if isinstance(curve, Path):
|
||||
seg_idx, seg_t = curve.T2t(t)
|
||||
seg = curve[seg_idx]
|
||||
|
@ -1123,7 +1133,7 @@ class TestPath(unittest.TestCase):
|
|||
curve.scaled(sx, sy).point(t)
|
||||
else:
|
||||
curve_scaled = curve.scaled(sx, sy)
|
||||
seg_scaled = seg.scaled(sx, sy)
|
||||
_ = seg.scaled(sx, sy)
|
||||
if isinstance(curve, Path):
|
||||
res = curve_scaled[seg_idx].point(seg_t)
|
||||
else:
|
||||
|
@ -1197,20 +1207,21 @@ class TestPath(unittest.TestCase):
|
|||
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
|
||||
# test values were generated (using the .length() method).
|
||||
##############################################################
|
||||
|
||||
def test_ilength_lines(self):
|
||||
l = Line(1, 3-1j)
|
||||
nodall = Line(1+1j, 1+1j)
|
||||
# nodall = Line(1+1j, 1+1j)
|
||||
|
||||
tests = [(l, 0.01, 0.022360679774997897),
|
||||
(l, 0.1, 0.223606797749979),
|
||||
(l, 0.5, 1.118033988749895),
|
||||
(l, 0.9, 2.012461179749811),
|
||||
(l, 0.99, 2.213707297724792)]
|
||||
(l, 0.1, 0.223606797749979),
|
||||
(l, 0.5, 1.118033988749895),
|
||||
(l, 0.9, 2.012461179749811),
|
||||
(l, 0.99, 2.213707297724792)]
|
||||
|
||||
for (l, t, s) in tests:
|
||||
self.assertAlmostEqual(l.ilength(s), t, delta=TOL)
|
||||
|
@ -1220,37 +1231,31 @@ class Test_ilength(unittest.TestCase):
|
|||
q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j)
|
||||
closedq = QuadraticBezier(6 + 2j, 5 - 1j, 6 + 2j)
|
||||
linq = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
|
||||
nodalq = QuadraticBezier(1, 1, 1)
|
||||
# nodalq = QuadraticBezier(1, 1, 1)
|
||||
|
||||
tests = [(q1, 0.01, 6.364183310105577),
|
||||
(q1, 0.1, 60.23857499635088),
|
||||
(q1, 0.5, 243.8855469477619),
|
||||
(q1, 0.9, 427.53251889917294),
|
||||
(q1, 0.99, 481.40691058541813),
|
||||
(q2, 0.01, 6.365673533661836),
|
||||
(q2, 0.1, 60.31675895732397),
|
||||
(q2, 0.5, 233.24592830045907),
|
||||
(q2, 0.9, 346.42891253298706),
|
||||
(q2, 0.99, 376.32659156736844),
|
||||
(closedq, 0.01, 0.06261309767133393),
|
||||
(closedq, 0.1, 0.5692099788303084),
|
||||
(closedq, 0.5, 1.5811388300841898),
|
||||
(closedq, 0.9, 2.5930676813380713),
|
||||
(closedq, 0.99, 3.0996645624970456),
|
||||
(linq, 0.01, 0.04203807797699605),
|
||||
(linq, 0.1, 0.19379255804998186),
|
||||
(linq, 0.5, 4.844813951249544),
|
||||
(linq, 0.9, 18.0823363780483),
|
||||
(linq, 0.99, 22.24410609777091)]
|
||||
(q1, 0.1, 60.23857499635088),
|
||||
(q1, 0.5, 243.8855469477619),
|
||||
(q1, 0.9, 427.53251889917294),
|
||||
(q1, 0.99, 481.40691058541813),
|
||||
(q2, 0.01, 6.365673533661836),
|
||||
(q2, 0.1, 60.31675895732397),
|
||||
(q2, 0.5, 233.24592830045907),
|
||||
(q2, 0.9, 346.42891253298706),
|
||||
(q2, 0.99, 376.32659156736844),
|
||||
(closedq, 0.01, 0.06261309767133393),
|
||||
(closedq, 0.1, 0.5692099788303084),
|
||||
(closedq, 0.5, 1.5811388300841898),
|
||||
(closedq, 0.9, 2.5930676813380713),
|
||||
(closedq, 0.99, 3.0996645624970456),
|
||||
(linq, 0.01, 0.04203807797699605),
|
||||
(linq, 0.1, 0.19379255804998186),
|
||||
(linq, 0.5, 4.844813951249544),
|
||||
(linq, 0.9, 18.0823363780483),
|
||||
(linq, 0.99, 22.24410609777091)]
|
||||
|
||||
for q, t, s in tests:
|
||||
try:
|
||||
self.assertAlmostEqual(q.ilength(s), t, delta=TOL)
|
||||
except:
|
||||
print(q)
|
||||
print(s)
|
||||
print(t)
|
||||
raise
|
||||
self.assertAlmostEqual(q.ilength(s), t, delta=TOL)
|
||||
|
||||
def test_ilength_cubics(self):
|
||||
c1 = CubicBezier(200 + 300j, 400 + 50j, 600+100j, -200)
|
||||
|
@ -1425,7 +1430,7 @@ class Test_ilength(unittest.TestCase):
|
|||
for (c, t, s) in tests:
|
||||
try:
|
||||
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
|
||||
# with scipy installed -- if scipy is not installed,
|
||||
# then in cases where `t == 1`, `s` may be slightly
|
||||
|
@ -1448,7 +1453,8 @@ class Test_ilength(unittest.TestCase):
|
|||
lin.ilength(1)
|
||||
|
||||
|
||||
class Test_intersect(unittest.TestCase):
|
||||
# noinspection PyTypeChecker
|
||||
class Test_intersect(TestCase):
|
||||
def test_intersect(self):
|
||||
|
||||
###################################################################
|
||||
|
@ -1496,50 +1502,6 @@ class Test_intersect(unittest.TestCase):
|
|||
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):
|
||||
l0 = Line(start=(25.389999999999997+99.989999999999995j),
|
||||
end=(25.389999999999997+90.484999999999999j))
|
||||
|
@ -1633,9 +1595,9 @@ class Test_intersect(unittest.TestCase):
|
|||
assert_intersections(self, a, l, intersections, 0)
|
||||
|
||||
random.seed()
|
||||
for arc_index in range(50):
|
||||
for _ in range(50):
|
||||
a = random_arc()
|
||||
for line_index in range(100):
|
||||
for __ in range(100):
|
||||
l = random_line()
|
||||
intersections = a.intersect(l)
|
||||
msg = 'Generated: arc = {}, line = {}'.format(a, l)
|
||||
|
@ -1792,7 +1754,8 @@ class Test_intersect(unittest.TestCase):
|
|||
assert_intersections(self, a0, a1, intersections, 0)
|
||||
|
||||
|
||||
class TestPathTools(unittest.TestCase):
|
||||
# noinspection PyTypeChecker
|
||||
class TestPathTools(TestCase):
|
||||
# moved from test_pathtools.py
|
||||
|
||||
def setUp(self):
|
||||
|
@ -2027,7 +1990,7 @@ class TestPathTools(unittest.TestCase):
|
|||
|
||||
def test_path_area(self):
|
||||
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
|
||||
cw_square = Path()
|
||||
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))
|
||||
|
||||
|
||||
class TestPathBugs(unittest.TestCase):
|
||||
# noinspection PyTypeChecker
|
||||
class TestPathBugs(TestCase):
|
||||
|
||||
def test_issue_113(self):
|
||||
"""
|
||||
|
@ -2107,9 +2071,21 @@ class TestPathBugs(unittest.TestCase):
|
|||
self.assertAlmostEqual(p.length(), 236.70287281737836, delta=TOL)
|
||||
|
||||
def test_issue_71(self):
|
||||
p = Path("M327 468z")
|
||||
m = p.closed
|
||||
q = p.d() # Failing to Crash is good.
|
||||
"""Test that degenerate (point-like) paths behave properly."""
|
||||
# degenerate (point-like) closed path
|
||||
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):
|
||||
"""
|
||||
|
@ -2128,4 +2104,5 @@ class TestPathBugs(unittest.TestCase):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
from unittest import main
|
||||
main()
|
||||
|
|
|
@ -1,15 +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 svg2paths, Path, Line, Arc
|
||||
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):
|
||||
|
@ -24,8 +16,8 @@ class TestSVG2Paths(unittest.TestCase):
|
|||
Line(105.5+50j, 55.5+0j)
|
||||
)
|
||||
self.assertTrue(path.isclosed())
|
||||
self.assertTrue(len(path)==3)
|
||||
self.assertTrue(path==path_correct)
|
||||
self.assertEqual(len(path), 3)
|
||||
self.assertEqual(path, path_correct)
|
||||
|
||||
# triangular quadrilateral (with a redundant 4th "closure" point)
|
||||
path = paths[1]
|
||||
|
@ -35,8 +27,8 @@ class TestSVG2Paths(unittest.TestCase):
|
|||
Line(0+0j, 0+0j) # result of redundant point
|
||||
)
|
||||
self.assertTrue(path.isclosed())
|
||||
self.assertTrue(len(path)==4)
|
||||
self.assertTrue(path==path_correct)
|
||||
self.assertEqual(len(path), 4)
|
||||
self.assertEqual(path, path_correct)
|
||||
|
||||
def test_svg2paths_ellipses(self):
|
||||
|
||||
|
@ -46,8 +38,8 @@ class TestSVG2Paths(unittest.TestCase):
|
|||
path_ellipse = paths[0]
|
||||
path_ellipse_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j),
|
||||
Arc(150+100j, 50+50j, 0.0, True, False, 50+100j))
|
||||
self.assertTrue(len(path_ellipse)==2)
|
||||
self.assertTrue(path_ellipse==path_ellipse_correct)
|
||||
self.assertEqual(len(path_ellipse), 2)
|
||||
self.assertEqual(path_ellipse, path_ellipse_correct)
|
||||
self.assertTrue(path_ellipse.isclosed())
|
||||
|
||||
# circle tests
|
||||
|
@ -55,82 +47,7 @@ class TestSVG2Paths(unittest.TestCase):
|
|||
|
||||
path_circle = paths[0]
|
||||
path_circle_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j),
|
||||
Arc(150+100j, 50+50j, 0.0, True, False, 50+100j))
|
||||
self.assertTrue(len(path_circle)==2)
|
||||
self.assertTrue(path_circle==path_circle_correct)
|
||||
Arc(150+100j, 50+50j, 0.0, True, False, 50+100j))
|
||||
self.assertEqual(len(path_circle), 2)
|
||||
self.assertEqual(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()
|
||||
|
|
Loading…
Reference in New Issue