Compare commits

...

40 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 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
24 changed files with 258 additions and 58 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&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD)\n", "[![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", "![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",

View File

@ -1,7 +1,6 @@
[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD) [![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD)
![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg) ![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)
[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/) [![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)
![Build](https://img.shields.io/github/workflow/status/mathandy/svgpathtools/Github%20CI%20Unit%20Testing)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools) [![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)
# svgpathtools # svgpathtools

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

@ -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.
@ -44,6 +44,7 @@ import warnings
from io import StringIO 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
@ -51,7 +52,7 @@ 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:
@ -259,9 +260,7 @@ class Document:
@classmethod @classmethod
def from_svg_string(cls, svg_string): def from_svg_string(cls, svg_string):
"""Factory method for creating a document from a string holding a svg """Constructor for creating a Document object from a string."""
object
"""
# wrap string into StringIO object # wrap string into StringIO object
svg_file_obj = StringIO(svg_string) svg_file_obj = StringIO(svg_string)
# create document from file object # create document from file object

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
@ -336,9 +339,13 @@ 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

@ -51,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):

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

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 Document
from io import StringIO from io import StringIO
from io import open # overrides build-in open for compatibility with python2 from io import open # overrides build-in open for compatibility with python2
from os.path import join, dirname from os.path import join, dirname

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,13 @@
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 StringIO
from io import open # overrides build-in open for compatibility with python2 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 from sys import version_info
import tempfile
import shutil
from svgpathtools.svg_to_paths import rect2pathd from svgpathtools.svg_to_paths import rect2pathd
@ -57,6 +60,26 @@ 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')
@ -107,3 +130,7 @@ class TestSVG2Paths(unittest.TestCase):
paths, _ = svgstr2paths(file_content) paths, _ = svgstr2paths(file_content)
self.assertEqual(len(paths), 2) self.assertEqual(len(paths), 2)
if __name__ == '__main__':
unittest.main()