Compare commits

...

58 Commits

Author SHA1 Message Date
Andrew Port fcb648b9bb add back pypi action new tag condition 2023-05-20 14:38:22 -04:00
Andrew Port ec546a71d4 Merge branch 'PyPI' 2023-05-20 14:37:02 -04:00
Andrew Port bc930005c2 test fixed pypi action 2023-05-20 14:34:32 -04:00
Andrew Port ae9b79e77a revert temporary fix for pushing 1.6.1 to pypi 2023-05-20 14:26:44 -04:00
Andrew Port 289ee6ecb4 temporary fix for pushing 1.6.1 to pypi 2023-05-20 14:25:19 -04:00
Andrew Port 592fe3a525 fix pypi actions 2023-05-20 14:18:42 -04:00
Andrew Port 81870e1f85 update version 2023-05-20 14:13:15 -04:00
Andrew Port 6015a97090
Merge pull request #207 from mathandy/fix-issue-205
return error if Path.point() cannot be computed
2023-05-20 14:11:07 -04:00
Andrew Port 788b2b43a2
Merge pull request #203 from kasbah/fix-escape-sequences
Fix invalid escape sequences in strings
2023-05-20 14:10:19 -04:00
Andrew Port b282094b53 return error if Path.point() cannot be computed 2023-05-20 13:20:05 -04:00
Andrew Port 229773ff9d
Merge pull request #201 from SebKuzminsky/fix-arc-sweep-with-negative-scale
Fix arc sweep with negative scale
2023-05-20 12:50:17 -04:00
Kaspar Emanuel a16a060c27
Fix invalid escape sequences in strings 2023-05-06 19:33:31 +01:00
Sebastian Kuzminsky e94483510e path.transform: Arc sweep is reversed by negative scale
When transforming an Arc, negative scale reverses the sweep.
2023-05-05 00:38:55 -06:00
Sebastian Kuzminsky 6abda09d1c add a test loading arcs with negative scale
This test currently fails, fix in the following commit.

