diff --git a/setup.cfg b/setup.cfg index 440299b6..f55eb7a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = svgelements -version = 1.1.4 +version = 1.2.0 description = Svg Elements Parsing long_description_content_type=text/markdown long_description = file: README.md diff --git a/svgelements/svgelements.py b/svgelements/svgelements.py index 97e07e63..43b81e10 100644 --- a/svgelements/svgelements.py +++ b/svgelements/svgelements.py @@ -119,6 +119,7 @@ PATTERN_WS = r'[\s\t\n]*' PATTERN_COMMA = r'(?:\s*,\s*|\s+|(?=-))' +PATTERN_COMMAWSP = r'[ ,\t\n\x09\x0A\x0C\x0D]+' PATTERN_FLOAT = '[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?' PATTERN_LENGTH_UNITS = 'cm|mm|Q|in|pt|pc|px|em|cx|ch|rem|vw|vh|vmin|vmax' PATTERN_ANGLE_UNITS = 'deg|grad|rad|turn' @@ -160,190 +161,274 @@ REGEX_CSS_FONT = re.compile( r'(?:(normal|italic|oblique)\s|(normal|small-caps)\s|(normal|bold|bolder|lighter|\d{3})\s|(normal|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)\s)*\s*(xx-small|x-small|small|medium|large|x-large|xx-large|larger|smaller|\d+(?:em|pt|pc|px|%))(?:/(xx-small|x-small|small|medium|large|x-large|xx-large|larger|smaller|\d+(?:em|pt|pc|px|%)))?\s*(.*),?\s+(serif|sans-serif|cursive|fantasy|monospace);?') - -# PathTokens class. -class PathTokens: - """Path Tokens is the class for the general outline of how SVG Pathd objects - are stored. Namely, a single non-'e' character and a collection of floating - point numbers. While this is explicitly used for SVG pathd objects the method - for serializing command data in this fashion is also useful as a standalone - class.""" - - def __init__(self, command_elements): - self.command_elements = command_elements - commands = '' - for k in command_elements: - commands += k - self.COMMAND_RE = re.compile("([%s])" % (commands)) - self.elements = None - self.command = None - self.last_command = None +svg_parse = [ + ('COMMAND', r'[MmZzLlHhVvCcSsQqTtAa]'), + ('SKIP', PATTERN_COMMAWSP) +] +svg_re = re.compile('|'.join('(?P<%s>%s)' % pair for pair in svg_parse)) +num_parse = [ + ('FLOAT', PATTERN_FLOAT), + ('CLOSE', r'[Zz]'), + ('SKIP', PATTERN_COMMAWSP) +] +num_re = re.compile('|'.join('(?P<%s>%s)' % pair for pair in num_parse)) +flag_parse = [ + ('FLAG', r'[01]'), + ('SKIP', PATTERN_COMMAWSP) +] +flag_re = re.compile('|'.join('(?P<%s>%s)' % pair for pair in flag_parse)) + + +class SVGLexicalParser: + def __init__(self): self.parser = None + self.pathd = None + self.pos = 0 + self.limit = 0 + self.inline_close = None + + def _command(self): + while self.pos < self.limit: + match = svg_re.match(self.pathd, self.pos) + if match is None: + return None # Did not match at command sequence. + self.pos = match.end() + kind = match.lastgroup + if kind == 'SKIP': + continue + return match.group() + return None - def _tokenize_path(self, pathdef): - for x in self.COMMAND_RE.split(pathdef): - if x in self.command_elements: - yield x - for token in REGEX_FLOAT.findall(x): - yield token - - def get(self): - """Gets the element from the stack.""" - return self.elements.pop() - - def pre_execute(self): - """Called before any command element is executed.""" - pass - - def post_execute(self): - """Called after any command element is executed.""" - pass - - def new_command(self): - """Called when command element is switched.""" - pass + def _more(self): + while self.pos < self.limit: + match = num_re.match(self.pathd, self.pos) + if match is None: + return False + kind = match.lastgroup + if kind == 'CLOSE': + self.inline_close = match.group() + return False + if kind == 'SKIP': + # move skipped elements forward. + self.pos = match.end() + continue + return True + return None - def parse(self, pathdef): - self.elements = list(self._tokenize_path(pathdef)) - # Reverse for easy use of .pop() - self.elements.reverse() - - while self.elements: - if self.elements[-1] in self.command_elements: - self.last_command = self.command - self.command = self.get() - self.new_command() - else: - if self.command is None: - raise ValueError("Invalid command.") # could be faulty implicit or unaccepted element. - self.pre_execute() - self.command_elements[self.command]() - self.post_execute() + def _number(self): + while self.pos < self.limit: + match = num_re.match(self.pathd, self.pos) + if match is None: + break # No more matches. + kind = match.lastgroup + if kind == 'CLOSE': + # Inline Close + self.inline_close = match.group() + return None + self.pos = match.end() + if kind == 'SKIP': + continue + return float(match.group()) + return None + def _flag(self): + while self.pos < self.limit: + match = flag_re.match(self.pathd, self.pos) + if match is None: + break # No more matches. + self.pos = match.end() + kind = match.lastgroup + if kind == 'SKIP': + continue + return bool(int(match.group())) + return None -# SVG Path Tokens. -class SVGPathTokens(PathTokens): - """Utilizes the general PathTokens class to parse SVG pathd strings. - This class has been updated to account for SVG 2.0 version of the zZ command.""" + def _coord(self): + x = self._number() + if x is None: + return None + y = self._number() + if y is None: + raise ValueError + return x, y - def __init__(self): - PathTokens.__init__(self, { - 'M': self.move_to, - 'm': self.move_to, - 'L': self.line_to, - 'l': self.line_to, - "H": self.h_to, - "h": self.h_to, - "V": self.v_to, - "v": self.v_to, - "C": self.cubic_to, - "c": self.cubic_to, - "S": self.smooth_cubic_to, - "s": self.smooth_cubic_to, - "Q": self.quad_to, - "q": self.quad_to, - "T": self.smooth_quad_to, - "t": self.smooth_quad_to, - "A": self.arc_to, - "a": self.arc_to, - "Z": self.close, - "z": self.close - }) - self.parser = None - self.absolute = False + def _rcoord(self): + position = self._coord() + if position is None: + return None + current_pos = self.parser.current_point + if current_pos is None: + return position + return position[0] + current_pos[0], position[1] + current_pos[1] - def svg_parse(self, parser, pathdef): + def parse(self, parser, pathd): self.parser = parser - self.absolute = False self.parser.start() - self.parse(pathdef) + self.pathd = pathd + self.pos = 0 + self.limit = len(pathd) + while True: + cmd = self._command() + if cmd is None: + return + elif cmd == 'z' or cmd == 'Z': + if self._more(): + raise ValueError + self.parser.closed(relative=cmd.islower()) + self.inline_close = None + continue + elif cmd == 'm': + if not self._more(): + raise ValueError + coord = self._rcoord() + self.parser.move(coord, relative=True) + while self._more(): + coord = self._rcoord() + self.parser.line(coord, relative=True) + elif cmd == 'M': + if not self._more(): + raise ValueError + coord = self._coord() + self.parser.move(coord, relative=False) + while self._more(): + coord = self._coord() + self.parser.line(coord, relative=False) + elif cmd == 'l': + while True: + coord = self._rcoord() + if coord is None: + coord = self.inline_close + self.parser.line(coord, relative=True) + if not self._more(): + break + elif cmd == 'L': + while True: + coord = self._coord() + if coord is None: + coord = self.inline_close + self.parser.line(coord, relative=False) + if not self._more(): + break + elif cmd == 't': + while True: + coord = self._rcoord() + if coord is None: + coord = self.inline_close + self.parser.smooth_quad(coord, relative=True) + if not self._more(): + break + elif cmd == 'T': + while True: + coord = self._coord() + if coord is None: + coord = self.inline_close + self.parser.smooth_quad(coord, relative=False) + if not self._more(): + break + elif cmd == 'h': + while True: + value = self._number() + self.parser.horizontal(value, relative=True) + if not self._more(): + break + elif cmd == 'H': + while True: + value = self._number() + self.parser.horizontal(value, relative=False) + if not self._more(): + break + elif cmd == 'v': + while True: + value = self._number() + self.parser.vertical(value, relative=True) + if not self._more(): + break + elif cmd == 'V': + while self._more(): + value = self._number() + self.parser.vertical(value, relative=False) + elif cmd == 'c': + while True: + coord1, coord2, coord3 = self._rcoord(), self._rcoord(), self._rcoord() + if coord1 is None: + coord1 = self.inline_close + if coord2 is None: + coord2 = self.inline_close + if coord3 is None: + coord3 = self.inline_close + self.parser.cubic(coord1, coord2, coord3, relative=True) + if not self._more(): + break + elif cmd == 'C': + while True: + coord1, coord2, coord3 = self._coord(), self._coord(), self._coord() + if coord1 is None: + coord1 = self.inline_close + if coord2 is None: + coord2 = self.inline_close + if coord3 is None: + coord3 = self.inline_close + self.parser.cubic(coord1, coord2, coord3, relative=False) + if not self._more(): + break + elif cmd == 'q': + while True: + coord1, coord2 = self._rcoord(), self._rcoord() + if coord1 is None: + coord1 = self.inline_close + if coord2 is None: + coord2 = self.inline_close + self.parser.quad(coord1, coord2, relative=True) + if not self._more(): + break + elif cmd == 'Q': + while True: + coord1, coord2 = self._coord(), self._coord() + if coord1 is None: + coord1 = self.inline_close + if coord2 is None: + coord2 = self.inline_close + self.parser.quad(coord1, coord2, relative=False) + if not self._more(): + break + elif cmd == 's': + while True: + coord1, coord2 = self._rcoord(), self._rcoord() + if coord1 is None: + coord1 = self.inline_close + if coord2 is None: + coord2 = self.inline_close + self.parser.smooth_cubic(coord1, coord2, relative=True) + if not self._more(): + break + elif cmd == 'S': + while True: + coord1, coord2 = self._coord(), self._coord() + if coord1 is None: + coord1 = self.inline_close + if coord2 is None: + coord2 = self.inline_close + self.parser.smooth_cubic(coord1, coord2, relative=False) + if not self._more(): + break + elif cmd == 'a': + while self._more(): + rx, ry, rotation, arc, sweep, coord = \ + self._number(), self._number(), self._number(), self._flag(), self._flag(), self._rcoord() + if sweep is None: + raise ValueError + if coord is None: + coord = self.inline_close + self.parser.arc(rx, ry, rotation, arc, sweep, coord, relative=True) + elif cmd == 'A': + while self._more(): + rx, ry, rotation, arc, sweep, coord = \ + self._number(), self._number(), self._number(), self._flag(), self._flag(), self._coord() + if coord is None: + coord = self.inline_close + self.parser.arc(rx, ry, rotation, arc, sweep, coord, relative=False) self.parser.end() - def get_pos(self): - if self.command == 'Z': - return "z" # After Z, all further expected values are also Z. - coord0 = self.get() - if coord0 == 'z' or coord0 == 'Z': - self.command = 'Z' - return "z" - coord1 = self.get() - position = (float(coord0), float(coord1)) - if not self.absolute: - current_pos = self.parser.current_point - if current_pos is None: - return position - return [position[0] + current_pos[0], position[1] + current_pos[1]] - return position - - def move_to(self): - # Moveto command. - pos = self.get_pos() - self.parser.move(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. - self.command = 'L' - - def line_to(self): - pos = self.get_pos() - self.parser.line(pos) - - def h_to(self): - x = float(self.get()) - if self.absolute: - self.parser.absolute_h(x) - else: - self.parser.relative_h(x) - - def v_to(self): - y = float(self.get()) - if self.absolute: - self.parser.absolute_v(y) - else: - self.parser.relative_v(y) - - def cubic_to(self): - control1 = self.get_pos() - control2 = self.get_pos() - end = self.get_pos() - self.parser.cubic(control1, control2, end) - - def smooth_cubic_to(self): - control2 = self.get_pos() - end = self.get_pos() - self.parser.smooth_cubic(control2, end) - - def quad_to(self): - control = self.get_pos() - end = self.get_pos() - self.parser.quad(control, end) - - def smooth_quad_to(self): - end = self.get_pos() - self.parser.smooth_quad(end) - - def arc_to(self): - rx = float(self.get()) - ry = float(self.get()) - rotation = float(self.get()) - arc = float(self.get()) - sweep = float(self.get()) - end = self.get_pos() - - self.parser.arc(rx, ry, rotation, arc, sweep, end) - - def close(self): - # Close path - self.parser.closed() - self.command = None - - def new_command(self): - self.absolute = self.command.isupper() - - def post_execute(self): - pass - class Length(object): """ @@ -2064,7 +2149,7 @@ def __copy__(self): def __str__(self): """ - Many of SVG's graphics operations utilize 2x3 matrices of the form: + Many of SVG's graphics operations utilize 2x3: :returns string representation of matrix. """ @@ -2883,7 +2968,15 @@ class PathSegment: svg.elements but they are not written. """ - def __init__(self): + def __init__(self, **kwargs): + try: + self.relative = bool(kwargs['relative']) + except (KeyError, ValueError): + self.relative = False + try: + self.smooth = bool(kwargs['smooth']) + except (KeyError, ValueError): + self.smooth = True self.start = None self.end = None @@ -2908,9 +3001,17 @@ def __iadd__(self, other): __add__ = __iadd__ def __str__(self): + """ + This defines an individual path segment string. Since this isn't part of a Path it appends a pseudo-Move + command to correctly provide the starting position. + :return: string representation of the object. + """ d = self.d() if self.start is not None: - return 'M %s %s' % (self.start, d) + if self.relative: + return 'm %s %s' % (self.start, d) + else: + return 'M %s %s' % (self.start, d) return d def __iter__(self): @@ -2971,26 +3072,44 @@ def bbox(self): return xmin, ymin, xmax, ymax def reverse(self): + """ + Reverses the current path segment. + """ end = self.end self.end = self.start self.start = end def point(self, position): + """ + Returns the point at a given amount through the path segment. + :param position: t value between 0 and 1 + :return: + """ return self.end def length(self, error=ERROR, min_depth=MIN_DEPTH): + """ + Returns the length of this path segment. + + :param error: + :param min_depth: + :return: + """ return 0 - def d(self, current_point=None, smooth=False): - """If current point is None, the function will return the absolute form. If it contains a point, - it will give the value relative to that point.""" + def d(self, current_point=None, relative=None, smooth=None): + """Returns the fragment path_d value for the current path segment. + + For a relative segment the current_point must be provided. If it is omitted then only an absolute segment + can be returned.""" raise NotImplementedError class Move(PathSegment): - """Represents move commands. Does nothing, but is there to handle - paths that consist of only move commands, which is valid, but pointless. - Also serve as a bridge to make discontinuous paths into continuous paths + """Represents move commands. Moves to a new location without any path distance. + Paths that consist of only move commands, are valid. + + Move serve to make discontinuous paths into continuous linked paths segments with non-drawn sections. """ @@ -2999,13 +3118,16 @@ def __init__(self, *args, **kwargs): Move commands most importantly go to a place. So if one location is given, that's the end point. If two locations are given then first is the start location. + For many Move commands it is not necessary to have an original start location. The start point provides a + linked locations for some elements that may require it. If known it can be provided. + Move(p) where p is the End point. Move(s,e) where s is the Start point, e is the End point. Move(p, start=s) where p is End point, s is the Start point. Move(p, end=e) where p is the Start point, e is the End point. Move(start=s, end=e) where s is the Start point, e is the End point. """ - PathSegment.__init__(self) + PathSegment.__init__(self, **kwargs) self.end = None self.start = None if len(args) == 0: @@ -3048,7 +3170,7 @@ def __repr__(self): return 'Move(start=%s, end=%s)' % (repr(self.start), repr(self.end)) def __copy__(self): - return Move(self.start, self.end) + return Move(self.start, self.end, relative=self.relative) def __eq__(self, other): if not isinstance(other, Move): @@ -3071,11 +3193,10 @@ def __getitem__(self, item): else: raise IndexError - def d(self, current_point=None, smooth=False): - if current_point is None: - return 'M %s' % (self.end) - else: - return 'm %s' % (self.end - current_point) + def d(self, current_point=None, relative=None, smooth=None): + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): + return 'M %s' % self.end + return 'm %s' % (self.end - current_point) class Close(PathSegment): @@ -3084,8 +3205,8 @@ class Close(PathSegment): which can close or not close several times. """ - def __init__(self, start=None, end=None): - PathSegment.__init__(self) + def __init__(self, start=None, end=None, **kwargs): + PathSegment.__init__(self, **kwargs) self.end = None self.start = None if start is not None: @@ -3115,7 +3236,7 @@ def __repr__(self): return 'Close(start=%s, end=%s)' % (s, e) def __copy__(self): - return Close(self.start, self.end) + return Close(self.start, self.end, relative=self.relative) def __eq__(self, other): if not isinstance(other, Close): @@ -3147,8 +3268,8 @@ def length(self, error=None, min_depth=None): else: return 0 - def d(self, current_point=None, smooth=False): - if current_point is None: + def d(self, current_point=None, relative=None, smooth=None): + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): return 'Z' else: return 'z' @@ -3157,8 +3278,8 @@ def d(self, current_point=None, smooth=False): class Line(PathSegment): """Represents line commands.""" - def __init__(self, start, end): - PathSegment.__init__(self) + def __init__(self, start, end, **kwargs): + PathSegment.__init__(self, **kwargs) self.end = None self.start = None if start is not None: @@ -3172,7 +3293,7 @@ def __repr__(self): return 'Line(start=%s, end=%s)' % (repr(self.start), repr(self.end)) def __copy__(self): - return Line(self.start, self.end) + return Line(self.start, self.end, relative=self.relative) def __eq__(self, other): if not isinstance(other, Line): @@ -3231,9 +3352,9 @@ def closest_segment_point(self, p, respect_bounds=True): amount = 0 return self.point(amount) - def d(self, current_point=None, smooth=False): - if current_point is None: - return 'L %s' % (self.end) + def d(self, current_point=None, relative=None, smooth=None): + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): + return 'L %s' % self.end else: return 'l %s' % (self.end - current_point) @@ -3241,8 +3362,8 @@ def d(self, current_point=None, smooth=False): class QuadraticBezier(PathSegment): """Represents Quadratic Bezier commands.""" - def __init__(self, start, control, end): - PathSegment.__init__(self) + def __init__(self, start, control, end, **kwargs): + PathSegment.__init__(self, **kwargs) self.end = None self.control = None self.start = None @@ -3258,7 +3379,7 @@ def __repr__(self): repr(self.start), repr(self.control), repr(self.end)) def __copy__(self): - return QuadraticBezier(self.start, self.control, self.end) + return QuadraticBezier(self.start, self.control, self.end, relative=self.relative, smooth=self.smooth) def __eq__(self, other): if not isinstance(other, QuadraticBezier): @@ -3369,14 +3490,14 @@ def is_smooth_from(self, previous): else: return self.control == self.start - def d(self, current_point=None, smooth=False): - if smooth: - if current_point is None: - return 'T %s' % (self.end) + def d(self, current_point=None, relative=None, smooth=None): + if (smooth is None and self.smooth) or (smooth is not None and smooth): + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): + return 'T %s' % self.end else: return 't %s' % (self.end - current_point) else: - if current_point is None: + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): return 'Q %s %s' % (self.control, self.end) else: return 'q %s %s' % (self.control - current_point, self.end - current_point) @@ -3385,8 +3506,8 @@ def d(self, current_point=None, smooth=False): class CubicBezier(PathSegment): """Represents Cubic Bezier commands.""" - def __init__(self, start, control1, control2, end): - PathSegment.__init__(self) + def __init__(self, start, control1, control2, end, **kwargs): + PathSegment.__init__(self, **kwargs) self.end = None self.control1 = None self.control2 = None @@ -3405,7 +3526,8 @@ def __repr__(self): repr(self.start), repr(self.control1), repr(self.control2), repr(self.end)) def __copy__(self): - return CubicBezier(self.start, self.control1, self.control2, self.end) + return CubicBezier(self.start, self.control1, self.control2, self.end, relative=self.relative, + smooth=self.smooth) def __eq__(self, other): if not isinstance(other, CubicBezier): @@ -3487,7 +3609,7 @@ def _real_minmax(self, v): a = [c[v] for c in self] denom = a[0] - 3 * a[1] + 3 * a[2] - a[3] if abs(denom) >= 1e-12: - delta = a[1] ** 2 -\ + delta = a[1] ** 2 - \ (a[0] + a[1]) * a[2] + \ a[2] ** 2 + \ (a[0] - a[1]) * a[3] @@ -3517,14 +3639,14 @@ def is_smooth_from(self, previous): else: return self.control1 == self.start - def d(self, current_point=None, smooth=False): - if smooth: - if current_point is None: + def d(self, current_point=None, relative=None, smooth=None): + if (smooth is None and self.smooth) or (smooth is not None and smooth): + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): return 'S %s %s' % (self.control2, self.end) else: return 's %s %s' % (self.control2 - current_point, self.end - current_point) else: - if current_point is None: + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): return 'C %s %s %s' % (self.control1, self.control2, self.end) else: return 'c %s %s %s' % ( @@ -3546,7 +3668,7 @@ def __init__(self, *args, **kwargs): If points are modified by an affine transformation, the arc is transformed. There is a special case for when the scale factor inverts, it inverts the sweep. - Note: t-values are not angles from center in ellipical arcs. These are the same thing in + Note: t-values are not angles from center in elliptical arcs. These are the same thing in circular arcs. But, here t is a parameterization around the ellipse, as if it were a circle. The position on the arc is (a * cos(t), b * sin(t)). If r-major was 0 for example. The positions would all fall on the x-axis. And the angle from center would all be either 0 or @@ -3564,11 +3686,11 @@ def __init__(self, *args, **kwargs): The sweep angle can be a value greater than tau and less than -tau. However if this is the case, conversion back to Path.d() is expected to fail. We can denote these arc events but not as a single command. - should equal sweep or mod thereof. + start_t + sweep = end_t """ - PathSegment.__init__(self) + PathSegment.__init__(self, **kwargs) self.start = None self.end = None self.center = None @@ -3771,7 +3893,7 @@ def __repr__(self): repr(self.start), repr(self.end), repr(self.center), repr(self.prx), repr(self.pry), self.sweep) def __copy__(self): - return Arc(self.start, self.end, self.center, self.prx, self.pry, self.sweep) + return Arc(self.start, self.end, self.center, self.prx, self.pry, self.sweep, relative=self.relative) def __eq__(self, other): if not isinstance(other, Arc): @@ -4226,8 +4348,8 @@ def angle_inv(ang, k): # inverse of angle from Arc.derivative() return min(xtrema), min(ytrema), max(xtrema), max(ytrema) - def d(self, current_point=None, smooth=False): - if current_point is None: + def d(self, current_point=None, relative=None, smooth=None): + if current_point is None or (relative is None and self.relative) or (relative is not None and not relative): return 'A %G,%G %G %d,%d %s' % ( self.rx, self.ry, @@ -4452,8 +4574,8 @@ def __ne__(self, other): def parse(self, pathdef): """Parses the SVG path.""" - tokens = SVGPathTokens() - tokens.svg_parse(self, pathdef) + tokens = SVGLexicalParser() + tokens.parse(self, pathdef) def validate_connections(self): """ @@ -4544,123 +4666,127 @@ def start(self): def end(self): pass - def move(self, *points): - end_pos = points[0] + def move(self, *points, relative=False): start_pos = self.current_point - self.append(Move(start_pos, end_pos)) + end_pos = points[0] + if end_pos in ('z', 'Z'): + end_pos = self.z_point + segment = Move(start_pos, end_pos) + segment.relative = relative + self.append(segment) if len(points) > 1: - self.line(*points[1:]) + self.line(*points[1:], relative=relative) + return self - def line(self, *points): + def line(self, *points, relative=False): start_pos = self.current_point end_pos = points[0] - if end_pos == 'z': - self.append(Line(start_pos, self.z_point)) - self.closed() - return - self.append(Line(start_pos, end_pos)) + if end_pos in ('z', 'Z'): + end_pos = self.z_point + segment = Line(start_pos, end_pos) + segment.relative = relative + self.append(segment) if len(points) > 1: self.line(*points[1:]) + return self - def absolute_v(self, *y_points): - y_pos = y_points[0] + def vertical(self, *y_points, relative=False): start_pos = self.current_point - self.append(Line(start_pos, Point(start_pos[0], y_pos))) + if relative: + segment = Line(start_pos, Point(start_pos[0], start_pos[1] + y_points[0])) + else: + segment = Line(start_pos, Point(start_pos[0], y_points[0])) + segment.relative = relative + self.append(segment) if len(y_points) > 1: - self.absolute_v(*y_points[1:]) - - def relative_v(self, *dys): - dy = dys[0] - start_pos = self.current_point - self.append(Line(start_pos, Point(start_pos[0], start_pos[1] + dy))) - if len(dys) > 1: - self.relative_v(*dys[1:]) + self.vertical(*y_points[1:], relative=relative) + return self - def absolute_h(self, *x_points): - x_pos = x_points[0] + def horizontal(self, *x_points, relative=False): start_pos = self.current_point - self.append(Line(start_pos, Point(x_pos, start_pos[1]))) + if relative: + segment = Line(start_pos, Point(start_pos[0] + x_points[0], start_pos[1])) + segment.relative = relative + else: + segment = Line(start_pos, Point(x_points[0], start_pos[1])) + segment.relative = relative + self.append(segment) if len(x_points) > 1: - self.absolute_h(*x_points[1:]) - - def relative_h(self, *dxs): - dx = dxs[0] - start_pos = self.current_point - self.append(Line(start_pos, Point(start_pos[0] + dx, start_pos[1]))) - if len(dxs) > 1: - self.relative_h(*dxs[1:]) + self.horizontal(*x_points[1:], relative=relative) + return self - def smooth_quad(self, *points): + def smooth_quad(self, *points, relative=False): """Smooth curve. First control point is the "reflection" of the second control point in the previous path.""" start_pos = self.current_point control1 = self.smooth_point end_pos = points[0] - if end_pos == 'z': - self.append(QuadraticBezier(start_pos, control1, self.z_point)) - self.closed() - return - self.append(QuadraticBezier(start_pos, control1, end_pos)) + if end_pos in ('z', 'Z'): + end_pos = self.z_point + segment = QuadraticBezier(start_pos, control1, end_pos) + segment.relative = relative + segment.smooth = True + self.append(segment) if len(points) > 1: self.smooth_quad(*points[1:]) + return self - def quad(self, *points): + def quad(self, *points, relative=False): start_pos = self.current_point control = points[0] - if control == 'z': - self.append(QuadraticBezier(start_pos, self.z_point, self.z_point)) - self.closed() - return + if control in ('z', 'Z'): + control = self.z_point end_pos = points[1] - if end_pos == 'z': - self.append(QuadraticBezier(start_pos, control, self.z_point)) - self.closed() - return - self.append(QuadraticBezier(start_pos, control, end_pos)) + if end_pos in ('z', 'Z'): + end_pos = self.z_point + segment = QuadraticBezier(start_pos, control, end_pos) + segment.relative = relative + segment.smooth = False + self.append(segment) if len(points) > 2: self.quad(*points[2:]) + return self - def smooth_cubic(self, *points): + def smooth_cubic(self, *points, relative=False): """Smooth curve. First control point is the "reflection" of the second control point in the previous path.""" start_pos = self.current_point control1 = self.smooth_point control2 = points[0] - if control2 == 'z': - self.append(CubicBezier(start_pos, control1, self.z_point, self.z_point)) - self.closed() - return + + if control2 in ('z', 'Z'): + control2 = self.z_point end_pos = points[1] - if end_pos == 'z': - self.append(CubicBezier(start_pos, control1, control2, self.z_point)) - self.closed() - return - self.append(CubicBezier(start_pos, control1, control2, end_pos)) + if end_pos in ('z', 'Z'): + end_pos = self.z_point + segment = CubicBezier(start_pos, control1, control2, end_pos) + segment.relative = relative + segment.smooth = True + self.append(segment) if len(points) > 2: self.smooth_cubic(*points[2:]) + return self - def cubic(self, *points): + def cubic(self, *points, relative=False): start_pos = self.current_point control1 = points[0] - if control1 == 'z': - self.append(CubicBezier(start_pos, self.z_point, self.z_point, self.z_point)) - self.closed() - return + if control1 in ('z', 'Z'): + control1 = self.z_point control2 = points[1] - if control2 == 'z': - self.append(CubicBezier(start_pos, control1, self.z_point, self.z_point)) - self.closed() - return + if control2 in ('z', 'Z'): + control2 = self.z_point end_pos = points[2] - if end_pos == 'z': - self.append(CubicBezier(start_pos, control1, control2, self.z_point)) - self.closed() - return - self.append(CubicBezier(start_pos, control1, control2, end_pos)) + if end_pos in ('z', 'Z'): + end_pos = self.z_point + segment = CubicBezier(start_pos, control1, control2, end_pos) + segment.relative = relative + segment.smooth = False + self.append(segment) if len(points) > 3: self.cubic(*points[3:]) + return self - def arc(self, *arc_args): + def arc(self, *arc_args, relative=False): start_pos = self.current_point rx = arc_args[0] ry = arc_args[1] @@ -4668,18 +4794,22 @@ def arc(self, *arc_args): arc = arc_args[3] sweep = arc_args[4] end_pos = arc_args[5] - if end_pos == 'z': - self.append(Arc(start_pos, rx, ry, rotation, arc, sweep, self.z_point)) - self.closed() - return - self.append(Arc(start_pos, rx, ry, rotation, arc, sweep, end_pos)) + if end_pos in ('z', 'Z'): + end_pos = self.z_point + segment = Arc(start_pos, rx, ry, rotation, arc, sweep, end_pos) + segment.relative = relative + self.append(segment) if len(arc_args) > 6: self.arc(*arc_args[6:]) + return self - def closed(self): + def closed(self, relative=False): start_pos = self.current_point end_pos = self.z_point - self.append(Close(start_pos, end_pos)) + segment = Close(start_pos, end_pos) + segment.relative = relative + self.append(segment) + return self def _calc_lengths(self, error=ERROR, min_depth=MIN_DEPTH): if self._length is not None: @@ -4762,6 +4892,30 @@ def extend(self, iterable): self._validate_connection(index) self._validate_subpath(index) + def direct_close(self): + """Forces close operations to be zero length by introducing a direct + line to operation just before any non-zero length close. + + This is helpful because for some operations like reverse() because the + close must located at the very end of the path sequence. But, if it's + in effect a line-to and close, the line-to would need to start the sequence. + + But, for some operations this won't matter since it will still result in + a closed shape with reversed ordering. But, if the final point in the + sequence must exactly switch with the first point in the sequence. The + close segments must be direct and zero length. + """ + if len(self._segments) == 0: + return + for i in range(len(self._segments) - 1, -1, -1): + segment = self._segments[i] + if isinstance(segment, Close): + if segment.length() != 0: + line = Line(segment.start, segment.end) + segment.start = Point(segment.end) + self.insert(i, line) + return self + def reverse(self): if len(self._segments) == 0: return @@ -4816,47 +4970,48 @@ def reify(self): return self @staticmethod - def svg_d(segments, relative=False, transformed=True): + def svg_d(segments, relative=None, smooth=None): if len(segments) == 0: return '' - if relative: - return Path.svg_d_relative(segments, transformed=transformed) - else: - return Path.svg_d_absolute(segments, transformed=transformed) - - @staticmethod - def svg_d_relative(segments, transformed=True): parts = [] previous_segment = None p = Point(0) - for segment in segments: - if isinstance(segment, (Move, Line, Arc, Close)): - parts.append(segment.d(p)) - elif isinstance(segment, (CubicBezier, QuadraticBezier)): - parts.append(segment.d(p, smooth=segment.is_smooth_from(previous_segment))) - previous_segment = segment - p = previous_segment.end - return ' '.join(parts) - - @staticmethod - def svg_d_absolute(segments, transformed=True): - parts = [] - previous_segment = None - for segment in segments: - if isinstance(segment, (Move, Line, Arc, Close)): - parts.append(segment.d()) - elif isinstance(segment, (CubicBezier, QuadraticBezier)): - parts.append(segment.d(smooth=segment.is_smooth_from(previous_segment))) - previous_segment = segment + if smooth is None: + override_smooth = False + smooth_set_value = True + else: + override_smooth = True + smooth_set_value = bool(smooth) + if relative is not None: + for segment in segments: + if isinstance(segment, (Move, Line, Arc, Close)): + parts.append(segment.d(p, relative=relative)) + elif isinstance(segment, (CubicBezier, QuadraticBezier)): + if (override_smooth and smooth_set_value) or (not override_smooth and segment.smooth): + parts.append(segment.d(p, relative=relative, smooth=segment.is_smooth_from(previous_segment))) + else: + parts.append(segment.d(p, relative=relative, smooth=False)) + previous_segment = segment + p = previous_segment.end + else: + for segment in segments: + if isinstance(segment, (Move, Line, Arc, Close)): + parts.append(segment.d(p, relative=segment.relative)) + elif isinstance(segment, (CubicBezier, QuadraticBezier)): + if (override_smooth and smooth_set_value) or (not override_smooth and segment.smooth): + parts.append( + segment.d(p, relative=segment.relative, smooth=segment.is_smooth_from(previous_segment))) + else: + parts.append(segment.d(p, relative=segment.relative, smooth=False)) + previous_segment = segment + p = previous_segment.end return ' '.join(parts) - def d(self, relative=False, transformed=True): + def d(self, relative=None, transformed=True, smooth=None): + path = self if transformed: - p = self.__copy__() - p.reify() - return Path.svg_d(p._segments, relative) - else: - return Path.svg_d(self._segments, relative) + path = abs(path) + return Path.svg_d(path._segments, relative=relative, smooth=smooth) def segments(self, transformed=True): if transformed: @@ -5762,7 +5917,7 @@ class Subpath: Subpath is a Path-backed window implementation. It does not store a list of segments but rather stores a Path, start position, end position. When a function is called on a subpath, the result of those events is performed on the backing Path. When the backing Path is modified the behavior is - undefined.""" + undefined.""" def __init__(self, path, start, end): self._path = path @@ -5899,9 +6054,9 @@ def bbox(self): ymax = max(ymaxs) return xmin, ymin, xmax, ymax - def d(self, relative=False): + def d(self, relative=None, smooth=None): segments = self._path._segments[self._start:self._end + 1] - return Path.svg_d(segments, relative) + return Path.svg_d(segments, relative=relative, smooth=None) def _reverse_segments(self, start, end): """Reverses segments between the given indexes in the subpath space.""" @@ -6771,9 +6926,13 @@ def parse(source, s = Rect(values) elif SVG_TAG_IMAGE == tag: s = SVGImage(values) - else: + elif tag in (SVG_TAG_STYLE, SVG_TAG_TEXT, SVG_TAG_DESC): #