diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index df45d76..955c671 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -4,205 +4,15 @@ Note: This file was taken (nearly) as is from the svg.path module (v 2.0).""" # External dependencies from __future__ import division, absolute_import, print_function -import re import numpy as np import warnings # Internal dependencies -from .path import Path, Line, QuadraticBezier, CubicBezier, Arc - -# To maintain forward/backward compatibility -try: - str = basestring -except NameError: - pass - -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 +from .path import Path def parse_path(pathdef, current_pos=0j, tree_element=None): - # 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() - - if tree_element is None: - segments = Path() - else: - segments = Path(tree_element=tree_element) - - 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))) - - 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 - - # 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 not (current_pos == start_pos): - segments.append(Line(current_pos, start_pos)) - segments.closed = True - current_pos = start_pos - command = None - - elif command == 'L': - x = elements.pop() - y = elements.pop() - pos = float(x) + float(y) * 1j - if not absolute: - pos += current_pos - segments.append(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(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(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(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(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(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(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(Arc(current_pos, radius, rotation, arc, sweep, end)) - current_pos = end - - return segments + return Path(pathdef, current_pos=current_pos, tree_element=tree_element) def _check_num_parsed_values(values, allowed): diff --git a/svgpathtools/path.py b/svgpathtools/path.py index eca12f5..e5102bb 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -6,6 +6,7 @@ Arc.""" from __future__ import division, absolute_import, print_function from math import sqrt, cos, sin, acos, asin, degrees, radians, log, pi, ceil from cmath import exp, sqrt as csqrt, phase +import re try: from collections.abc import MutableSequence # noqa except ImportError: @@ -26,6 +27,17 @@ from .bezier import (bezier_intersections, bezier_bounding_box, split_bezier, from .misctools import BugException from .polytools import rational_limit, polyroots, polyroots01, imag, real +# To maintain forward/backward compatibility +try: + str = basestring +except NameError: + pass + +COMMANDS = set('MmZzLlHhVvCcSsQqTtAa') +UPPERCASE = set('MZLHVCSQTA') + +COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])") +FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?") # Default Parameters ########################################################## @@ -2246,11 +2258,24 @@ class Path(MutableSequence): meta = None # meant as container for storage of arbitrary meta data def __init__(self, *segments, **kw): - self._segments = list(segments) self._length = None self._lengths = None if 'closed' in kw: self.closed = kw['closed'] # DEPRECATED + if len(segments) >= 1: + if isinstance(segments[0], str): + if len(segments) >= 2: + current_pos = segments[1] + elif 'current_pos' in kw: + current_pos = kw['current_pos'] + else: + current_pos = 0j + self._segments = list() + self._parse_path(segments[0], current_pos) + else: + self._segments = list(segments) + else: + self._segments = list() if self._segments: self._start = self._segments[0].start self._end = self._segments[-1].end @@ -2880,3 +2905,179 @@ class Path(MutableSequence): opt = complex(xmin-1, ymin-1) return path_encloses_pt(pt, opt, other) + + def _tokenize_path(self, 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(self, pathdef, current_pos=0j, tree_element=None): + # 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(self._tokenize_path(pathdef)) + # Reverse for easy use of .pop() + elements.reverse() + + segments = self._segments + + 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))) + + 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 + + # 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 not (current_pos == start_pos): + segments.append(Line(current_pos, start_pos)) + self.closed = True + current_pos = start_pos + command = None + + elif command == 'L': + x = elements.pop() + y = elements.pop() + pos = float(x) + float(y) * 1j + if not absolute: + pos += current_pos + segments.append(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(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(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(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(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(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(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(Arc(current_pos, radius, rotation, arc, sweep, end)) + current_pos = end + + return segments diff --git a/test/test_parsing.py b/test/test_parsing.py index 84648e8..7897faa 100644 --- a/test/test_parsing.py +++ b/test/test_parsing.py @@ -247,3 +247,33 @@ class TestParser(unittest.TestCase): skewX(40) scale(10 0.5)""") )) + + def test_pathd_init(self): + path0 = Path('') + path1 = parse_path("M 100 100 L 300 100 L 200 300 z") + path2 = Path("M 100 100 L 300 100 L 200 300 z") + self.assertEqual(path1, path2) + + path1 = parse_path("m 100 100 L 300 100 L 200 300 z", current_pos=50+50j) + path2 = Path("m 100 100 L 300 100 L 200 300 z") + self.assertNotEqual(path1, path2) + + path1 = parse_path("m 100 100 L 300 100 L 200 300 z") + path2 = Path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j) + self.assertNotEqual(path1, path2) + + path1 = parse_path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j) + path2 = Path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j) + self.assertEqual(path1, path2) + + path1 = parse_path("m 100 100 L 300 100 L 200 300 z", 50+50j) + path2 = Path("m 100 100 L 300 100 L 200 300 z") + self.assertNotEqual(path1, path2) + + path1 = parse_path("m 100 100 L 300 100 L 200 300 z") + path2 = Path("m 100 100 L 300 100 L 200 300 z", 50 + 50j) + self.assertNotEqual(path1, path2) + + path1 = parse_path("m 100 100 L 300 100 L 200 300 z", 50 + 50j) + path2 = Path("m 100 100 L 300 100 L 200 300 z", 50 + 50j) + self.assertEqual(path1, path2)