The test loads an svg with a group with a transform with "scale(1,-1)".
This situation can mess up arc sweeps, resulting in corrupted paths.
2023-05-05 00:38:49 -06:00
Andrew Port 5c73056420
Merge pull request #199 from mathandy/issue-198
Issue 198
2023-04-01 15:39:39 -04:00
Andrew Port 4f5d8f3bf2 fix issue-198; circles parsing to non-closed paths 2023-04-01 15:30:04 -04:00
Andrew Port c4d98afc68 add test for issue 198 2023-04-01 15:26:22 -04:00
Andrew Port 9c69e45d6e add python==3.11 to setup.py 2023-02-13 18:15:36 -05:00
Andrew Port dc2f6e90cc remove broken build shield 2023-02-13 18:13:32 -05:00
Andrew Port 96676b7697 increment version number 2023-02-13 17:46:17 -05:00
Andrew Port 2a1cb735e9
Merge pull request #191 from tatarize/fastfail-intersection
Fastfail Intersection
2023-02-03 18:06:56 -08:00
Andrew Port 3eb21161cf add report of intersection count 2023-02-03 21:00:57 -05:00
Andrew Port 31b6f3dd90 Merge branch 'master' into fastfail-intersection 2023-02-03 20:49:49 -05:00
Andrew Port d9515ea399 remove all wildcard imports 2023-02-03 19:46:23 -05:00
Andrew Port 944ccf5e89 create new yaml for legacy system tests 2023-02-03 18:45:50 -05:00
Tatarize b6e5a623ea Add random intersections test 2022-12-04 00:59:40 -08:00
Tatarize 4c6abc5820 Add quick fails to paths 2022-12-04 00:59:15 -08:00
Andrew Port b8dfb6770a correct descriptive action name 2022-07-10 22:38:14 -03:00
Andrew Port 0e17702d04 update version to 1.5.1 2022-07-10 22:19:50 -03:00
Andrew Port 8df19f1c12 fixed issue 171 2022-07-09 19:49:12 -03:00
Andrew Port 9ac7f62515 cleanup long comment 2022-07-09 19:38:28 -03:00
Andrew Port d8a6e5e509 delete unused line 2022-07-09 19:30:40 -03:00
Andrew Port 73c887a8a3 rename as name already used by outside function 2022-07-09 19:28:42 -03:00
Andrew Port f7e074339d update version to 1.5.0 (now that string/file-like objects are handled) 2022-06-05 22:27:22 -07:00
Andrew Port d9f5a2a781 Merge branch 'master' into PyPI 2022-06-05 22:22:18 -07:00
Andrew Port 740e2bf991 Merge branch 'master' into PyPI 2022-06-05 22:21:36 -07:00
Andrew Port 8cbe6f0f81 minor docstring change 2022-06-05 22:19:26 -07:00
Andrew Port b82530aaac minor fix to docstring formatting 2022-06-05 21:49:21 -07:00
Andrew Port e8792f4d2d add support for os.PathLike arguments 2022-06-05 21:47:37 -07:00
Andrew Port d3a66f0bbd skip pathlib support for python < 3.6 2022-06-05 21:17:28 -07:00
Andrew Port 356d86df78 restore support for PosixPath inputs for Document 2022-06-05 21:00:42 -07:00
Andrew Port a989c9831d
Merge pull request #176 from FlyingSamson/read-svg-from-file-like-obj
Support reading of SVGs from strings and file-like objects
2022-06-05 20:12:19 -07:00
FlyingSamson 07f46d41f8 Rename svg_string2paths to svgstr2paths 2022-05-25 19:24:50 +02:00
FlyingSamson 2fc016d48f Make factory method a classmethod 2022-05-25 17:58:14 +02:00
FlyingSamson aacd5fa96d Remove second version of function returning the svg_attributes by default 2022-05-25 17:57:22 +02:00
FlyingSamson db5200f460 Switch back to previous function parameter names 2022-05-25 17:39:10 +02:00
FlyingSamson a473ee3f4c Remove unnecessary seek commands 2022-05-24 18:15:52 +02:00
FlyingSamson 02a223c220 Fix fileio for test compatibility with python2.7 2022-05-22 17:02:51 +02:00
FlyingSamson 68e0d1f30d Fix tests for old python versions not supporting type hints 2022-05-22 15:51:03 +02:00
FlyingSamson a743e0293c Add tests for creating from file location, file, StringIO, and string 2022-05-22 15:46:56 +02:00
FlyingSamson 1771fbfb06 Add factory method for creating from string holding svg object 2022-05-22 15:30:48 +02:00
FlyingSamson 33f4639bbf Add tests for functions taking svg objects as string 2022-05-22 14:51:16 +02:00
FlyingSamson 50b335f3da Add convenience functions for converting svgs contained in a string to paths 2022-05-22 14:37:27 +02:00
FlyingSamson ccdd10212c Add unit tests for reading from different sources in svg2paths 2022-05-22 13:32:09 +02:00
FlyingSamson ce43c75cd8 Allow file-like object as input to Documents ctor and svg2paths function 2022-05-22 13:09:13 +02:00
Andrew Port 1b8caeec71 fix github action syntax 2021-11-09 20:35:58 -08:00
Andrew Port 002e691686 Publish now on any v* tag pushed to PyPI branch 2021-11-09 20:28:00 -08:00
Andrew Port 3576591e08 Publish now on any v* tag pushed to PyPI branch 2021-11-09 20:26:36 -08:00
25 changed files with 420 additions and 69 deletions

34
.github/workflows/github-ci-legacy.yml vendored Normal file
View File

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

View File

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

View File

@ -8,7 +8,7 @@ on:
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish to TestPyPI name: Build and publish to TestPyPI
runs-on: ubuntu-18.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Set up Python 3 - name: Set up Python 3
@ -30,7 +30,7 @@ jobs:
--outdir dist/ --outdir dist/
. .
- name: Publish to Test PyPI - name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@master uses: pypa/gh-action-pypi-publish@release/v1
with: with:
skip_existing: true skip_existing: true
password: ${{ secrets.TESTPYPI_API_TOKEN }} password: ${{ secrets.TESTPYPI_API_TOKEN }}

View File

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

View File

