svg.path-3.0/0000775000175000017500000000000013334563713014544 5ustar lregebrolregebro00000000000000svg.path-3.0/MANIFEST.in0000664000175000017500000000003413334563713016277 0ustar lregebrolregebro00000000000000include *.rst include *.txt svg.path-3.0/setup.cfg0000664000175000017500000000016413334563713016366 0ustar lregebrolregebro00000000000000[pep8] max-line-length = 100 ignore = E126,E127 [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 svg.path-3.0/setup.py0000664000175000017500000000323513334563713016261 0ustar lregebrolregebro00000000000000from setuptools import setup, find_packages import os version = '3.0' long_description = ( open('README.rst').read() + '\n' + 'Contributors\n' '============\n' + '\n' + open('CONTRIBUTORS.txt').read() + '\n' + open('CHANGES.txt').read() + '\n') setup(name='svg.path', version=version, description='SVG path objects and parser', long_description=long_description, # Get more strings from # http://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Multimedia :: Graphics' ], keywords='svg path maths', author='Lennart Regebro', author_email='regebro@gmail.com', url='https://github.com/regebro/svg.path', license='MIT', packages=find_packages('src'), package_dir={'': 'src'}, namespace_packages=['svg'], include_package_data=True, zip_safe=True, install_requires=[ 'setuptools', ], test_suite='svg.path.tests', ) svg.path-3.0/LICENSE.txt0000664000175000017500000000207713334563713016375 0ustar lregebrolregebro00000000000000The MIT License (MIT) Copyright (c) 2013-2014 Lennart Regebro Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. svg.path-3.0/src/0000775000175000017500000000000013334563713015333 5ustar lregebrolregebro00000000000000svg.path-3.0/src/svg/0000775000175000017500000000000013334563713016132 5ustar lregebrolregebro00000000000000svg.path-3.0/src/svg/__init__.py0000664000175000017500000000007013334563713020240 0ustar lregebrolregebro00000000000000__import__('pkg_resources').declare_namespace(__name__) svg.path-3.0/src/svg/path/0000775000175000017500000000000013334563713017066 5ustar lregebrolregebro00000000000000svg.path-3.0/src/svg/path/parser.py0000664000175000017500000001545313334563713020744 0ustar lregebrolregebro00000000000000# SVG Path specification parser import re from . import path COMMANDS = set('MmZzLlHhVvCcSsQqTtAa') UPPERCASE = set('MZLHVCSQTA') COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])") FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?") def _tokenize_path(pathdef): for x in COMMAND_RE.split(pathdef): if x in COMMANDS: yield x for token in FLOAT_RE.findall(x): yield token def parse_path(pathdef, current_pos=0j): # In the SVG specs, initial movetos are absolute, even if # specified as 'm'. This is the default behavior here as well. # But if you pass in a current_pos variable, the initial moveto # will be relative to that current_pos. This is useful. elements = list(_tokenize_path(pathdef)) # Reverse for easy use of .pop() elements.reverse() segments = path.Path() start_pos = None command = None while elements: if elements[-1] in COMMANDS: # New command. last_command = command # Used by S and T command = elements.pop() absolute = command in UPPERCASE command = command.upper() else: # If this element starts with numbers, it is an implicit command # and we don't change the command. Check that it's allowed: if command is None: raise ValueError("Unallowed implicit command in %s, position %s" % ( pathdef, len(pathdef.split()) - len(elements))) last_command = command # Used by S and T if command == 'M': # Moveto command. x = elements.pop() y = elements.pop() pos = float(x) + float(y) * 1j if absolute: current_pos = pos else: current_pos += pos segments.append(path.Move(current_pos)) # when M is called, reset start_pos # This behavior of Z is defined in svg spec: # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand start_pos = current_pos # Implicit moveto commands are treated as lineto commands. # So we set command to lineto here, in case there are # further implicit commands after this moveto. command = 'L' elif command == 'Z': # Close path if current_pos != start_pos: segments.append(path.Line(current_pos, start_pos)) segments.closed = True current_pos = start_pos start_pos = None command = None # You can't have implicit commands after closing. elif command == 'L': x = elements.pop() y = elements.pop() pos = float(x) + float(y) * 1j if not absolute: pos += current_pos segments.append(path.Line(current_pos, pos)) current_pos = pos elif command == 'H': x = elements.pop() pos = float(x) + current_pos.imag * 1j if not absolute: pos += current_pos.real segments.append(path.Line(current_pos, pos)) current_pos = pos elif command == 'V': y = elements.pop() pos = current_pos.real + float(y) * 1j if not absolute: pos += current_pos.imag * 1j segments.append(path.Line(current_pos, pos)) current_pos = pos elif command == 'C': control1 = float(elements.pop()) + float(elements.pop()) * 1j control2 = float(elements.pop()) + float(elements.pop()) * 1j end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: control1 += current_pos control2 += current_pos end += current_pos segments.append(path.CubicBezier(current_pos, control1, control2, end)) current_pos = end elif command == 'S': # Smooth curve. First control point is the "reflection" of # the second control point in the previous path. if last_command not in 'CS': # If there is no previous command or if the previous command # was not an C, c, S or s, assume the first control point is # coincident with the current point. control1 = current_pos else: # The first control point is assumed to be the reflection of # the second control point on the previous command relative # to the current point. control1 = current_pos + current_pos - segments[-1].control2 control2 = float(elements.pop()) + float(elements.pop()) * 1j end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: control2 += current_pos end += current_pos segments.append(path.CubicBezier(current_pos, control1, control2, end)) current_pos = end elif command == 'Q': control = float(elements.pop()) + float(elements.pop()) * 1j end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: control += current_pos end += current_pos segments.append(path.QuadraticBezier(current_pos, control, end)) current_pos = end elif command == 'T': # Smooth curve. Control point is the "reflection" of # the second control point in the previous path. if last_command not in 'QT': # If there is no previous command or if the previous command # was not an Q, q, T or t, assume the first control point is # coincident with the current point. control = current_pos else: # The control point is assumed to be the reflection of # the control point on the previous command relative # to the current point. control = current_pos + current_pos - segments[-1].control end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: end += current_pos segments.append(path.QuadraticBezier(current_pos, control, end)) current_pos = end elif command == 'A': radius = float(elements.pop()) + float(elements.pop()) * 1j rotation = float(elements.pop()) arc = float(elements.pop()) sweep = float(elements.pop()) end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: end += current_pos segments.append(path.Arc(current_pos, radius, rotation, arc, sweep, end)) current_pos = end return segments svg.path-3.0/src/svg/path/tests/0000775000175000017500000000000013334563713020230 5ustar lregebrolregebro00000000000000svg.path-3.0/src/svg/path/tests/test_doc.py0000664000175000017500000000024413334563713022406 0ustar lregebrolregebro00000000000000import unittest import doctest def load_tests(loader, tests, ignore): tests.addTests(doctest.DocFileSuite('README.rst', package='__main__')) return tests svg.path-3.0/src/svg/path/tests/__init__.py0000664000175000017500000000000013334563713022327 0ustar lregebrolregebro00000000000000svg.path-3.0/src/svg/path/tests/test_paths.py0000664000175000017500000006474313334563713022776 0ustar lregebrolregebro00000000000000from __future__ import division import unittest from math import sqrt, pi from ..path import CubicBezier, QuadraticBezier, Line, Arc, Path # Most of these test points are not calculated serparately, as that would # take too long and be too error prone. Instead the curves have been verified # to be correct visually, by drawing them with the turtle module, with code # like this: # # import turtle # t = turtle.Turtle() # t.penup() # # for arc in (path1, path2): # p = arc.point(0) # t.goto(p.real - 500, -p.imag + 300) # t.dot(3, 'black') # t.pendown() # for x in range(1, 101): # p = arc.point(x * 0.01) # t.goto(p.real - 500, -p.imag + 300) # t.penup() # t.dot(3, 'black') # # raw_input() # # After the paths have been verified to be correct this way, the testing of # points along the paths has been added as regression tests, to make sure # nobody changes the way curves are drawn by mistake. Therefore, do not take # these points religiously. They might be subtly wrong, unless otherwise # noted. class LineTest(unittest.TestCase): def test_lines(self): # These points are calculated, and not just regression tests. line1 = Line(0j, 400 + 0j) self.assertAlmostEqual(line1.point(0), (0j)) self.assertAlmostEqual(line1.point(0.3), (120 + 0j)) self.assertAlmostEqual(line1.point(0.5), (200 + 0j)) self.assertAlmostEqual(line1.point(0.9), (360 + 0j)) self.assertAlmostEqual(line1.point(1), (400 + 0j)) self.assertAlmostEqual(line1.length(), 400) line2 = Line(400 + 0j, 400 + 300j) self.assertAlmostEqual(line2.point(0), (400 + 0j)) self.assertAlmostEqual(line2.point(0.3), (400 + 90j)) self.assertAlmostEqual(line2.point(0.5), (400 + 150j)) self.assertAlmostEqual(line2.point(0.9), (400 + 270j)) self.assertAlmostEqual(line2.point(1), (400 + 300j)) self.assertAlmostEqual(line2.length(), 300) line3 = Line(400 + 300j, 0j) self.assertAlmostEqual(line3.point(0), (400 + 300j)) self.assertAlmostEqual(line3.point(0.3), (280 + 210j)) self.assertAlmostEqual(line3.point(0.5), (200 + 150j)) self.assertAlmostEqual(line3.point(0.9), (40 + 30j)) self.assertAlmostEqual(line3.point(1), (0j)) self.assertAlmostEqual(line3.length(), 500) def test_equality(self): # This is to test the __eq__ and __ne__ methods, so we can't use # assertEqual and assertNotEqual line = Line(0j, 400 + 0j) self.assertTrue(line == Line(0, 400)) self.assertTrue(line != Line(100, 400)) self.assertFalse(line == str(line)) self.assertTrue(line != str(line)) self.assertFalse(CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line) class CubicBezierTest(unittest.TestCase): def test_approx_circle(self): """This is a approximate circle drawn in Inkscape""" arc1 = CubicBezier( complex(0, 0), complex(0, 109.66797), complex(-88.90345, 198.57142), complex(-198.57142, 198.57142) ) self.assertAlmostEqual(arc1.point(0), (0j)) self.assertAlmostEqual(arc1.point(0.1), (-2.59896457 + 32.20931647j)) self.assertAlmostEqual(arc1.point(0.2), (-10.12330256 + 62.76392816j)) self.assertAlmostEqual(arc1.point(0.3), (-22.16418039 + 91.25500149j)) self.assertAlmostEqual(arc1.point(0.4), (-38.31276448 + 117.27370288j)) self.assertAlmostEqual(arc1.point(0.5), (-58.16022125 + 140.41119875j)) self.assertAlmostEqual(arc1.point(0.6), (-81.29771712 + 160.25865552j)) self.assertAlmostEqual(arc1.point(0.7), (-107.31641851 + 176.40723961j)) self.assertAlmostEqual(arc1.point(0.8), (-135.80749184 + 188.44811744j)) self.assertAlmostEqual(arc1.point(0.9), (-166.36210353 + 195.97245543j)) self.assertAlmostEqual(arc1.point(1), (-198.57142 + 198.57142j)) arc2 = CubicBezier( complex(-198.57142, 198.57142), complex(-109.66797 - 198.57142, 0 + 198.57142), complex(-198.57143 - 198.57142, -88.90345 + 198.57142), complex(-198.57143 - 198.57142, 0), ) self.assertAlmostEqual(arc2.point(0), (-198.57142 + 198.57142j)) self.assertAlmostEqual(arc2.point(0.1), (-230.78073675 + 195.97245543j)) self.assertAlmostEqual(arc2.point(0.2), (-261.3353492 + 188.44811744j)) self.assertAlmostEqual(arc2.point(0.3), (-289.82642365 + 176.40723961j)) self.assertAlmostEqual(arc2.point(0.4), (-315.8451264 + 160.25865552j)) self.assertAlmostEqual(arc2.point(0.5), (-338.98262375 + 140.41119875j)) self.assertAlmostEqual(arc2.point(0.6), (-358.830082 + 117.27370288j)) self.assertAlmostEqual(arc2.point(0.7), (-374.97866745 + 91.25500149j)) self.assertAlmostEqual(arc2.point(0.8), (-387.0195464 + 62.76392816j)) self.assertAlmostEqual(arc2.point(0.9), (-394.54388515 + 32.20931647j)) self.assertAlmostEqual(arc2.point(1), (-397.14285 + 0j)) arc3 = CubicBezier( complex(-198.57143 - 198.57142, 0), complex(0 - 198.57143 - 198.57142, -109.66797), complex(88.90346 - 198.57143 - 198.57142, -198.57143), complex(-198.57142, -198.57143) ) self.assertAlmostEqual(arc3.point(0), (-397.14285 + 0j)) self.assertAlmostEqual(arc3.point(0.1), (-394.54388515 - 32.20931675j)) self.assertAlmostEqual(arc3.point(0.2), (-387.0195464 - 62.7639292j)) self.assertAlmostEqual(arc3.point(0.3), (-374.97866745 - 91.25500365j)) self.assertAlmostEqual(arc3.point(0.4), (-358.830082 - 117.2737064j)) self.assertAlmostEqual(arc3.point(0.5), (-338.98262375 - 140.41120375j)) self.assertAlmostEqual(arc3.point(0.6), (-315.8451264 - 160.258662j)) self.assertAlmostEqual(arc3.point(0.7), (-289.82642365 - 176.40724745j)) self.assertAlmostEqual(arc3.point(0.8), (-261.3353492 - 188.4481264j)) self.assertAlmostEqual(arc3.point(0.9), (-230.78073675 - 195.97246515j)) self.assertAlmostEqual(arc3.point(1), (-198.57142 - 198.57143j)) arc4 = CubicBezier( complex(-198.57142, -198.57143), complex(109.66797 - 198.57142, 0 - 198.57143), complex(0, 88.90346 - 198.57143), complex(0, 0), ) self.assertAlmostEqual(arc4.point(0), (-198.57142 - 198.57143j)) self.assertAlmostEqual(arc4.point(0.1), (-166.36210353 - 195.97246515j)) self.assertAlmostEqual(arc4.point(0.2), (-135.80749184 - 188.4481264j)) self.assertAlmostEqual(arc4.point(0.3), (-107.31641851 - 176.40724745j)) self.assertAlmostEqual(arc4.point(0.4), (-81.29771712 - 160.258662j)) self.assertAlmostEqual(arc4.point(0.5), (-58.16022125 - 140.41120375j)) self.assertAlmostEqual(arc4.point(0.6), (-38.31276448 - 117.2737064j)) self.assertAlmostEqual(arc4.point(0.7), (-22.16418039 - 91.25500365j)) self.assertAlmostEqual(arc4.point(0.8), (-10.12330256 - 62.7639292j)) self.assertAlmostEqual(arc4.point(0.9), (-2.59896457 - 32.20931675j)) self.assertAlmostEqual(arc4.point(1), (0j)) def test_svg_examples(self): # M100,200 C100,100 250,100 250,200 path1 = CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j) self.assertAlmostEqual(path1.point(0), (100 + 200j)) self.assertAlmostEqual(path1.point(0.3), (132.4 + 137j)) self.assertAlmostEqual(path1.point(0.5), (175 + 125j)) self.assertAlmostEqual(path1.point(0.9), (245.8 + 173j)) self.assertAlmostEqual(path1.point(1), (250 + 200j)) # S400,300 400,200 path2 = CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j) self.assertAlmostEqual(path2.point(0), (250 + 200j)) self.assertAlmostEqual(path2.point(0.3), (282.4 + 263j)) self.assertAlmostEqual(path2.point(0.5), (325 + 275j)) self.assertAlmostEqual(path2.point(0.9), (395.8 + 227j)) self.assertAlmostEqual(path2.point(1), (400 + 200j)) # M100,200 C100,100 400,100 400,200 path3 = CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j) self.assertAlmostEqual(path3.point(0), (100 + 200j)) self.assertAlmostEqual(path3.point(0.3), (164.8 + 137j)) self.assertAlmostEqual(path3.point(0.5), (250 + 125j)) self.assertAlmostEqual(path3.point(0.9), (391.6 + 173j)) self.assertAlmostEqual(path3.point(1), (400 + 200j)) # M100,500 C25,400 475,400 400,500 path4 = CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j) self.assertAlmostEqual(path4.point(0), (100 + 500j)) self.assertAlmostEqual(path4.point(0.3), (145.9 + 437j)) self.assertAlmostEqual(path4.point(0.5), (250 + 425j)) self.assertAlmostEqual(path4.point(0.9), (407.8 + 473j)) self.assertAlmostEqual(path4.point(1), (400 + 500j)) # M100,800 C175,700 325,700 400,800 path5 = CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j) self.assertAlmostEqual(path5.point(0), (100 + 800j)) self.assertAlmostEqual(path5.point(0.3), (183.7 + 737j)) self.assertAlmostEqual(path5.point(0.5), (250 + 725j)) self.assertAlmostEqual(path5.point(0.9), (375.4 + 773j)) self.assertAlmostEqual(path5.point(1), (400 + 800j)) # M600,200 C675,100 975,100 900,200 path6 = CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j) self.assertAlmostEqual(path6.point(0), (600 + 200j)) self.assertAlmostEqual(path6.point(0.3), (712.05 + 137j)) self.assertAlmostEqual(path6.point(0.5), (806.25 + 125j)) self.assertAlmostEqual(path6.point(0.9), (911.85 + 173j)) self.assertAlmostEqual(path6.point(1), (900 + 200j)) # M600,500 C600,350 900,650 900,500 path7 = CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) self.assertAlmostEqual(path7.point(0), (600 + 500j)) self.assertAlmostEqual(path7.point(0.3), (664.8 + 462.2j)) self.assertAlmostEqual(path7.point(0.5), (750 + 500j)) self.assertAlmostEqual(path7.point(0.9), (891.6 + 532.4j)) self.assertAlmostEqual(path7.point(1), (900 + 500j)) # M600,800 C625,700 725,700 750,800 path8 = CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j) self.assertAlmostEqual(path8.point(0), (600 + 800j)) self.assertAlmostEqual(path8.point(0.3), (638.7 + 737j)) self.assertAlmostEqual(path8.point(0.5), (675 + 725j)) self.assertAlmostEqual(path8.point(0.9), (740.4 + 773j)) self.assertAlmostEqual(path8.point(1), (750 + 800j)) # S875,900 900,800 inversion = (750 + 800j) + (750 + 800j) - (725 + 700j) path9 = CubicBezier(750 + 800j, inversion, 875 + 900j, 900 + 800j) self.assertAlmostEqual(path9.point(0), (750 + 800j)) self.assertAlmostEqual(path9.point(0.3), (788.7 + 863j)) self.assertAlmostEqual(path9.point(0.5), (825 + 875j)) self.assertAlmostEqual(path9.point(0.9), (890.4 + 827j)) self.assertAlmostEqual(path9.point(1), (900 + 800j)) def test_length(self): # A straight line: arc = CubicBezier( complex(0, 0), complex(0, 0), complex(0, 100), complex(0, 100) ) self.assertAlmostEqual(arc.length(), 100) # A diagonal line: arc = CubicBezier( complex(0, 0), complex(0, 0), complex(100, 100), complex(100, 100) ) self.assertAlmostEqual(arc.length(), sqrt(2 * 100 * 100)) # A quarter circle arc with radius 100: kappa = 4 * (sqrt(2) - 1) / 3 # http://www.whizkidtech.redprince.net/bezier/circle/ arc = CubicBezier( complex(0, 0), complex(0, kappa * 100), complex(100 - kappa * 100, 100), complex(100, 100) ) # We can't compare with pi*50 here, because this is just an # approximation of a circle arc. pi*50 is 157.079632679 # So this is just yet another "warn if this changes" test. # This value is not verified to be correct. self.assertAlmostEqual(arc.length(), 157.1016698) # A recursive solution has also been suggested, but for CubicBezier # curves it could get a false solution on curves where the midpoint is on a # straight line between the start and end. For example, the following # curve would get solved as a straight line and get the length 300. # Make sure this is not the case. arc = CubicBezier( complex(600, 500), complex(600, 350), complex(900, 650), complex(900, 500) ) self.assertTrue(arc.length() > 300.0) def test_equality(self): # This is to test the __eq__ and __ne__ methods, so we can't use # assertEqual and assertNotEqual segment = CubicBezier(complex(600, 500), complex(600, 350), complex(900, 650), complex(900, 500)) self.assertTrue(segment == CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)) self.assertTrue(segment != CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j)) self.assertTrue(segment != Line(0, 400)) class QuadraticBezierTest(unittest.TestCase): def test_svg_examples(self): """These is the path in the SVG specs""" # M200,300 Q400,50 600,300 T1000,300 path1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) self.assertAlmostEqual(path1.point(0), (200 + 300j)) self.assertAlmostEqual(path1.point(0.3), (320 + 195j)) self.assertAlmostEqual(path1.point(0.5), (400 + 175j)) self.assertAlmostEqual(path1.point(0.9), (560 + 255j)) self.assertAlmostEqual(path1.point(1), (600 + 300j)) # T1000, 300 inversion = (600 + 300j) + (600 + 300j) - (400 + 50j) path2 = QuadraticBezier(600 + 300j, inversion, 1000 + 300j) self.assertAlmostEqual(path2.point(0), (600 + 300j)) self.assertAlmostEqual(path2.point(0.3), (720 + 405j)) self.assertAlmostEqual(path2.point(0.5), (800 + 425j)) self.assertAlmostEqual(path2.point(0.9), (960 + 345j)) self.assertAlmostEqual(path2.point(1), (1000 + 300j)) def test_length(self): # expected results calculated with # svg.path.segment_length(q, 0, 1, q.start, q.end, 1e-14, 20, 0) q1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j) closedq = QuadraticBezier(6+2j, 5-1j, 6+2j) linq1 = QuadraticBezier(1, 2, 3) linq2 = QuadraticBezier(1+3j, 2+5j, -9 - 17j) nodalq = QuadraticBezier(1, 1, 1) tests = [(q1, 487.77109389525975), (q2, 379.90458193489155), (closedq, 3.1622776601683795), (linq1, 2), (linq2, 22.73335777124786), (nodalq, 0)] for q, exp_res in tests: self.assertAlmostEqual(q.length(), exp_res) def test_equality(self): # This is to test the __eq__ and __ne__ methods, so we can't use # assertEqual and assertNotEqual segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)) self.assertTrue(segment != QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j)) self.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)) self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment) class ArcTest(unittest.TestCase): def test_points(self): arc1 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) self.assertAlmostEqual(arc1.center, 100 + 0j) self.assertAlmostEqual(arc1.theta, 180.0) self.assertAlmostEqual(arc1.delta, -90.0) self.assertAlmostEqual(arc1.point(0.0), (0j)) self.assertAlmostEqual(arc1.point(0.1), (1.23116594049 + 7.82172325201j)) self.assertAlmostEqual(arc1.point(0.2), (4.89434837048 + 15.4508497187j)) self.assertAlmostEqual(arc1.point(0.3), (10.8993475812 + 22.699524987j)) self.assertAlmostEqual(arc1.point(0.4), (19.0983005625 + 29.3892626146j)) self.assertAlmostEqual(arc1.point(0.5), (29.2893218813 + 35.3553390593j)) self.assertAlmostEqual(arc1.point(0.6), (41.2214747708 + 40.4508497187j)) self.assertAlmostEqual(arc1.point(0.7), (54.6009500260 + 44.5503262094j)) self.assertAlmostEqual(arc1.point(0.8), (69.0983005625 + 47.5528258148j)) self.assertAlmostEqual(arc1.point(0.9), (84.3565534960 + 49.3844170298j)) self.assertAlmostEqual(arc1.point(1.0), (100 + 50j)) arc2 = Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j) self.assertAlmostEqual(arc2.center, 50j) self.assertAlmostEqual(arc2.theta, 270.0) self.assertAlmostEqual(arc2.delta, -270.0) self.assertAlmostEqual(arc2.point(0.0), (0j)) self.assertAlmostEqual(arc2.point(0.1), (-45.399049974 + 5.44967379058j)) self.assertAlmostEqual(arc2.point(0.2), (-80.9016994375 + 20.6107373854j)) self.assertAlmostEqual(arc2.point(0.3), (-98.7688340595 + 42.178276748j)) self.assertAlmostEqual(arc2.point(0.4), (-95.1056516295 + 65.4508497187j)) self.assertAlmostEqual(arc2.point(0.5), (-70.7106781187 + 85.3553390593j)) self.assertAlmostEqual(arc2.point(0.6), (-30.9016994375 + 97.5528258148j)) self.assertAlmostEqual(arc2.point(0.7), (15.643446504 + 99.3844170298j)) self.assertAlmostEqual(arc2.point(0.8), (58.7785252292 + 90.4508497187j)) self.assertAlmostEqual(arc2.point(0.9), (89.1006524188 + 72.699524987j)) self.assertAlmostEqual(arc2.point(1.0), (100 + 50j)) arc3 = Arc(0j, 100 + 50j, 0, 0, 1, 100 + 50j) self.assertAlmostEqual(arc3.center, 50j) self.assertAlmostEqual(arc3.theta, 270.0) self.assertAlmostEqual(arc3.delta, 90.0) self.assertAlmostEqual(arc3.point(0.0), (0j)) self.assertAlmostEqual(arc3.point(0.1), (15.643446504 + 0.615582970243j)) self.assertAlmostEqual(arc3.point(0.2), (30.9016994375 + 2.44717418524j)) self.assertAlmostEqual(arc3.point(0.3), (45.399049974 + 5.44967379058j)) self.assertAlmostEqual(arc3.point(0.4), (58.7785252292 + 9.54915028125j)) self.assertAlmostEqual(arc3.point(0.5), (70.7106781187 + 14.6446609407j)) self.assertAlmostEqual(arc3.point(0.6), (80.9016994375 + 20.6107373854j)) self.assertAlmostEqual(arc3.point(0.7), (89.1006524188 + 27.300475013j)) self.assertAlmostEqual(arc3.point(0.8), (95.1056516295 + 34.5491502813j)) self.assertAlmostEqual(arc3.point(0.9), (98.7688340595 + 42.178276748j)) self.assertAlmostEqual(arc3.point(1.0), (100 + 50j)) arc4 = Arc(0j, 100 + 50j, 0, 1, 1, 100 + 50j) self.assertAlmostEqual(arc4.center, 100 + 0j) self.assertAlmostEqual(arc4.theta, 180.0) self.assertAlmostEqual(arc4.delta, 270.0) self.assertAlmostEqual(arc4.point(0.0), (0j)) self.assertAlmostEqual(arc4.point(0.1), (10.8993475812 - 22.699524987j)) self.assertAlmostEqual(arc4.point(0.2), (41.2214747708 - 40.4508497187j)) self.assertAlmostEqual(arc4.point(0.3), (84.3565534960 - 49.3844170298j)) self.assertAlmostEqual(arc4.point(0.4), (130.901699437 - 47.5528258148j)) self.assertAlmostEqual(arc4.point(0.5), (170.710678119 - 35.3553390593j)) self.assertAlmostEqual(arc4.point(0.6), (195.105651630 - 15.4508497187j)) self.assertAlmostEqual(arc4.point(0.7), (198.768834060 + 7.82172325201j)) self.assertAlmostEqual(arc4.point(0.8), (180.901699437 + 29.3892626146j)) self.assertAlmostEqual(arc4.point(0.9), (145.399049974 + 44.5503262094j)) self.assertAlmostEqual(arc4.point(1.0), (100 + 50j)) def test_length(self): # I'll test the length calculations by making a circle, in two parts. arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j) arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j) self.assertAlmostEqual(arc1.length(), pi * 100) self.assertAlmostEqual(arc2.length(), pi * 100) def test_equality(self): # This is to test the __eq__ and __ne__ methods, so we can't use # assertEqual and assertNotEqual segment = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) self.assertTrue(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)) self.assertTrue(segment != Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j)) def test_issue25(self): # This raised a math domain error Arc((725.307482225571-915.5548199281527j), (202.79421639137703+148.77294617167183j), 225.6910319606926, 1, 1, (-624.6375539637027+896.5483089399895j)) class TestPath(unittest.TestCase): def test_circle(self): arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j) arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j) path = Path(arc1, arc2) self.assertAlmostEqual(path.point(0.0), (0j)) self.assertAlmostEqual(path.point(0.25), (100 + 100j)) self.assertAlmostEqual(path.point(0.5), (200 + 0j)) self.assertAlmostEqual(path.point(0.75), (100 - 100j)) self.assertAlmostEqual(path.point(1.0), (0j)) self.assertAlmostEqual(path.length(), pi * 200) def test_svg_specs(self): """The paths that are in the SVG specs""" # Big pie: M300,200 h-150 a150,150 0 1,0 150,-150 z path = Path(Line(300 + 200j, 150 + 200j), Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), Line(300 + 50j, 300 + 200j)) # The points and length for this path are calculated and not regression tests. self.assertAlmostEqual(path.point(0.0), (300 + 200j)) self.assertAlmostEqual(path.point(0.14897825542), (150 + 200j)) self.assertAlmostEqual(path.point(0.5), (406.066017177 + 306.066017177j)) self.assertAlmostEqual(path.point(1 - 0.14897825542), (300 + 50j)) self.assertAlmostEqual(path.point(1.0), (300 + 200j)) # The errors seem to accumulate. Still 6 decimal places is more than good enough. self.assertAlmostEqual(path.length(), pi * 225 + 300, places=6) # Little pie: M275,175 v-150 a150,150 0 0,0 -150,150 z path = Path(Line(275 + 175j, 275 + 25j), Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), Line(125 + 175j, 275 + 175j)) # The points and length for this path are calculated and not regression tests. self.assertAlmostEqual(path.point(0.0), (275 + 175j)) self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j)) self.assertAlmostEqual(path.point(0.5), (168.93398282201787 + 68.93398282201787j)) self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j)) self.assertAlmostEqual(path.point(1.0), (275 + 175j)) # The errors seem to accumulate. Still 6 decimal places is more than good enough. self.assertAlmostEqual(path.length(), pi * 75 + 300, places=6) # Bumpy path: M600,350 l 50,-25 # a25,25 -30 0,1 50,-25 l 50,-25 # a25,50 -30 0,1 50,-25 l 50,-25 # a25,75 -30 0,1 50,-25 l 50,-25 # a25,100 -30 0,1 50,-25 l 50,-25 path = Path(Line(600 + 350j, 650 + 325j), Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j), Line(700 + 300j, 750 + 275j), Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j), Line(800 + 250j, 850 + 225j), Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j), Line(900 + 200j, 950 + 175j), Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j), Line(1000 + 150j, 1050 + 125j), ) # These are *not* calculated, but just regression tests. Be skeptical. self.assertAlmostEqual(path.point(0.0), (600 + 350j)) self.assertAlmostEqual(path.point(0.3), (755.31526434 + 217.51578768j)) self.assertAlmostEqual(path.point(0.5), (832.23324151 + 156.33454892j)) self.assertAlmostEqual(path.point(0.9), (974.00559321 + 115.26473532j)) self.assertAlmostEqual(path.point(1.0), (1050 + 125j)) # The errors seem to accumulate. Still 6 decimal places is more than good enough. self.assertAlmostEqual(path.length(), 860.6756221710) def test_repr(self): path = Path( Line(start=600 + 350j, end=650 + 325j), Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, arc=0, sweep=1, end=700 + 300j), CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) self.assertEqual(eval(repr(path)), path) def test_reverse(self): # Currently you can't reverse paths. self.assertRaises(NotImplementedError, Path().reverse) def test_equality(self): # This is to test the __eq__ and __ne__ methods, so we can't use # assertEqual and assertNotEqual path1 = Path( Line(start=600 + 350j, end=650 + 325j), Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, arc=0, sweep=1, end=700 + 300j), CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) path2 = Path( Line(start=600 + 350j, end=650 + 325j), Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, arc=0, sweep=1, end=700 + 300j), CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) self.assertTrue(path1 == path2) # Modify path2: path2[0].start = 601 + 350j self.assertTrue(path1 != path2) # Modify back: path2[0].start = 600 + 350j self.assertFalse(path1 != path2) # Get rid of the last segment: del path2[-1] self.assertFalse(path1 == path2) # It's not equal to a list of it's segments self.assertTrue(path1 != path1[:]) self.assertFalse(path1 == path1[:]) def test_non_arc(self): # And arc with the same start and end is a noop. segment = Arc(0j + 70j, 35 + 35j, 0, 1, 0, 0 + 70j) self.assertEqual(segment.length(), 0) self.assertEqual(segment.point(0.5), segment.start) svg.path-3.0/src/svg/path/tests/test_generation.py0000664000175000017500000000223613334563713023777 0ustar lregebrolregebro00000000000000from __future__ import division import unittest from ..path import CubicBezier, QuadraticBezier, Line, Arc, Path from ..parser import parse_path class TestGeneration(unittest.TestCase): def test_svg_examples(self): """Examples from the SVG spec""" paths = [ 'M 100,100 L 300,100 L 200,300 Z', 'M 0,0 L 50,20 M 100,100 L 300,100 L 200,300 Z', 'M 100,100 L 200,200', 'M 100,200 L 200,100 L -100,-200', 'M 100,200 C 100,100 250,100 250,200 S 400,300 400,200', 'M 100,200 C 100,100 400,100 400,200', 'M 100,500 C 25,400 475,400 400,500', 'M 100,800 C 175,700 325,700 400,800', 'M 600,200 C 675,100 975,100 900,200', 'M 600,500 C 600,350 900,650 900,500', 'M 600,800 C 625,700 725,700 750,800 S 875,900 900,800', 'M 200,300 Q 400,50 600,300 T 1000,300', 'M -3.4E+38,3.4E+38 L -3.4E-38,3.4E-38', 'M 0,0 L 50,20 M 50,20 L 200,100 Z', 'M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275', ] for path in paths: self.assertEqual(parse_path(path).d(), path) svg.path-3.0/src/svg/path/tests/test_parsing.py0000664000175000017500000001722713334563713023315 0ustar lregebrolregebro00000000000000from __future__ import division import unittest from ..path import CubicBezier, QuadraticBezier, Line, Arc, Path, Move from ..parser import parse_path class TestParser(unittest.TestCase): def test_svg_examples(self): """Examples from the SVG spec""" path1 = parse_path('M 100 100 L 300 100 L 200 300 z') self.assertEqual(path1, Path(Move(100 + 100j), Line(100 + 100j, 300 + 100j), Line(300 + 100j, 200 + 300j), Line(200 + 300j, 100 + 100j))) self.assertTrue(path1.closed) # for Z command behavior when there is multiple subpaths path1 = parse_path('M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z') self.assertEqual(path1, Path( Move(0j), Line(0 + 0j, 50 + 20j), Move(100+100j), Line(100 + 100j, 300 + 100j), Line(300 + 100j, 200 + 300j), Line(200 + 300j, 100 + 100j))) path1 = parse_path('M 100 100 L 200 200') path2 = parse_path('M100 100L200 200') self.assertEqual(path1, path2) path1 = parse_path('M 100 200 L 200 100 L -100 -200') path2 = parse_path('M 100 200 L 200 100 -100 -200') self.assertEqual(path1, path2) path1 = parse_path("""M100,200 C100,100 250,100 250,200 S400,300 400,200""") self.assertEqual(path1, Path(Move(100 + 200j), CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j), CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j))) path1 = parse_path('M100,200 C100,100 400,100 400,200') self.assertEqual(path1, Path(Move(100 + 200j), CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j))) path1 = parse_path('M100,500 C25,400 475,400 400,500') self.assertEqual(path1, Path(Move(100 + 500j), CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j))) path1 = parse_path('M100,800 C175,700 325,700 400,800') self.assertEqual(path1, Path(Move(100+800j), CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j))) path1 = parse_path('M600,200 C675,100 975,100 900,200') self.assertEqual(path1, Path(Move(600 + 200j), CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j))) path1 = parse_path('M600,500 C600,350 900,650 900,500') self.assertEqual(path1, Path(Move(600 + 500j), CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))) path1 = parse_path("""M600,800 C625,700 725,700 750,800 S875,900 900,800""") self.assertEqual(path1, Path(Move(600 + 800j), CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j), CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j))) path1 = parse_path('M200,300 Q400,50 600,300 T1000,300') self.assertEqual(path1, Path(Move(200 + 300j), QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j), QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j))) path1 = parse_path('M300,200 h-150 a150,150 0 1,0 150,-150 z') self.assertEqual(path1, Path(Move(300 + 200j), Line(300 + 200j, 150 + 200j), Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), Line(300 + 50j, 300 + 200j))) path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 z') self.assertEqual(path1, Path(Move(275 + 175j), Line(275 + 175j, 275 + 25j), Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), Line(125 + 175j, 275 + 175j))) path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 L 275,175 z') self.assertEqual(path1, Path(Move(275 + 175j), Line(275 + 175j, 275 + 25j), Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), Line(125 + 175j, 275 + 175j))) path1 = parse_path("""M600,350 l 50,-25 a25,25 -30 0,1 50,-25 l 50,-25 a25,50 -30 0,1 50,-25 l 50,-25 a25,75 -30 0,1 50,-25 l 50,-25 a25,100 -30 0,1 50,-25 l 50,-25""") self.assertEqual(path1, Path(Move(600 + 350j), Line(600 + 350j, 650 + 325j), Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j), Line(700 + 300j, 750 + 275j), Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j), Line(800 + 250j, 850 + 225j), Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j), Line(900 + 200j, 950 + 175j), Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j), Line(1000 + 150j, 1050 + 125j))) def test_others(self): # Other paths that need testing: # Relative moveto: path1 = parse_path('M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z') self.assertEqual(path1, Path( Move(0j), Line(0 + 0j, 50 + 20j), Move(100 + 100j), Line(100 + 100j, 300 + 100j), Line(300 + 100j, 200 + 300j), Line(200 + 300j, 100 + 100j))) # Initial smooth and relative CubicBezier path1 = parse_path("""M100,200 s 150,-100 150,0""") self.assertEqual(path1, Path(Move(100 + 200j), CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j))) # Initial smooth and relative QuadraticBezier path1 = parse_path("""M100,200 t 150,0""") self.assertEqual(path1, Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j))) # Relative QuadraticBezier path1 = parse_path("""M100,200 q 0,0 150,0""") self.assertEqual(path1, Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j))) def test_negative(self): """You don't need spaces before a minus-sign""" path1 = parse_path('M100,200c10-5,20-10,30-20') path2 = parse_path('M 100 200 c 10 -5 20 -10 30 -20') self.assertEqual(path1, path2) def test_numbers(self): """Exponents and other number format cases""" # It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported. path1 = parse_path('M-3.4e38 3.4E+38L-3.4E-38,3.4e-38') path2 = Path(Move(-3.4e+38 + 3.4e+38j), Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j)) self.assertEqual(path1, path2) def test_errors(self): self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200') def test_non_path(self): # It's possible in SVG to create paths that has zero length, # we need to handle that. path = parse_path("M10.236,100.184") self.assertEqual(path.d(), 'M 10.236,100.184') svg.path-3.0/src/svg/path/path.py0000664000175000017500000004127513334563713020405 0ustar lregebrolregebro00000000000000from __future__ import division from math import sqrt, cos, sin, acos, degrees, radians, log from collections import MutableSequence # This file contains classes for the different types of SVG path segments as # well as a Path object that contains a sequence of path segments. MIN_DEPTH = 5 ERROR = 1e-12 def segment_length(curve, start, end, start_point, end_point, error, min_depth, depth): """Recursively approximates the length by straight lines""" mid = (start + end) / 2 mid_point = curve.point(mid) length = abs(end_point - start_point) first_half = abs(mid_point - start_point) second_half = abs(end_point - mid_point) length2 = first_half + second_half if (length2 - length > error) or (depth < min_depth): # Calculate the length of each segment: depth += 1 return (segment_length(curve, start, mid, start_point, mid_point, error, min_depth, depth) + segment_length(curve, mid, end, mid_point, end_point, error, min_depth, depth)) # This is accurate enough. return length2 class Line(object): def __init__(self, start, end): self.start = start self.end = end def __repr__(self): return 'Line(start=%s, end=%s)' % (self.start, self.end) def __eq__(self, other): if not isinstance(other, Line): return NotImplemented return self.start == other.start and self.end == other.end def __ne__(self, other): if not isinstance(other, Line): return NotImplemented return not self == other def point(self, pos): distance = self.end - self.start return self.start + distance * pos def length(self, error=None, min_depth=None): distance = (self.end - self.start) return sqrt(distance.real ** 2 + distance.imag ** 2) class CubicBezier(object): def __init__(self, start, control1, control2, end): self.start = start self.control1 = control1 self.control2 = control2 self.end = end def __repr__(self): return 'CubicBezier(start=%s, control1=%s, control2=%s, end=%s)' % ( self.start, self.control1, self.control2, self.end) def __eq__(self, other): if not isinstance(other, CubicBezier): return NotImplemented return self.start == other.start and self.end == other.end and \ self.control1 == other.control1 and self.control2 == other.control2 def __ne__(self, other): if not isinstance(other, CubicBezier): return NotImplemented return not self == other def is_smooth_from(self, previous): """Checks if this segment would be a smooth segment following the previous""" if isinstance(previous, CubicBezier): return (self.start == previous.end and (self.control1 - self.start) == (previous.end - previous.control2)) else: return self.control1 == self.start def point(self, pos): """Calculate the x,y position at a certain position of the path""" return ((1 - pos) ** 3 * self.start) + \ (3 * (1 - pos) ** 2 * pos * self.control1) + \ (3 * (1 - pos) * pos ** 2 * self.control2) + \ (pos ** 3 * self.end) def length(self, error=ERROR, min_depth=MIN_DEPTH): """Calculate the length of the path up to a certain position""" start_point = self.point(0) end_point = self.point(1) return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0) class QuadraticBezier(object): def __init__(self, start, control, end): self.start = start self.end = end self.control = control def __repr__(self): return 'QuadraticBezier(start=%s, control=%s, end=%s)' % ( self.start, self.control, self.end) def __eq__(self, other): if not isinstance(other, QuadraticBezier): return NotImplemented return self.start == other.start and self.end == other.end and \ self.control == other.control def __ne__(self, other): if not isinstance(other, QuadraticBezier): return NotImplemented return not self == other def is_smooth_from(self, previous): """Checks if this segment would be a smooth segment following the previous""" if isinstance(previous, QuadraticBezier): return (self.start == previous.end and (self.control - self.start) == (previous.end - previous.control)) else: return self.control == self.start def point(self, pos): return (1 - pos) ** 2 * self.start + 2 * (1 - pos) * pos * self.control + \ pos ** 2 * self.end def length(self, error=None, min_depth=None): a = self.start - 2*self.control + self.end b = 2*(self.control - self.start) a_dot_b = a.real*b.real + a.imag*b.imag if abs(a) < 1e-12: s = abs(b) elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12: k = abs(b)/abs(a) if k >= 2: s = abs(b) - abs(a) else: s = abs(a)*(k**2/2 - k + 1) else: # For an explanation of this case, see # http://www.malczak.info/blog/quadratic-bezier-curve-length/ A = 4 * (a.real ** 2 + a.imag ** 2) B = 4 * (a.real * b.real + a.imag * b.imag) C = b.real ** 2 + b.imag ** 2 Sabc = 2 * sqrt(A + B + C) A2 = sqrt(A) A32 = 2 * A * A2 C2 = 2 * sqrt(C) BA = B / A2 s = (A32 * Sabc + A2 * B * (Sabc - C2) + (4 * C * A - B ** 2) * log((2 * A2 + BA + Sabc) / (BA + C2))) / (4 * A32) return s class Arc(object): def __init__(self, start, radius, rotation, arc, sweep, end): """radius is complex, rotation is in degrees, large and sweep are 1 or 0 (True/False also work)""" self.start = start self.radius = radius self.rotation = rotation self.arc = bool(arc) self.sweep = bool(sweep) self.end = end self._parameterize() def __repr__(self): return 'Arc(start=%s, radius=%s, rotation=%s, arc=%s, sweep=%s, end=%s)' % ( self.start, self.radius, self.rotation, self.arc, self.sweep, self.end) def __eq__(self, other): if not isinstance(other, Arc): return NotImplemented return self.start == other.start and self.end == other.end and \ self.radius == other.radius and self.rotation == other.rotation and \ self.arc == other.arc and self.sweep == other.sweep def __ne__(self, other): if not isinstance(other, Arc): return NotImplemented return not self == other def _parameterize(self): # Conversion from endpoint to center parameterization # http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes if self.start == self.end: # This is equivalent of omitting the segment, so do nothing return cosr = cos(radians(self.rotation)) sinr = sin(radians(self.rotation)) dx = (self.start.real - self.end.real) / 2 dy = (self.start.imag - self.end.imag) / 2 x1prim = cosr * dx + sinr * dy x1prim_sq = x1prim * x1prim y1prim = -sinr * dx + cosr * dy y1prim_sq = y1prim * y1prim rx = self.radius.real rx_sq = rx * rx ry = self.radius.imag ry_sq = ry * ry # Correct out of range radii radius_check = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq) if radius_check > 1: rx *= sqrt(radius_check) ry *= sqrt(radius_check) rx_sq = rx * rx ry_sq = ry * ry t1 = rx_sq * y1prim_sq t2 = ry_sq * x1prim_sq c = sqrt(abs((rx_sq * ry_sq - t1 - t2) / (t1 + t2))) if self.arc == self.sweep: c = -c cxprim = c * rx * y1prim / ry cyprim = -c * ry * x1prim / rx self.center = complex((cosr * cxprim - sinr * cyprim) + ((self.start.real + self.end.real) / 2), (sinr * cxprim + cosr * cyprim) + ((self.start.imag + self.end.imag) / 2)) ux = (x1prim - cxprim) / rx uy = (y1prim - cyprim) / ry vx = (-x1prim - cxprim) / rx vy = (-y1prim - cyprim) / ry n = sqrt(ux * ux + uy * uy) p = ux theta = degrees(acos(p / n)) if uy < 0: theta = -theta self.theta = theta % 360 n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)) p = ux * vx + uy * vy d = p/n # In certain cases the above calculation can through inaccuracies # become just slightly out of range, f ex -1.0000000000000002. if d > 1.0: d = 1.0 elif d < -1.0: d = -1.0 delta = degrees(acos(d)) if (ux * vy - uy * vx) < 0: delta = -delta self.delta = delta % 360 if not self.sweep: self.delta -= 360 def point(self, pos): if self.start == self.end: # This is equivalent of omitting the segment return self.start angle = radians(self.theta + (self.delta * pos)) cosr = cos(radians(self.rotation)) sinr = sin(radians(self.rotation)) x = (cosr * cos(angle) * self.radius.real - sinr * sin(angle) * self.radius.imag + self.center.real) y = (sinr * cos(angle) * self.radius.real + cosr * sin(angle) * self.radius.imag + self.center.imag) return complex(x, y) def length(self, error=ERROR, min_depth=MIN_DEPTH): """The length of an elliptical arc segment requires numerical integration, and in that case it's simpler to just do a geometric approximation, as for cubic bezier curves. """ if self.start == self.end: # This is equivalent of omitting the segment return 0 start_point = self.point(0) end_point = self.point(1) return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0) class Move(object): """Represents move commands. Does nothing, but is there to handle paths that consist of only move commands, which is valid, but pointless. """ def __init__(self, to): self.start = self.end = to def __repr__(self): return 'Move(to=%s)' % self.start def __eq__(self, other): if not isinstance(other, Move): return NotImplemented return self.start == other.start def __ne__(self, other): if not isinstance(other, Move): return NotImplemented return not self == other def point(self, pos): return self.start def length(self, error=ERROR, min_depth=MIN_DEPTH): return 0 class Path(MutableSequence): """A Path is a sequence of path segments""" # Put it here, so there is a default if unpickled. _closed = False def __init__(self, *segments, **kw): self._segments = list(segments) self._length = None self._lengths = None if 'closed' in kw: self.closed = kw['closed'] def __getitem__(self, index): return self._segments[index] def __setitem__(self, index, value): self._segments[index] = value self._length = None def __delitem__(self, index): del self._segments[index] self._length = None def insert(self, index, value): self._segments.insert(index, value) self._length = None def reverse(self): # Reversing the order of a path would require reversing each element # as well. That's not implemented. raise NotImplementedError def __len__(self): return len(self._segments) def __repr__(self): return 'Path(%s, closed=%s)' % ( ', '.join(repr(x) for x in self._segments), self.closed) def __eq__(self, other): if not isinstance(other, Path): return NotImplemented if len(self) != len(other): return False for s, o in zip(self._segments, other._segments): if not s == o: return False return True def __ne__(self, other): if not isinstance(other, Path): return NotImplemented return not self == other def _calc_lengths(self, error=ERROR, min_depth=MIN_DEPTH): if self._length is not None: return lengths = [each.length(error=error, min_depth=min_depth) for each in self._segments] self._length = sum(lengths) self._lengths = [each / self._length for each in lengths] def point(self, pos, error=ERROR): # Shortcuts if pos == 0.0: return self._segments[0].point(pos) if pos == 1.0: return self._segments[-1].point(pos) self._calc_lengths(error=error) # Find which segment the point we search for is located on: segment_start = 0 for index, segment in enumerate(self._segments): segment_end = segment_start + self._lengths[index] if segment_end >= pos: # This is the segment! How far in on the segment is the point? segment_pos = (pos - segment_start) / (segment_end - segment_start) break segment_start = segment_end return segment.point(segment_pos) def length(self, error=ERROR, min_depth=MIN_DEPTH): self._calc_lengths(error, min_depth) return self._length def _is_closable(self): """Returns true if the end is on the start of a segment""" end = self[-1].end for segment in self: if segment.start == end: return True return False @property def closed(self): """Checks that the path is closed""" return self._closed and self._is_closable() @closed.setter def closed(self, value): value = bool(value) if value and not self._is_closable(): raise ValueError("End does not coincide with a segment start.") self._closed = value def d(self): if self.closed: segments = self[:-1] else: segments = self[:] current_pos = None parts = [] previous_segment = None end = self[-1].end for segment in segments: start = segment.start # If the start of this segment does not coincide with the end of # the last segment or if this segment is actually the close point # of a closed path, then we should start a new subpath here. if isinstance(segment, Move) or (current_pos != start) or ( start == end and not isinstance(previous_segment, Move)): parts.append('M {0:G},{1:G}'.format(start.real, start.imag)) if isinstance(segment, Line): parts.append('L {0:G},{1:G}'.format( segment.end.real, segment.end.imag) ) elif isinstance(segment, CubicBezier): if segment.is_smooth_from(previous_segment): parts.append('S {0:G},{1:G} {2:G},{3:G}'.format( segment.control2.real, segment.control2.imag, segment.end.real, segment.end.imag) ) else: parts.append('C {0:G},{1:G} {2:G},{3:G} {4:G},{5:G}'.format( segment.control1.real, segment.control1.imag, segment.control2.real, segment.control2.imag, segment.end.real, segment.end.imag) ) elif isinstance(segment, QuadraticBezier): if segment.is_smooth_from(previous_segment): parts.append('T {0:G},{1:G}'.format( segment.end.real, segment.end.imag) ) else: parts.append('Q {0:G},{1:G} {2:G},{3:G}'.format( segment.control.real, segment.control.imag, segment.end.real, segment.end.imag) ) elif isinstance(segment, Arc): parts.append('A {0:G},{1:G} {2:G} {3:d},{4:d} {5:G},{6:G}'.format( segment.radius.real, segment.radius.imag, segment.rotation, int(segment.arc), int(segment.sweep), segment.end.real, segment.end.imag) ) current_pos = segment.end previous_segment = segment if self.closed: parts.append('Z') return ' '.join(parts) svg.path-3.0/src/svg/path/__init__.py0000664000175000017500000000013713334563713021200 0ustar lregebrolregebro00000000000000from .path import Path, Line, Arc, CubicBezier, QuadraticBezier from .parser import parse_path svg.path-3.0/src/svg.path.egg-info/0000775000175000017500000000000013334563713020557 5ustar lregebrolregebro00000000000000svg.path-3.0/src/svg.path.egg-info/requires.txt0000664000175000017500000000001313334563713023151 0ustar lregebrolregebro00000000000000setuptools svg.path-3.0/src/svg.path.egg-info/SOURCES.txt0000664000175000017500000000112213334563713022437 0ustar lregebrolregebro00000000000000CHANGES.txt CONTRIBUTORS.txt LICENSE.txt MANIFEST.in README.rst setup.cfg setup.py src/svg/__init__.py src/svg.path.egg-info/PKG-INFO src/svg.path.egg-info/SOURCES.txt src/svg.path.egg-info/dependency_links.txt src/svg.path.egg-info/namespace_packages.txt src/svg.path.egg-info/requires.txt src/svg.path.egg-info/top_level.txt src/svg.path.egg-info/zip-safe src/svg/path/__init__.py src/svg/path/parser.py src/svg/path/path.py src/svg/path/tests/__init__.py src/svg/path/tests/test_doc.py src/svg/path/tests/test_generation.py src/svg/path/tests/test_parsing.py src/svg/path/tests/test_paths.pysvg.path-3.0/src/svg.path.egg-info/zip-safe0000664000175000017500000000000113334563713022207 0ustar lregebrolregebro00000000000000 svg.path-3.0/src/svg.path.egg-info/dependency_links.txt0000664000175000017500000000000113334563713024625 0ustar lregebrolregebro00000000000000 svg.path-3.0/src/svg.path.egg-info/namespace_packages.txt0000664000175000017500000000000413334563713025104 0ustar lregebrolregebro00000000000000svg svg.path-3.0/src/svg.path.egg-info/PKG-INFO0000664000175000017500000002552113334563713021661 0ustar lregebrolregebro00000000000000Metadata-Version: 1.1 Name: svg.path Version: 3.0 Summary: SVG path objects and parser Home-page: https://github.com/regebro/svg.path Author: Lennart Regebro Author-email: regebro@gmail.com License: MIT Description-Content-Type: UNKNOWN Description: svg.path ======== svg.path is a collection of objects that implement the different path commands in SVG, and a parser for SVG path definitions. Usage ----- There are four path segment objects, ``Line``, ``Arc``, ``CubicBezier`` and ``QuadraticBezier``.`There is also a ``Path`` object that acts as a collection of the path segment objects. All coordinate values for these classes are given as ``complex`` values, where the ``.real`` part represents the X coordinate, and the ``.imag`` part representes the Y coordinate. >>> from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier All of these objects have a ``.point()`` function which will return the coordinates of a point on the path, where the point is given as a floating point value where ``0.0`` is the start of the path and ``1.0`` is end end. You can calculate the length of a Path or it's segments with the ``.length()`` function. For CubicBezier and Arc segments this is done by geometric approximation and for this reason **may be very slow**. You can make it faster by passing in an ``error`` option to the method. If you don't pass in error, it defaults to ``1e-12``. >>> CubicBezier(300+100j, 100+100j, 200+200j, 200+300j).length(error=1e-5) 297.2208145656899 CubicBezier and Arc also has a ``min_depth`` option that specifies the minimum recursion depth. This is set to 5 by default, resulting in using a minimum of 32 segments for the calculation. Setting it to 0 is a bad idea for CubicBeziers, as they may become approximated to a straight line. ``Line.length()`` and ``QuadraticBezier.length()`` also takes these parameters, but they are ignored. CubicBezier and QuadraticBezier also has ``is_smooth_from(previous)`` methods, that check if the segment is a "smooth" segment compared to the given segment. There is also a ``parse_path()`` function that will take an SVG path definition and return a ``Path`` object. >>> from svg.path import parse_path >>> parse_path('M 100 100 L 300 100') Path(Move(to=(100+100j)), Line(start=(100+100j), end=(300+100j)), closed=False) Classes ....... These are the SVG path segment classes. See the `SVG specifications `_ for more information on what each parameter means. * ``Line(start, end)`` * ``Arc(start, radius, rotation, arc, sweep, end)`` * ``QuadraticBezier(start, control, end)`` * ``CubicBezier(start, control1, control2, end)`` In addition to that, there is the ``Path`` class, which is instantiated with a sequence of path segments: * ``Path(*segments)`` The ``Path`` class is a mutable sequence, so it behaves like a list. You can add to it and replace path segments etc. >>> path = Path(Line(100+100j,300+100j), Line(100+100j,300+100j)) >>> path.append(QuadraticBezier(300+100j, 200+200j, 200+300j)) >>> path[0] = Line(200+100j,300+100j) >>> del path[1] The path object also has a ``d()`` method that will return the SVG representation of the Path segments. >>> path.d() 'M 200,100 L 300,100 Q 200,200 200,300' Examples ........ This SVG path example draws a triangle: >>> path1 = parse_path('M 100 100 L 300 100 L 200 300 z') You can format SVG paths in many different ways, all valid paths should be accepted: >>> path2 = parse_path('M100,100L300,100L200,300z') And these paths should be equal: >>> path1 == path2 True You can also build a path from objects: >>> path3 = Path(Line(100+100j,300+100j), Line(300+100j, 200+300j), Line(200+300j, 100+100j)) And it should again be equal to the first path: >>> path1 == path2 True Paths are mutable sequences, you can slice and append: >>> path1.append(QuadraticBezier(300+100j, 200+200j, 200+300j)) >>> len(path1[2:]) == 3 True Paths also have a ``closed`` property, which defines if the path should be seen as a closed path or not. >>> path = parse_path('M100,100L300,100L200,300z') >>> path.closed True If you modify the path in such a way that it is no longer closeable, it will not be closed. >>> path[0].start = (100+105j) >>> path[1].start = (100+105j) >>> path.closed False However, a path previously set as closed will automatically close if it it further modified to that it can be closed. >>> path[-1].end = (300+100j) >>> path.closed True Trying to set a Path to be closed if the end does not coincide with the start of any segment will raise an error. >>> path = parse_path('M100,100L300,100L200,300') >>> path.closed = True Traceback (most recent call last): ... ValueError: End does not coincide with a segment start. Future features --------------- * Reversing paths. They should then reasonably be drawn "backwards" meaning each path segment also needs to be reversed. * Mathematical transformations might make sense. Licence ------- This module is under a MIT License. Contributors ============ Lennart Regebro , Original Author Justin Gruenberg implemented the Quadradic Bezier calculations and provided suggestions and feedback about the d() function. Michiel Schallig suggested calculating length by recursive straight-line approximations, which enables you to choose between accuracy or speed. Steve Schwarz added an error argument to make that choice an argument. Thanks also to bug fixers Martin R, abcjjy, Daniel Stender and MTician. Changelog ========= 3.0 (2018-08-14) ---------------- - Dropped support for Python 3.1 and 3.2. It still works, but it may stop. Added support for Python 3.6. Dropped support for Jython, it's not supported by Travis, and hasn't seen a release in over a year. - #33: Move commands are now preserved when parsed. - Subpaths are no longer merged even if they are joined. - #30: Arcs where the endpoint is the same as the start point caused a crash. The SVG specs say that it instead should be the equavalent of skipping that section, which now is the case. 2.2 (2016-10-15) ---------------- - Don't add a line when closing a path if it's not needed. 2.1.1 (2016-02-28) ------------------ - #18: QuadraticBeziers could get a DivideByZero error under certain circumstances. [MTician] - Accept an error parameter to Path.point() to be able to control error vs performance setting. [saschwarz] - #25: Arc's could create a MathDomain error under certain circumstances. - #17: Set last_command always. 2.0.1 (2015-10-17) ------------------ - #20: The doctext for the closed() setter was incorrect. - #19: Fixed so tests didn't use relative paths. [danstender] 2.0 (2015-05-15) ---------------- - Nothing changed yet. 2.0b1 (2014-11-06) ------------------ - Added a Path.d() function to generate the Path's d attribute. - Added is_smooth_from() on QubicBezier and QuadradicBezier. - Path()'s now have a .closed property. - Fixed the representation so it's parseable. - The calculations for CubicBezier and Arc segments are now recursive, and will end when a specific accuracy has been achieved. This is somewhat faster for Arcs and somewhat slower for CubicBezier. However, you can now specify an accuracy, so if you want faster but looser calculations, you can have that. - 't' segments (smooth, relative QuadraticBeziers) whose previous segment was not a QuadraticBezier would get an incorrect control point. 1.2 (2014-11-01) ---------------- - New Quadradic Bezier implementation. [Justin Gruenberg] - Solved issue #6: Z close path behavior. [abcjjy] 1.1 (2013-10-19) ---------------- - Floats with negative exponents work again. - New tokenizer that is around 20 times faster. 1.0 (2013-05-28) ---------------- - Solved issue #2: Paths with negative values and no spaces didn't work. [regebro] 1.0b1 (2013-02-03) ------------------ - Original release. Keywords: svg path maths Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Multimedia :: Graphics svg.path-3.0/src/svg.path.egg-info/top_level.txt0000664000175000017500000000000413334563713023303 0ustar lregebrolregebro00000000000000svg svg.path-3.0/README.rst0000664000175000017500000001145413334563713016240 0ustar lregebrolregebro00000000000000svg.path ======== svg.path is a collection of objects that implement the different path commands in SVG, and a parser for SVG path definitions. Usage ----- There are four path segment objects, ``Line``, ``Arc``, ``CubicBezier`` and ``QuadraticBezier``.`There is also a ``Path`` object that acts as a collection of the path segment objects. All coordinate values for these classes are given as ``complex`` values, where the ``.real`` part represents the X coordinate, and the ``.imag`` part representes the Y coordinate. >>> from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier All of these objects have a ``.point()`` function which will return the coordinates of a point on the path, where the point is given as a floating point value where ``0.0`` is the start of the path and ``1.0`` is end end. You can calculate the length of a Path or it's segments with the ``.length()`` function. For CubicBezier and Arc segments this is done by geometric approximation and for this reason **may be very slow**. You can make it faster by passing in an ``error`` option to the method. If you don't pass in error, it defaults to ``1e-12``. >>> CubicBezier(300+100j, 100+100j, 200+200j, 200+300j).length(error=1e-5) 297.2208145656899 CubicBezier and Arc also has a ``min_depth`` option that specifies the minimum recursion depth. This is set to 5 by default, resulting in using a minimum of 32 segments for the calculation. Setting it to 0 is a bad idea for CubicBeziers, as they may become approximated to a straight line. ``Line.length()`` and ``QuadraticBezier.length()`` also takes these parameters, but they are ignored. CubicBezier and QuadraticBezier also has ``is_smooth_from(previous)`` methods, that check if the segment is a "smooth" segment compared to the given segment. There is also a ``parse_path()`` function that will take an SVG path definition and return a ``Path`` object. >>> from svg.path import parse_path >>> parse_path('M 100 100 L 300 100') Path(Move(to=(100+100j)), Line(start=(100+100j), end=(300+100j)), closed=False) Classes ....... These are the SVG path segment classes. See the `SVG specifications `_ for more information on what each parameter means. * ``Line(start, end)`` * ``Arc(start, radius, rotation, arc, sweep, end)`` * ``QuadraticBezier(start, control, end)`` * ``CubicBezier(start, control1, control2, end)`` In addition to that, there is the ``Path`` class, which is instantiated with a sequence of path segments: * ``Path(*segments)`` The ``Path`` class is a mutable sequence, so it behaves like a list. You can add to it and replace path segments etc. >>> path = Path(Line(100+100j,300+100j), Line(100+100j,300+100j)) >>> path.append(QuadraticBezier(300+100j, 200+200j, 200+300j)) >>> path[0] = Line(200+100j,300+100j) >>> del path[1] The path object also has a ``d()`` method that will return the SVG representation of the Path segments. >>> path.d() 'M 200,100 L 300,100 Q 200,200 200,300' Examples ........ This SVG path example draws a triangle: >>> path1 = parse_path('M 100 100 L 300 100 L 200 300 z') You can format SVG paths in many different ways, all valid paths should be accepted: >>> path2 = parse_path('M100,100L300,100L200,300z') And these paths should be equal: >>> path1 == path2 True You can also build a path from objects: >>> path3 = Path(Line(100+100j,300+100j), Line(300+100j, 200+300j), Line(200+300j, 100+100j)) And it should again be equal to the first path: >>> path1 == path2 True Paths are mutable sequences, you can slice and append: >>> path1.append(QuadraticBezier(300+100j, 200+200j, 200+300j)) >>> len(path1[2:]) == 3 True Paths also have a ``closed`` property, which defines if the path should be seen as a closed path or not. >>> path = parse_path('M100,100L300,100L200,300z') >>> path.closed True If you modify the path in such a way that it is no longer closeable, it will not be closed. >>> path[0].start = (100+105j) >>> path[1].start = (100+105j) >>> path.closed False However, a path previously set as closed will automatically close if it it further modified to that it can be closed. >>> path[-1].end = (300+100j) >>> path.closed True Trying to set a Path to be closed if the end does not coincide with the start of any segment will raise an error. >>> path = parse_path('M100,100L300,100L200,300') >>> path.closed = True Traceback (most recent call last): ... ValueError: End does not coincide with a segment start. Future features --------------- * Reversing paths. They should then reasonably be drawn "backwards" meaning each path segment also needs to be reversed. * Mathematical transformations might make sense. Licence ------- This module is under a MIT License. svg.path-3.0/CHANGES.txt0000664000175000017500000000455713334563713016370 0ustar lregebrolregebro00000000000000Changelog ========= 3.0 (2018-08-14) ---------------- - Dropped support for Python 3.1 and 3.2. It still works, but it may stop. Added support for Python 3.6. Dropped support for Jython, it's not supported by Travis, and hasn't seen a release in over a year. - #33: Move commands are now preserved when parsed. - Subpaths are no longer merged even if they are joined. - #30: Arcs where the endpoint is the same as the start point caused a crash. The SVG specs say that it instead should be the equavalent of skipping that section, which now is the case. 2.2 (2016-10-15) ---------------- - Don't add a line when closing a path if it's not needed. 2.1.1 (2016-02-28) ------------------ - #18: QuadraticBeziers could get a DivideByZero error under certain circumstances. [MTician] - Accept an error parameter to Path.point() to be able to control error vs performance setting. [saschwarz] - #25: Arc's could create a MathDomain error under certain circumstances. - #17: Set last_command always. 2.0.1 (2015-10-17) ------------------ - #20: The doctext for the closed() setter was incorrect. - #19: Fixed so tests didn't use relative paths. [danstender] 2.0 (2015-05-15) ---------------- - Nothing changed yet. 2.0b1 (2014-11-06) ------------------ - Added a Path.d() function to generate the Path's d attribute. - Added is_smooth_from() on QubicBezier and QuadradicBezier. - Path()'s now have a .closed property. - Fixed the representation so it's parseable. - The calculations for CubicBezier and Arc segments are now recursive, and will end when a specific accuracy has been achieved. This is somewhat faster for Arcs and somewhat slower for CubicBezier. However, you can now specify an accuracy, so if you want faster but looser calculations, you can have that. - 't' segments (smooth, relative QuadraticBeziers) whose previous segment was not a QuadraticBezier would get an incorrect control point. 1.2 (2014-11-01) ---------------- - New Quadradic Bezier implementation. [Justin Gruenberg] - Solved issue #6: Z close path behavior. [abcjjy] 1.1 (2013-10-19) ---------------- - Floats with negative exponents work again. - New tokenizer that is around 20 times faster. 1.0 (2013-05-28) ---------------- - Solved issue #2: Paths with negative values and no spaces didn't work. [regebro] 1.0b1 (2013-02-03) ------------------ - Original release. svg.path-3.0/CONTRIBUTORS.txt0000664000175000017500000000072413334563713017245 0ustar lregebrolregebro00000000000000Lennart Regebro , Original Author Justin Gruenberg implemented the Quadradic Bezier calculations and provided suggestions and feedback about the d() function. Michiel Schallig suggested calculating length by recursive straight-line approximations, which enables you to choose between accuracy or speed. Steve Schwarz added an error argument to make that choice an argument. Thanks also to bug fixers Martin R, abcjjy, Daniel Stender and MTician. svg.path-3.0/PKG-INFO0000664000175000017500000002552113334563713015646 0ustar lregebrolregebro00000000000000Metadata-Version: 1.1 Name: svg.path Version: 3.0 Summary: SVG path objects and parser Home-page: https://github.com/regebro/svg.path Author: Lennart Regebro Author-email: regebro@gmail.com License: MIT Description-Content-Type: UNKNOWN Description: svg.path ======== svg.path is a collection of objects that implement the different path commands in SVG, and a parser for SVG path definitions. Usage ----- There are four path segment objects, ``Line``, ``Arc``, ``CubicBezier`` and ``QuadraticBezier``.`There is also a ``Path`` object that acts as a collection of the path segment objects. All coordinate values for these classes are given as ``complex`` values, where the ``.real`` part represents the X coordinate, and the ``.imag`` part representes the Y coordinate. >>> from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier All of these objects have a ``.point()`` function which will return the coordinates of a point on the path, where the point is given as a floating point value where ``0.0`` is the start of the path and ``1.0`` is end end. You can calculate the length of a Path or it's segments with the ``.length()`` function. For CubicBezier and Arc segments this is done by geometric approximation and for this reason **may be very slow**. You can make it faster by passing in an ``error`` option to the method. If you don't pass in error, it defaults to ``1e-12``. >>> CubicBezier(300+100j, 100+100j, 200+200j, 200+300j).length(error=1e-5) 297.2208145656899 CubicBezier and Arc also has a ``min_depth`` option that specifies the minimum recursion depth. This is set to 5 by default, resulting in using a minimum of 32 segments for the calculation. Setting it to 0 is a bad idea for CubicBeziers, as they may become approximated to a straight line. ``Line.length()`` and ``QuadraticBezier.length()`` also takes these parameters, but they are ignored. CubicBezier and QuadraticBezier also has ``is_smooth_from(previous)`` methods, that check if the segment is a "smooth" segment compared to the given segment. There is also a ``parse_path()`` function that will take an SVG path definition and return a ``Path`` object. >>> from svg.path import parse_path >>> parse_path('M 100 100 L 300 100') Path(Move(to=(100+100j)), Line(start=(100+100j), end=(300+100j)), closed=False) Classes ....... These are the SVG path segment classes. See the `SVG specifications `_ for more information on what each parameter means. * ``Line(start, end)`` * ``Arc(start, radius, rotation, arc, sweep, end)`` * ``QuadraticBezier(start, control, end)`` * ``CubicBezier(start, control1, control2, end)`` In addition to that, there is the ``Path`` class, which is instantiated with a sequence of path segments: * ``Path(*segments)`` The ``Path`` class is a mutable sequence, so it behaves like a list. You can add to it and replace path segments etc. >>> path = Path(Line(100+100j,300+100j), Line(100+100j,300+100j)) >>> path.append(QuadraticBezier(300+100j, 200+200j, 200+300j)) >>> path[0] = Line(200+100j,300+100j) >>> del path[1] The path object also has a ``d()`` method that will return the SVG representation of the Path segments. >>> path.d() 'M 200,100 L 300,100 Q 200,200 200,300' Examples ........ This SVG path example draws a triangle: >>> path1 = parse_path('M 100 100 L 300 100 L 200 300 z') You can format SVG paths in many different ways, all valid paths should be accepted: >>> path2 = parse_path('M100,100L300,100L200,300z') And these paths should be equal: >>> path1 == path2 True You can also build a path from objects: >>> path3 = Path(Line(100+100j,300+100j), Line(300+100j, 200+300j), Line(200+300j, 100+100j)) And it should again be equal to the first path: >>> path1 == path2 True Paths are mutable sequences, you can slice and append: >>> path1.append(QuadraticBezier(300+100j, 200+200j, 200+300j)) >>> len(path1[2:]) == 3 True Paths also have a ``closed`` property, which defines if the path should be seen as a closed path or not. >>> path = parse_path('M100,100L300,100L200,300z') >>> path.closed True If you modify the path in such a way that it is no longer closeable, it will not be closed. >>> path[0].start = (100+105j) >>> path[1].start = (100+105j) >>> path.closed False However, a path previously set as closed will automatically close if it it further modified to that it can be closed. >>> path[-1].end = (300+100j) >>> path.closed True Trying to set a Path to be closed if the end does not coincide with the start of any segment will raise an error. >>> path = parse_path('M100,100L300,100L200,300') >>> path.closed = True Traceback (most recent call last): ... ValueError: End does not coincide with a segment start. Future features --------------- * Reversing paths. They should then reasonably be drawn "backwards" meaning each path segment also needs to be reversed. * Mathematical transformations might make sense. Licence ------- This module is under a MIT License. Contributors ============ Lennart Regebro , Original Author Justin Gruenberg implemented the Quadradic Bezier calculations and provided suggestions and feedback about the d() function. Michiel Schallig suggested calculating length by recursive straight-line approximations, which enables you to choose between accuracy or speed. Steve Schwarz added an error argument to make that choice an argument. Thanks also to bug fixers Martin R, abcjjy, Daniel Stender and MTician. Changelog ========= 3.0 (2018-08-14) ---------------- - Dropped support for Python 3.1 and 3.2. It still works, but it may stop. Added support for Python 3.6. Dropped support for Jython, it's not supported by Travis, and hasn't seen a release in over a year. - #33: Move commands are now preserved when parsed. - Subpaths are no longer merged even if they are joined. - #30: Arcs where the endpoint is the same as the start point caused a crash. The SVG specs say that it instead should be the equavalent of skipping that section, which now is the case. 2.2 (2016-10-15) ---------------- - Don't add a line when closing a path if it's not needed. 2.1.1 (2016-02-28) ------------------ - #18: QuadraticBeziers could get a DivideByZero error under certain circumstances. [MTician] - Accept an error parameter to Path.point() to be able to control error vs performance setting. [saschwarz] - #25: Arc's could create a MathDomain error under certain circumstances. - #17: Set last_command always. 2.0.1 (2015-10-17) ------------------ - #20: The doctext for the closed() setter was incorrect. - #19: Fixed so tests didn't use relative paths. [danstender] 2.0 (2015-05-15) ---------------- - Nothing changed yet. 2.0b1 (2014-11-06) ------------------ - Added a Path.d() function to generate the Path's d attribute. - Added is_smooth_from() on QubicBezier and QuadradicBezier. - Path()'s now have a .closed property. - Fixed the representation so it's parseable. - The calculations for CubicBezier and Arc segments are now recursive, and will end when a specific accuracy has been achieved. This is somewhat faster for Arcs and somewhat slower for CubicBezier. However, you can now specify an accuracy, so if you want faster but looser calculations, you can have that. - 't' segments (smooth, relative QuadraticBeziers) whose previous segment was not a QuadraticBezier would get an incorrect control point. 1.2 (2014-11-01) ---------------- - New Quadradic Bezier implementation. [Justin Gruenberg] - Solved issue #6: Z close path behavior. [abcjjy] 1.1 (2013-10-19) ---------------- - Floats with negative exponents work again. - New tokenizer that is around 20 times faster. 1.0 (2013-05-28) ---------------- - Solved issue #2: Paths with negative values and no spaces didn't work. [regebro] 1.0b1 (2013-02-03) ------------------ - Original release. Keywords: svg path maths Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Multimedia :: Graphics