Compare commits

..

10 Commits

Author SHA1 Message Date
Andrew Port d4f3bc5631 one more try 2021-10-19 01:01:42 -07:00
Andrew Port 039a5a89a1 try php fix 2021-10-19 00:58:27 -07:00
Andrew Port 46203b0e86 fix workflow file 2021-10-19 00:41:03 -07:00
Andrew Port 14a9d6c5d1 comment out scipy 2021-10-19 00:37:34 -07:00
Andrew Port 1c333273c6 fix heroku deployment 2021-10-19 00:36:58 -07:00
Andrew Port b42653b03f add procfile for heroku 2021-10-19 00:31:29 -07:00
Andrew Port 4d03c4b4e7 force push to heroku 2021-10-18 23:49:35 -07:00
Andrew Port 4c7a6a8592 specify appdir for heroku deployment 2021-10-18 23:45:30 -07:00
Andrew Port d89e2c5f0c add heroku deployment and badge 2021-10-18 23:39:55 -07:00
Andrew Port 4ff68ac17a add action to generate code coverage report 2021-10-18 22:59:54 -07:00
28 changed files with 213 additions and 579 deletions

56
.github/workflows/coverage.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Generate Coverage Report
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3
# install deps
- name: Setup python environment
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install coverage
# find and run all unit tests
- name: Compute test coverage
run: |
pip install scipy
coverage run --source=svgpathtools -m unittest discover test # run tests again w/ scipy
coverage html # generate static website detailing coverage
echo "coverage: $(coverage report | grep TOTAL | awk '{ print $4 }')" > htmlcov/percentage.yml # get overall percentage
- name: Prepare coverage report for Heroku as fake php site
run: |
echo `web: vendor/bin/heroku-php-apache2 htmlcov/` > Procfile
mv htmlcov/index.html htmlcov/home.html
echo "<?php include_once("./home.html"); ?>" > htmlcov/index.php
echo "{}" > htmlcov/composer.json
# - name: Deploy2
# run: |
# curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
- name: Deploy to coverage report to Heroku
uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: svgpathtools-coverage
heroku_email: ${{secrets.HEROKU_EMAIL}}
#
# # send report to Codacy
# - name: Report to Codacy
# run: |
# export CODACY_API_TOKEN=${{ secrets.CODACY_API_TOKEN }}
# export CODACY_ORGANIZATION_PROVIDER=gh
# export CODACY_USERNAME=mathandy
# export CODACY_PROJECT_NAME=svgpathtools
# bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r <coverage report file name ???>

View File

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

View File

@ -12,7 +12,7 @@ jobs:
strategy:
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

View File

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

View File

@ -1,14 +1,12 @@
name: Publish to PyPI if new version
name: Publish to TestPyPI and if new version PyPI
on:
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 }}

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&amp;no_recurring=0&amp;item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&amp;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,7 +1,9 @@
[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](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)
![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)
[![Code Coverage](https://img.shields.io/badge/dynamic/yaml?color=9cf&label=coverage&query=%24.DATA.SUBDATA&url=https%3A%2F%2Fsvgpathtools-coverage.herokuapp.com%2Fcoverage.yml)](https://svgpathtools-coverage.herokuapp.com)
# svgpathtools
@ -35,10 +37,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

@ -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,3 +1,2 @@
numpy
svgwrite
scipy

View File

@ -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'],
platforms="OS Independent",
requires=['numpy', 'svgwrite'],
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",

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

@ -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
@ -1055,19 +1035,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 +1295,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 +1352,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 +2526,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 +2543,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)

View File

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

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)

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

View File

@ -1,8 +1,7 @@
# External dependencies
from __future__ import division, absolute_import, print_function
import os
import time
from sys import version_info
import sys
import unittest
from math import sqrt, pi
from operator import itemgetter
@ -11,12 +10,8 @@ import random
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
)
from svgpathtools.path import bezier_radialrange
from svgpathtools import *
from svgpathtools.path import _NotImplemented4ArcException, bezier_radialrange
# An important note for those doing any debugging:
# ------------------------------------------------
@ -724,64 +719,59 @@ class ArcTest(unittest.TestCase):
class TestPath(unittest.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)
@ -1496,50 +1486,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))

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