@ -7,7 +7,6 @@
"[![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", "[![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", "![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](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)\n",
"![Build](https://img.shields.io/github/workflow/status/mathandy/svgpathtools/Github%20CI%20Unit%20Testing)\n",
"[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)\n", "[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)\n",
"# svgpathtools\n", "# svgpathtools\n",
"\n", "\n",
@ -721,4 +720,4 @@
}, },
"nbformat": 4, "nbformat": 4,
"nbformat_minor": 1 "nbformat_minor": 1
} }

View File

@ -1,7 +1,6 @@
[![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) [![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) ![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)
[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/) [![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)
![Build](https://img.shields.io/github/workflow/status/mathandy/svgpathtools/Github%20CI%20Unit%20Testing)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools) [![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)
# svgpathtools # svgpathtools

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import codecs
import os import os
VERSION = '1.4.4' VERSION = '1.6.1'
AUTHOR_NAME = 'Andy Port' AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = 'AndyAPort@gmail.com' AUTHOR_EMAIL = 'AndyAPort@gmail.com'
GITHUB = 'https://github.com/mathandy/svgpathtools' GITHUB = 'https://github.com/mathandy/svgpathtools'
@ -47,6 +47,7 @@ setup(name='svgpathtools',
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
"Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Recognition", "Topic :: Scientific/Engineering :: Image Recognition",

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,13 @@ The main tool being the svg2paths() function."""
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse from xml.dom.minidom import parse
from os import path as os_path, getcwd import os
from io import StringIO
import re import re
try:
from os import PathLike as FilePathLike
except ImportError:
FilePathLike = str
# Internal dependencies # Internal dependencies
from .parser import parse_path from .parser import parse_path
@ -46,7 +51,7 @@ def ellipse2pathd(ellipse):
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0' d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0' d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
return d return d + 'z'
def polyline2pathd(polyline, is_polygon=False): def polyline2pathd(polyline, is_polygon=False):
@ -144,7 +149,9 @@ def svg2paths(svg_file_location,
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements. SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
Args: Args:
svg_file_location (string): the location of the svg file 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
return_svg_attributes (bool): Set to True and a dictionary of return_svg_attributes (bool): Set to True and a dictionary of
svg-attributes will be extracted and returned. See also the svg-attributes will be extracted and returned. See also the
`svg2paths2()` function. `svg2paths2()` function.
@ -168,8 +175,10 @@ def svg2paths(svg_file_location,
list: The list of corresponding path attribute dictionaries. list: The list of corresponding path attribute dictionaries.
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`). dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
""" """
if os_path.dirname(svg_file_location) == '': # strings are interpreted as file location everything else is treated as
svg_file_location = os_path.join(getcwd(), svg_file_location) # 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
doc = parse(svg_file_location) doc = parse(svg_file_location)
@ -249,3 +258,26 @@ def svg2paths2(svg_file_location,
convert_polylines_to_paths=convert_polylines_to_paths, convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths, convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths) convert_rectangles_to_paths=convert_rectangles_to_paths)
def svgstr2paths(svg_string,
return_svg_attributes=False,
convert_circles_to_paths=True,
convert_ellipses_to_paths=True,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
convert_rectangles_to_paths=True):
"""Convenience function; identical to svg2paths() except that it takes the
svg object as string. See svg2paths() docstring for more
info."""
# wrap string into StringIO object
svg_file_obj = StringIO(svg_string)
return svg2paths(svg_file_location=svg_file_obj,
return_svg_attributes=return_svg_attributes,
convert_circles_to_paths=convert_circles_to_paths,
convert_ellipses_to_paths=convert_ellipses_to_paths,
convert_lines_to_paths=convert_lines_to_paths,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths)

19
test/negative-scale.svg Normal file
View File

@ -0,0 +1,19 @@
<?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>

After

Width:  |  Height:  |  Size: 665 B

View File

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

54
test/test_document.py Normal file
View File

@ -0,0 +1,54 @@
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 from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import * from svgpathtools import parse_path
class TestGeneration(unittest.TestCase): class TestGeneration(unittest.TestCase):

View File

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

View File

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

View File

@ -1,6 +1,7 @@
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import os import os
import time
from sys import version_info from sys import version_info
import unittest import unittest
from math import sqrt, pi from math import sqrt, pi
@ -10,8 +11,12 @@ import random
import warnings import warnings
# Internal dependencies # Internal dependencies
from svgpathtools import * from svgpathtools import (
from svgpathtools.path import _NotImplemented4ArcException, bezier_radialrange 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
# An important note for those doing any debugging: # An important note for those doing any debugging:
# ------------------------------------------------ # ------------------------------------------------
@ -1491,6 +1496,50 @@ class Test_intersect(unittest.TestCase):
self.assertTrue(len(yix) == 1) self.assertTrue(len(yix) == 1)
################################################################### ###################################################################
def test_random_intersections(self):
from random import Random
r = Random()
distance = 100
distribution = 10000
count = 500
def random_complex(offset_x=0.0, offset_y=0.0):
return complex(r.random() * distance + offset_x, r.random() * distance + offset_y)
def random_line():
offset_x = r.random() * distribution
offset_y = r.random() * distribution
return Line(random_complex(offset_x, offset_y), random_complex(offset_x, offset_y))
def random_quad():
offset_x = r.random() * distribution
offset_y = r.random() * distribution
return QuadraticBezier(random_complex(offset_x, offset_y), random_complex(offset_x, offset_y), random_complex(offset_x, offset_y))
def random_cubic():
offset_x = r.random() * distribution
offset_y = r.random() * distribution
return CubicBezier(random_complex(offset_x, offset_y), random_complex(offset_x, offset_y), random_complex(offset_x, offset_y), random_complex(offset_x, offset_y))
def random_path():
path = Path()
for i in range(count):
type_segment = random.randint(0, 3)
if type_segment == 0:
path.append(random_line())
if type_segment == 1:
path.append(random_quad())
if type_segment == 2:
path.append(random_cubic())
return path
path1 = random_path()
path2 = random_path()
t = time.time()
intersections = path1.intersect(path2)
print("\nFound {} intersections in {} seconds.\n"
"".format(len(intersections), time.time() - t))
def test_line_line_0(self): def test_line_line_0(self):
l0 = Line(start=(25.389999999999997+99.989999999999995j), l0 = Line(start=(25.389999999999997+99.989999999999995j),
end=(25.389999999999997+90.484999999999999j)) end=(25.389999999999997+90.484999999999999j))

View File

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

View File

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

View File

@ -1,10 +1,17 @@
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import * 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 os.path import join, dirname from os.path import join, dirname
from sys import version_info
import tempfile
import shutil
from svgpathtools.svg_to_paths import rect2pathd from svgpathtools.svg_to_paths import rect2pathd
class TestSVG2Paths(unittest.TestCase): class TestSVG2Paths(unittest.TestCase):
def test_svg2paths_polygons(self): def test_svg2paths_polygons(self):
@ -53,8 +60,77 @@ class TestSVG2Paths(unittest.TestCase):
self.assertTrue(path_circle==path_circle_correct) self.assertTrue(path_circle==path_circle_correct)
self.assertTrue(path_circle.isclosed()) self.assertTrue(path_circle.isclosed())
# test for issue #198 (circles not being closed)
svg = u"""<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="40mm" height="40mm"
viewBox="0 0 40 40" version="1.1">
<g id="layer">
<circle id="c1" cx="20.000" cy="20.000" r="11.000" />
<circle id="c2" cx="20.000" cy="20.000" r="5.15" />
</g>
</svg>"""
tmpdir = tempfile.mkdtemp()
svgfile = os.path.join(tmpdir, 'test.svg')
with open(svgfile, 'w') as f:
f.write(svg)
paths, _ = svg2paths(svgfile)
self.assertEqual(len(paths), 2)
self.assertTrue(paths[0].isclosed())
self.assertTrue(paths[1].isclosed())
shutil.rmtree(tmpdir)
def test_rect2pathd(self): def test_rect2pathd(self):
non_rounded = {"x":"10", "y":"10", "width":"100","height":"100"} 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') 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"} 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") 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()