docs: def cleanPath(element, options):
      """
         Cleans the path string (d attribute) of the element
      """
      global _num_bytes_saved_in_path_data
      global _num_path_segments_removed
  
      # this gets the parser object from svg_regex.py
      oldPathStr = element.getAttribute('d')
      path = svg_parser.parse(oldPathStr)
      style = _getStyle(element)
  
      # This determines whether the stroke has round or square linecaps.  If it does, we do not want to collapse empty
      # segments, as they are actually rendered (as circles or squares with diameter/dimension matching the path-width).
      has_round_or_square_linecaps = (
          element.getAttribute('stroke-linecap') in ['round', 'square']
          or 'stroke-linecap' in style and style['stroke-linecap'] in ['round', 'square']
      )
  
      # This determines whether the stroke has intermediate markers.  If it does, we do not want to collapse
      # straight segments running in the same direction, as markers are rendered on the intermediate nodes.
      has_intermediate_markers = (
          element.hasAttribute('marker')
          or element.hasAttribute('marker-mid')
          or 'marker' in style
          or 'marker-mid' in style
      )
  
      # The first command must be a moveto, and whether it's relative (m)
      # or absolute (M), the first set of coordinates *is* absolute. So
      # the first iteration of the loop below will get x,y and startx,starty.
  
      # convert absolute coordinates into relative ones.
      # Reuse the data structure 'path', since we're not adding or removing subcommands.
      # Also reuse the coordinate lists since we're not adding or removing any.
      x = y = 0
      for pathIndex in range(len(path)):
          cmd, data = path[pathIndex]  # Changes to cmd don't get through to the data structure
          i = 0
          # adjust abs to rel
          # only the A command has some values that we don't want to adjust (radii, rotation, flags)
          if cmd == 'A':
              for i in range(i, len(data), 7):
                  data[i + 5] -= x
                  data[i + 6] -= y
                  x += data[i + 5]
                  y += data[i + 6]
              path[pathIndex] = ('a', data)
          elif cmd == 'a':
              x += sum(data[5::7])
              y += sum(data[6::7])
          elif cmd == 'H':
              for i in range(i, len(data)):
                  data[i] -= x
                  x += data[i]
              path[pathIndex] = ('h', data)
          elif cmd == 'h':
              x += sum(data)
          elif cmd == 'V':
              for i in range(i, len(data)):
                  data[i] -= y
                  y += data[i]
              path[pathIndex] = ('v', data)
          elif cmd == 'v':
              y += sum(data)
          elif cmd == 'M':
              startx, starty = data[0], data[1]
              # If this is a path starter, don't convert its first
              # coordinate to relative; that would just make it (0, 0)
              if pathIndex != 0:
                  data[0] -= x
                  data[1] -= y
  
              x, y = startx, starty
              i = 2
              for i in range(i, len(data), 2):
                  data[i] -= x
                  data[i + 1] -= y
                  x += data[i]
                  y += data[i + 1]
              path[pathIndex] = ('m', data)
          elif cmd in ['L', 'T']:
              for i in range(i, len(data), 2):
                  data[i] -= x
                  data[i + 1] -= y
                  x += data[i]
                  y += data[i + 1]
              path[pathIndex] = (cmd.lower(), data)
          elif cmd in ['m']:
              if pathIndex == 0:
                  # START OF PATH - this is an absolute moveto
                  # followed by relative linetos
                  startx, starty = data[0], data[1]
                  x, y = startx, starty
                  i = 2
              else:
                  startx = x + data[0]
                  starty = y + data[1]
              for i in range(i, len(data), 2):
                  x += data[i]
                  y += data[i + 1]
          elif cmd in ['l', 't']:
              x += sum(data[0::2])
              y += sum(data[1::2])
          elif cmd in ['S', 'Q']:
              for i in range(i, len(data), 4):
                  data[i] -= x
                  data[i + 1] -= y
                  data[i + 2] -= x
                  data[i + 3] -= y
                  x += data[i + 2]
                  y += data[i + 3]
              path[pathIndex] = (cmd.lower(), data)
          elif cmd in ['s', 'q']:
              x += sum(data[2::4])
              y += sum(data[3::4])
          elif cmd == 'C':
              for i in range(i, len(data), 6):
                  data[i] -= x
                  data[i + 1] -= y
                  data[i + 2] -= x
                  data[i + 3] -= y
                  data[i + 4] -= x
                  data[i + 5] -= y
                  x += data[i + 4]
                  y += data[i + 5]
              path[pathIndex] = ('c', data)
          elif cmd == 'c':
              x += sum(data[4::6])
              y += sum(data[5::6])
          elif cmd in ['z', 'Z']:
              x, y = startx, starty
              path[pathIndex] = ('z', data)
  
      # remove empty segments and redundant commands
      # Reuse the data structure 'path' and the coordinate lists, even if we're
      # deleting items, because these deletions are relatively cheap.
      if not has_round_or_square_linecaps:
          # remove empty path segments
          for pathIndex in range(len(path)):
              cmd, data = path[pathIndex]
              i = 0
              if cmd in ['m', 'l', 't']:
                  if cmd == 'm':
                      # It might be tempting to rewrite "m0 0 ..." into
                      # "l..." here.  However, this is an unsound
                      # optimization in general as "m0 0 ... z" is
                      # different from "l...z".
                      #
                      # To do such a rewrite, we need to understand the
                      # full subpath.  This logic happens after this
                      # loop.
                      i = 2
                  while i < len(data):
                      if data[i] == data[i + 1] == 0:
                          del data[i:i + 2]
                          _num_path_segments_removed += 1
                      else:
                          i += 2
              elif cmd == 'c':
                  while i < len(data):
                      if data[i] == data[i + 1] == data[i + 2] == data[i + 3] == data[i + 4] == data[i + 5] == 0:
                          del data[i:i + 6]
                          _num_path_segments_removed += 1
                      else:
                          i += 6
              elif cmd == 'a':
                  while i < len(data):
                      if data[i + 5] == data[i + 6] == 0:
                          del data[i:i + 7]
                          _num_path_segments_removed += 1
                      else:
                          i += 7
              elif cmd == 'q':
                  while i < len(data):
                      if data[i] == data[i + 1] == data[i + 2] == data[i + 3] == 0:
                          del data[i:i + 4]
                          _num_path_segments_removed += 1
                      else:
                          i += 4
              elif cmd in ['h', 'v']:
                  oldLen = len(data)
                  path[pathIndex] = (cmd, [coord for coord in data if coord != 0])
                  _num_path_segments_removed += len(path[pathIndex][1]) - oldLen
  
          # remove no-op commands
          pathIndex = len(path)
          subpath_needs_anchor = False
          # NB: We can never rewrite the first m/M command (expect if it
          # is the only command)
          while pathIndex > 1:
              pathIndex -= 1
              cmd, data = path[pathIndex]
              if cmd == 'z':
                  next_cmd, next_data = path[pathIndex - 1]
                  if next_cmd == 'm' and len(next_data) == 2:
                      # mX Yz -> mX Y
  
                      # note the len check on next_data as it is not
                      # safe to rewrite "m0 0 1 1z" in general (it is a
                      # question of where the "pen" ends - you can
                      # continue a draw on the same subpath after a
                      # "z").
                      del path[pathIndex]
                      _num_path_segments_removed += 1
                  else:
                      # it is not safe to rewrite "m0 0 ..." to "l..."
                      # because of this "z" command.
                      subpath_needs_anchor = True
              elif cmd == 'm':
                  if len(path) - 1 == pathIndex and len(data) == 2:
                      # Ends with an empty move (but no line/draw
                      # following it)
                      del path[pathIndex]
                      _num_path_segments_removed += 1
                      continue
                  if subpath_needs_anchor:
                      subpath_needs_anchor = False
                  elif data[0] == data[1] == 0:
                      # unanchored, i.e. we can replace "m0 0 ..." with
                      # "l..." as there is no "z" after it.
                      path[pathIndex] = ('l', data[2:])
                      _num_path_segments_removed += 1
  
      # fixup: Delete subcommands having no coordinates.
      path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z']
  
      # convert straight curves into lines
      newPath = [path[0]]
      for (cmd, data) in path[1:]:
          i = 0
          newData = data
          if cmd == 'c':
              newData = []
              while i < len(data):
                  # since all commands are now relative, we can think of previous point as (0,0)
                  # and new point (dx,dy) is (data[i+4],data[i+5])
                  # eqn of line will be y = (dy/dx)*x or if dx=0 then eqn of line is x=0
                  (p1x, p1y) = (data[i], data[i + 1])
                  (p2x, p2y) = (data[i + 2], data[i + 3])
                  dx = data[i + 4]
                  dy = data[i + 5]
  
                  foundStraightCurve = False
  
                  if dx == 0:
                      if p1x == 0 and p2x == 0:
                          foundStraightCurve = True
                  else:
                      m = dy / dx
                      if p1y == m * p1x and p2y == m * p2x:
                          foundStraightCurve = True
  
                  if foundStraightCurve:
                      # flush any existing curve coords first
                      if newData:
                          newPath.append((cmd, newData))
                          newData = []
                      # now create a straight line segment
                      newPath.append(('l', [dx, dy]))
                  else:
                      newData.extend(data[i:i + 6])
  
                  i += 6
          if newData or cmd == 'z' or cmd == 'Z':
              newPath.append((cmd, newData))
      path = newPath
  
      # collapse all consecutive commands of the same type into one command
      prevCmd = ''
      prevData = []
      newPath = []
      for (cmd, data) in path:
          if prevCmd == '':
              # initialize with current path cmd and data
              prevCmd = cmd
              prevData = data
          else:
              # collapse if
              # - cmd is not moveto (explicit moveto commands are not drawn)
              # - the previous and current commands are the same type,
              # - the previous command is moveto and the current is lineto
              #   (subsequent moveto pairs are treated as implicit lineto commands)
              if cmd != 'm' and (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')):
                  prevData.extend(data)
              # else flush the previous command if it is not the same type as the current command
              else:
                  newPath.append((prevCmd, prevData))
                  prevCmd = cmd
                  prevData = data
      # flush last command and data
      newPath.append((prevCmd, prevData))
      path = newPath
  
      # convert to shorthand path segments where possible
      newPath = []
      for (cmd, data) in path:
          # convert line segments into h,v where possible
          if cmd == 'l':
              i = 0
              lineTuples = []
              while i < len(data):
                  if data[i] == 0:
                      # vertical
                      if lineTuples:
                          # flush the existing line command
                          newPath.append(('l', lineTuples))
                          lineTuples = []
                      # append the v and then the remaining line coords
                      newPath.append(('v', [data[i + 1]]))
                      _num_path_segments_removed += 1
                  elif data[i + 1] == 0:
                      if lineTuples:
                          # flush the line command, then append the h and then the remaining line coords
                          newPath.append(('l', lineTuples))
                          lineTuples = []
                      newPath.append(('h', [data[i]]))
                      _num_path_segments_removed += 1
                  else:
                      lineTuples.extend(data[i:i + 2])
                  i += 2
              if lineTuples:
                  newPath.append(('l', lineTuples))
          # also handle implied relative linetos
          elif cmd == 'm':
              i = 2
              lineTuples = [data[0], data[1]]
              while i < len(data):
                  if data[i] == 0:
                      # vertical
                      if lineTuples:
                          # flush the existing m/l command
                          newPath.append((cmd, lineTuples))
                          lineTuples = []
                          cmd = 'l'  # dealing with linetos now
                      # append the v and then the remaining line coords
                      newPath.append(('v', [data[i + 1]]))
                      _num_path_segments_removed += 1
                  elif data[i + 1] == 0:
                      if lineTuples:
                          # flush the m/l command, then append the h and then the remaining line coords
                          newPath.append((cmd, lineTuples))
                          lineTuples = []
                          cmd = 'l'  # dealing with linetos now
                      newPath.append(('h', [data[i]]))
                      _num_path_segments_removed += 1
                  else:
                      lineTuples.extend(data[i:i + 2])
                  i += 2
              if lineTuples:
                  newPath.append((cmd, lineTuples))
          # convert Bézier curve segments into s where possible
          elif cmd == 'c':
              # set up the assumed bezier control point as the current point,
              # i.e. (0,0) since we're using relative coords
              bez_ctl_pt = (0, 0)
              # however if the previous command was 's'
              # the assumed control point is a reflection of the previous control point at the current point
              if len(newPath):
                  (prevCmd, prevData) = newPath[-1]
                  if prevCmd == 's':
                      bez_ctl_pt = (prevData[-2] - prevData[-4], prevData[-1] - prevData[-3])
              i = 0
              curveTuples = []
              while i < len(data):
                  # rotate by 180deg means negate both coordinates
                  # if the previous control point is equal then we can substitute a
                  # shorthand bezier command
                  if bez_ctl_pt[0] == data[i] and bez_ctl_pt[1] == data[i + 1]:
                      if curveTuples:
                          newPath.append(('c', curveTuples))
                          curveTuples = []
                      # append the s command
                      newPath.append(('s', [data[i + 2], data[i + 3], data[i + 4], data[i + 5]]))
                      _num_path_segments_removed += 1
                  else:
                      j = 0
                      while j <= 5:
                          curveTuples.append(data[i + j])
                          j += 1
  
                  # set up control point for next curve segment
                  bez_ctl_pt = (data[i + 4] - data[i + 2], data[i + 5] - data[i + 3])
                  i += 6
  
              if curveTuples:
                  newPath.append(('c', curveTuples))
          # convert quadratic curve segments into t where possible
          elif cmd == 'q':
              quad_ctl_pt = (0, 0)
              i = 0
              curveTuples = []
              while i < len(data):
                  if quad_ctl_pt[0] == data[i] and quad_ctl_pt[1] == data[i + 1]:
                      if curveTuples:
                          newPath.append(('q', curveTuples))
                          curveTuples = []
                      # append the t command
                      newPath.append(('t', [data[i + 2], data[i + 3]]))
                      _num_path_segments_removed += 1
                  else:
                      j = 0
                      while j <= 3:
                          curveTuples.append(data[i + j])
                          j += 1
  
                  quad_ctl_pt = (data[i + 2] - data[i], data[i + 3] - data[i + 1])
                  i += 4
  
              if curveTuples:
                  newPath.append(('q', curveTuples))
          else:
              newPath.append((cmd, data))
      path = newPath
  
      # For each m, l, h or v, collapse unnecessary coordinates that run in the same direction
      # i.e. "h-100-100" becomes "h-200" but "h300-100" does not change.
      # If the path has intermediate markers we have to preserve intermediate nodes, though.
      # Reuse the data structure 'path', since we're not adding or removing subcommands.
      # Also reuse the coordinate lists, even if we're deleting items, because these
      # deletions are relatively cheap.
      if not has_intermediate_markers:
          for pathIndex in range(len(path)):
              cmd, data = path[pathIndex]
  
              # h / v expects only one parameter and we start drawing with the first (so we need at least 2)
              if cmd in ['h', 'v'] and len(data) >= 2:
                  coordIndex = 0
                  while coordIndex+1 < len(data):
                      if is_same_sign(data[coordIndex], data[coordIndex+1]):
                          data[coordIndex] += data[coordIndex+1]
                          del data[coordIndex+1]
                          _num_path_segments_removed += 1
                      else:
                          coordIndex += 1
  
              # l expects two parameters and we start drawing with the first (so we need at least 4)
              elif cmd == 'l' and len(data) >= 4:
                  coordIndex = 0
                  while coordIndex+2 < len(data):
                      if is_same_direction(*data[coordIndex:coordIndex+4]):
                          data[coordIndex] += data[coordIndex+2]
                          data[coordIndex+1] += data[coordIndex+3]
                          del data[coordIndex+2]  # delete the next two elements
                          del data[coordIndex+2]
                          _num_path_segments_removed += 1
                      else:
                          coordIndex += 2
  
              # m expects two parameters but we have to skip the first pair as it's not drawn (so we need at least 6)
              elif cmd == 'm' and len(data) >= 6:
                  coordIndex = 2
                  while coordIndex+2 < len(data):
                      if is_same_direction(*data[coordIndex:coordIndex+4]):
                          data[coordIndex] += data[coordIndex+2]
                          data[coordIndex+1] += data[coordIndex+3]
                          del data[coordIndex+2]  # delete the next two elements
                          del data[coordIndex+2]
                          _num_path_segments_removed += 1
                      else:
                          coordIndex += 2
  
      # it is possible that we have consecutive h, v, c, t commands now
      # so again collapse all consecutive commands of the same type into one command
      prevCmd = ''
      prevData = []
      newPath = [path[0]]
      for (cmd, data) in path[1:]:
          # flush the previous command if it is not the same type as the current command
          if prevCmd != '':
              if cmd != prevCmd or cmd == 'm':
                  newPath.append((prevCmd, prevData))
                  prevCmd = ''
                  prevData = []
  
          # if the previous and current commands are the same type, collapse
          if cmd == prevCmd and cmd != 'm':
              prevData.extend(data)
  
          # save last command and data
          else:
              prevCmd = cmd
              prevData = data
      # flush last command and data
      if prevCmd != '':
          newPath.append((prevCmd, prevData))
      path = newPath
  
      newPathStr = serializePath(path, options)
  
      # if for whatever reason we actually made the path longer don't use it
      # TODO: maybe we could compare path lengths after each optimization step and use the shortest
      if len(newPathStr) <= len(oldPathStr):
          _num_bytes_saved_in_path_data += (len(oldPathStr) - len(newPathStr))
          element.setAttribute('d', newPathStr)