Home | History | Annotate | Download | only in path
      1 # SVG Path specification parser.
      2 # This is an adaptation from 'svg.path' by Lennart Regebro (@regebro),
      3 # modified so that the parser takes a FontTools Pen object instead of
      4 # returning a list of svg.path Path objects.
      5 # The original code can be found at:
      6 # https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py
      7 # Copyright (c) 2013-2014 Lennart Regebro
      8 # License: MIT
      9 
     10 from __future__ import (
     11     print_function, division, absolute_import, unicode_literals)
     12 from fontTools.misc.py23 import *
     13 from .arc import EllipticalArc
     14 import re
     15 
     16 
     17 COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
     18 UPPERCASE = set('MZLHVCSQTA')
     19 
     20 COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
     21 FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
     22 
     23 
     24 def _tokenize_path(pathdef):
     25     for x in COMMAND_RE.split(pathdef):
     26         if x in COMMANDS:
     27             yield x
     28         for token in FLOAT_RE.findall(x):
     29             yield token
     30 
     31 
     32 def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc):
     33     """ Parse SVG path definition (i.e. "d" attribute of <path> elements)
     34     and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath
     35     methods.
     36 
     37     If 'current_pos' (2-float tuple) is provided, the initial moveTo will
     38     be relative to that instead being absolute.
     39 
     40     If the pen has an "arcTo" method, it is called with the original values
     41     of the elliptical arc curve commands:
     42 
     43         pen.arcTo(rx, ry, rotation, arc_large, arc_sweep, (x, y))
     44 
     45     Otherwise, the arcs are approximated by series of cubic Bezier segments
     46     ("curveTo"), one every 90 degrees.
     47     """
     48     # In the SVG specs, initial movetos are absolute, even if
     49     # specified as 'm'. This is the default behavior here as well.
     50     # But if you pass in a current_pos variable, the initial moveto
     51     # will be relative to that current_pos. This is useful.
     52     current_pos = complex(*current_pos)
     53 
     54     elements = list(_tokenize_path(pathdef))
     55     # Reverse for easy use of .pop()
     56     elements.reverse()
     57 
     58     start_pos = None
     59     command = None
     60     last_control = None
     61 
     62     have_arcTo = hasattr(pen, "arcTo")
     63 
     64     while elements:
     65 
     66         if elements[-1] in COMMANDS:
     67             # New command.
     68             last_command = command  # Used by S and T
     69             command = elements.pop()
     70             absolute = command in UPPERCASE
     71             command = command.upper()
     72         else:
     73             # If this element starts with numbers, it is an implicit command
     74             # and we don't change the command. Check that it's allowed:
     75             if command is None:
     76                 raise ValueError("Unallowed implicit command in %s, position %s" % (
     77                     pathdef, len(pathdef.split()) - len(elements)))
     78             last_command = command  # Used by S and T
     79 
     80         if command == 'M':
     81             # Moveto command.
     82             x = elements.pop()
     83             y = elements.pop()
     84             pos = float(x) + float(y) * 1j
     85             if absolute:
     86                 current_pos = pos
     87             else:
     88                 current_pos += pos
     89 
     90             # M is not preceded by Z; it's an open subpath
     91             if start_pos is not None:
     92                 pen.endPath()
     93 
     94             pen.moveTo((current_pos.real, current_pos.imag))
     95 
     96             # when M is called, reset start_pos
     97             # This behavior of Z is defined in svg spec:
     98             # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
     99             start_pos = current_pos
    100 
    101             # Implicit moveto commands are treated as lineto commands.
    102             # So we set command to lineto here, in case there are
    103             # further implicit commands after this moveto.
    104             command = 'L'
    105 
    106         elif command == 'Z':
    107             # Close path
    108             if current_pos != start_pos:
    109                 pen.lineTo((start_pos.real, start_pos.imag))
    110             pen.closePath()
    111             current_pos = start_pos
    112             start_pos = None
    113             command = None  # You can't have implicit commands after closing.
    114 
    115         elif command == 'L':
    116             x = elements.pop()
    117             y = elements.pop()
    118             pos = float(x) + float(y) * 1j
    119             if not absolute:
    120                 pos += current_pos
    121             pen.lineTo((pos.real, pos.imag))
    122             current_pos = pos
    123 
    124         elif command == 'H':
    125             x = elements.pop()
    126             pos = float(x) + current_pos.imag * 1j
    127             if not absolute:
    128                 pos += current_pos.real
    129             pen.lineTo((pos.real, pos.imag))
    130             current_pos = pos
    131 
    132         elif command == 'V':
    133             y = elements.pop()
    134             pos = current_pos.real + float(y) * 1j
    135             if not absolute:
    136                 pos += current_pos.imag * 1j
    137             pen.lineTo((pos.real, pos.imag))
    138             current_pos = pos
    139 
    140         elif command == 'C':
    141             control1 = float(elements.pop()) + float(elements.pop()) * 1j
    142             control2 = float(elements.pop()) + float(elements.pop()) * 1j
    143             end = float(elements.pop()) + float(elements.pop()) * 1j
    144 
    145             if not absolute:
    146                 control1 += current_pos
    147                 control2 += current_pos
    148                 end += current_pos
    149 
    150             pen.curveTo((control1.real, control1.imag),
    151                         (control2.real, control2.imag),
    152                         (end.real, end.imag))
    153             current_pos = end
    154             last_control = control2
    155 
    156         elif command == 'S':
    157             # Smooth curve. First control point is the "reflection" of
    158             # the second control point in the previous path.
    159 
    160             if last_command not in 'CS':
    161                 # If there is no previous command or if the previous command
    162                 # was not an C, c, S or s, assume the first control point is
    163                 # coincident with the current point.
    164                 control1 = current_pos
    165             else:
    166                 # The first control point is assumed to be the reflection of
    167                 # the second control point on the previous command relative
    168                 # to the current point.
    169                 control1 = current_pos + current_pos - last_control
    170 
    171             control2 = float(elements.pop()) + float(elements.pop()) * 1j
    172             end = float(elements.pop()) + float(elements.pop()) * 1j
    173 
    174             if not absolute:
    175                 control2 += current_pos
    176                 end += current_pos
    177 
    178             pen.curveTo((control1.real, control1.imag),
    179                         (control2.real, control2.imag),
    180                         (end.real, end.imag))
    181             current_pos = end
    182             last_control = control2
    183 
    184         elif command == 'Q':
    185             control = float(elements.pop()) + float(elements.pop()) * 1j
    186             end = float(elements.pop()) + float(elements.pop()) * 1j
    187 
    188             if not absolute:
    189                 control += current_pos
    190                 end += current_pos
    191 
    192             pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
    193             current_pos = end
    194             last_control = control
    195 
    196         elif command == 'T':
    197             # Smooth curve. Control point is the "reflection" of
    198             # the second control point in the previous path.
    199 
    200             if last_command not in 'QT':
    201                 # If there is no previous command or if the previous command
    202                 # was not an Q, q, T or t, assume the first control point is
    203                 # coincident with the current point.
    204                 control = current_pos
    205             else:
    206                 # The control point is assumed to be the reflection of
    207                 # the control point on the previous command relative
    208                 # to the current point.
    209                 control = current_pos + current_pos - last_control
    210 
    211             end = float(elements.pop()) + float(elements.pop()) * 1j
    212 
    213             if not absolute:
    214                 end += current_pos
    215 
    216             pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
    217             current_pos = end
    218             last_control = control
    219 
    220         elif command == 'A':
    221             rx = float(elements.pop())
    222             ry = float(elements.pop())
    223             rotation = float(elements.pop())
    224             arc_large = bool(int(elements.pop()))
    225             arc_sweep = bool(int(elements.pop()))
    226             end = float(elements.pop()) + float(elements.pop()) * 1j
    227 
    228             if not absolute:
    229                 end += current_pos
    230 
    231             # if the pen supports arcs, pass the values unchanged, otherwise
    232             # approximate the arc with a series of cubic bezier curves
    233             if have_arcTo:
    234                 pen.arcTo(
    235                     rx,
    236                     ry,
    237                     rotation,
    238                     arc_large,
    239                     arc_sweep,
    240                     (end.real, end.imag),
    241                 )
    242             else:
    243                 arc = arc_class(
    244                     current_pos, rx, ry, rotation, arc_large, arc_sweep, end
    245                 )
    246                 arc.draw(pen)
    247 
    248             current_pos = end
    249 
    250     # no final Z command, it's an open path
    251     if start_pos is not None:
    252         pen.endPath()
    253