text: """
  ASCII art to image converter.
  
  This is the main modlue that contains the parser.
  See svg.py and aa.py for output modules, that can render the parsed structure.
  
  (C) 2006 Chris Liechti <cliechti@gmx.net>
  """
  
  NOMINAL_SIZE = 2
  
  CLASS_LINE = 'line'
  CLASS_STRING = 'str'
  CLASS_RECTANGLE = 'rect'
  CLASS_JOIN = 'join'
  CLASS_FIXED = 'fixed'
  
  # - - - - - - - - - - - - - - Shapes - - - - - - - - - - - - - - -
  def point(object):
      """return a Point instance.
         - if object is already a Point instance it's returned as is
         - complex numbers are converted to Points
         - a tuple with two elements (x,y)
      """
      if isinstance(object, Point):
          return object
      #~ print type(object), object.__class__
      if type(object) is complex:
          return Point(object.real, object.imag)
      if type(object) is tuple and len(object) == 2:
          return Point(object[0], object[1])
      raise ValueError('can not convert %r to a Point')
  
  def group(list_of_shapes):
      """return a group if the number of shapes is greater than one"""
      if len(list_of_shapes) > 1:
          return [Group(list_of_shapes)]
      else:
          return list_of_shapes
  
  class Point:
      """A single point. This class primary use is to represent coordinates
         for the other shapes.
      """
      def __init__(self, x, y):
          self.x = x
          self.y = y
          
      def __repr__(self):
          return 'Point(%r, %r)' % (self.x, self.y)
  
  class Line:
      """Line with starting and ending point. Both ends can have arrows"""
      def __init__(self, start, end, thick=False):
          self.thick = thick
          self.start = point(start)
          self.end = point(end)
      
      def __repr__(self):
          return 'Line(%r, %r)' % (self.start, self.end)
  
  class Rectangle:
      """Rectangle with to edge coordiantes."""
      def __init__(self, p1, p2):
          self.p1 = point(p1)
          self.p2 = point(p2)
      def __repr__(self):
          return 'Rectangle(%r, %r)' % (self.p1, self.p2)
  
  class Circle:
      """Rectangle with to edge coordiantes."""
      def __init__(self, center, radius):
          self.center = point(center)
          self.radius = radius
          
      def __repr__(self):
          return 'Circle(%r, %r)' % (self.center, self.radius)
  
  class Label:
      """A label at a position"""
      def __init__(self, position, text):
          self.position = position
          self.text = text
      def __repr__(self):
          return 'Label(%r, %r)' % (self.position, self.text)
  
  class Group:
      """A group of shapes"""
      def __init__(self, shapes=None):
          if shapes is None: shapes = []
          self.shapes = shapes
      def __repr__(self):
          return 'Group(%r)' % (self.shapes,)
  
  
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  
  class AsciiArtImage:
      """This class hold a ASCII art figure and has methods to parse it.
         The resaulting list of shapes is also stored here.
      """
      QUOTATION_CHARACTERS = list('"\'`')
      
      def __init__(self, text, aspect_ratio=1, textual=False):
          """Take a ASCII art figure and store it, prepare for ``recognize``"""
          self.aspect_ratio = float(aspect_ratio)
          self.textual = textual
          #XXX TODO tab expansion
          #detect size of input image
          self.image = []
          max_x = 0
          for y, line in enumerate(text.splitlines()):
              max_x = max(max_x, len(line))
              self.image.append(line)
          self.width = max_x
          self.height = y+1
          #make sure it's rectangular
          for y, line in enumerate(self.image):
              if len(line) < max_x:
                  self.image[y] = line + ' '*(max_x-len(line))
          #initialize other data structures
          self.classification = [[None]*self.width for y in range(self.height)]
          self.shapes = []
          self.nominal_size = NOMINAL_SIZE
          
      def __str__(self):
          """Return the original image"""
          return '\n'.join([self.image[y] for y in range(self.height)])
      
      def get(self, x, y):
          """Get character from image. Gives no error for access out of
             bounds, just returns a space. This simplifies the scanner
             functions.
          """
          if 0 <= x < self.width and 0 <= y < self.height:
              return self.image[y][x]
          else:
              return ' '
  
      def tag(self, coordinates, classification):
          """Tag coordinates as used, store classification"""
          for x, y in coordinates:
              self.classification[y][x] = classification
  
      def cls(self, x, y):
          """get tag at coordinate"""
          try:
              return self.classification[y][x]
          except IndexError:
              return 'outside'
          
      # Coordinate converison and shifting
      def left(self, x):          return x*NOMINAL_SIZE*self.aspect_ratio
      def hcenter(self, x):       return (x+0.5)*NOMINAL_SIZE*self.aspect_ratio
      def right(self, x):         return (x+1)*NOMINAL_SIZE*self.aspect_ratio
      def top(self, y):           return y*NOMINAL_SIZE
      def vcenter(self, y):       return (y+0.5)*NOMINAL_SIZE
      def bottom(self, y):        return (y+1)*NOMINAL_SIZE
  
      def recognize(self):
          """Try to convert ASCII art to vector graphics."""
          #XXX search for symbols
          #~ #search for long strings
          #~ for y in range(self.height):
              #~ for x in range(self.width):
                  #~ character = self.image[y][x]
                  #~ if self.classification[y][x] is None:
                      #~ if character.isalnum():
                          #~ self.shapes.extend(self._follow_horizontal_string(x, y))
          #search for quoted texts
          for y in range(self.height):
              for x in range(self.width):
                  #if not yet classified, check for a line
                  character = self.image[y][x]
                  if character in self.QUOTATION_CHARACTERS:
                      self.shapes.extend(self._follow_horizontal_string(x, y, quoted=True))
          
          #search for standard shapes
          for y in range(self.height):
              for x in range(self.width):
                  #if not yet classified, check for a line
                  character = self.image[y][x]
                  if self.classification[y][x] is None:
                      if character == '-':
                          self.shapes.extend(self._follow_horizontal_line(x, y))
                      elif character == '|':
                          self.shapes.extend(self._follow_vertical_line(x, y))
                      elif character == '_':
                          self.shapes.extend(self._follow_lower_horizontal_line(x, y))
                      elif character == '=':
                          self.shapes.extend(self._follow_horizontal_line(x, y, thick=True))
                      if character == '+':
                          self.shapes.extend(self._plus_joiner(x, y))
                      #~ if character in self.FILL_CHARACTERS \
                          #~ and ((self.get(x+1,y) == character and self.get(x+2,y) == character) \
                               #~ or self.get(x,y+1) == character):
                      elif character in self.FIXED_CHARACTERS:
                          self.shapes.extend(self.get_fixed_character(character)(x, y))
                          self.tag([(x,y)], CLASS_FIXED)
                      elif character in self.FILL_CHARACTERS:
                          if self.textual:
                              if self.get(x,y+1) == character:
                                  self.shapes.extend(self._follow_fill(character, x, y))
                          else:
                              if (self.get(x+1,y) == character or self.get(x,y+1) == character):
                                  self.shapes.extend(self._follow_fill(character, x, y))
                  if character in '\\/':
                      self.shapes.extend(self._follow_rounded_edge(x, y))
          
          #search for short strings too
          for y in range(self.height):
              for x in range(self.width):
                  character = self.image[y][x]
                  if self.classification[y][x] is None:
                      if character != ' ':
                          self.shapes.extend(self._follow_horizontal_string(x, y, accept_anything=True))
  
      # - - - - - - - - - helper function for some shapes - - - - - - - - -
      # use complex numbers as 2D vectors as that means easy transformations like
      # scaling, rotation and translation
      
      # - - - - - - - - - arrows - - - - - - - - -
      def _standard_arrow(self, p1, p2):
          """-->
             return a possibly modified starting point and a list of shapes
          """
          direction_vector = p1 - p2
          direction_vector /= abs(direction_vector)
          return p1, [
              Line(p1, p1-direction_vector*1.5+direction_vector*0.5j),
              Line(p1, p1-direction_vector*1.5+direction_vector*-0.5j)
          ]
  
      def _reversed_arrow(self, p1, p2):
          """--<"""
          direction_vector = p1 - p2
          direction_vector /= abs(direction_vector)
          return p1-direction_vector*2, [
              Line(p1-direction_vector*2.0, p1+direction_vector*(-0.5+0.5j)),
              Line(p1-direction_vector*2.0, p1+direction_vector*(-0.5-0.5j))
          ]
  
      def _circle_head(self, p1, p2, radius=0.5):
          """--o"""
          direction_vector = p1 - p2
          direction_vector /= abs(direction_vector)
          return p1-direction_vector, [Circle(p1-direction_vector, radius)]
  
      def _large_circle_head(self, p1, p2):
          """--O"""
          return self._circle_head(p1, p2, radius=0.9)
  
      def _rectangular_head(self, p1, p2):
          """--#"""
          direction_vector = p1 - p2
          direction_vector /= abs(direction_vector)
          #~ return p1-direction_vector*1.414, [
              #~ Rectangle(p1-direction_vector-direction_vector*(0.707+0.707j),
                        #~ p1-direction_vector+direction_vector*(0.707+0.707j))
          #~ ]
          return p1-direction_vector*1.707, [
              Line(p1-direction_vector-direction_vector*(0.707+0.707j),
                   p1-direction_vector-direction_vector*(0.707-0.707j)),
              Line(p1-direction_vector+direction_vector*(0.707+0.707j),
                   p1-direction_vector+direction_vector*(0.707-0.707j)),
              Line(p1-direction_vector-direction_vector*(0.707+0.707j),
                   p1-direction_vector+direction_vector*(0.707-0.707j)),
              Line(p1-direction_vector-direction_vector*(0.707-0.707j),
                   p1-direction_vector+direction_vector*(0.707+0.707j)),
          ]
      
      #the same character can mean a different thing, depending from where
      #the line is coming. this table maps line direction (dx,dy) and the
      #arrow character to a arrow drawing function
      ARROW_TYPES = [
          #chr  dx  dy  arrow type
          ('>',  1,  0, '_standard_arrow'),
          ('<', -1,  0, '_standard_arrow'),
          ('^',  0, -1, '_standard_arrow'),
          ('A',  0, -1, '_standard_arrow'),
          ('V',  0,  1, '_standard_arrow'),
          ('v',  0,  1, '_standard_arrow'),
          ('>', -1,  0, '_reversed_arrow'),
          ('<',  1,  0, '_reversed_arrow'),
          ('^',  0,  1, '_reversed_arrow'),
          ('V',  0, -1, '_reversed_arrow'),
          ('v',  0, -1, '_reversed_arrow'),
          ('o',  1,  0, '_circle_head'),
          ('o', -1,  0, '_circle_head'),
          ('o',  0, -1, '_circle_head'),
          ('o',  0,  1, '_circle_head'),
          ('O',  1,  0, '_large_circle_head'),
          ('O', -1,  0, '_large_circle_head'),
          ('O',  0, -1, '_large_circle_head'),
          ('O',  0,  1, '_large_circle_head'),
          ('#',  1,  0, '_rectangular_head'),
          ('#', -1,  0, '_rectangular_head'),
          ('#',  0, -1, '_rectangular_head'),
          ('#',  0,  1, '_rectangular_head'),
      ]
      ARROW_HEADS = list('<>AVv^oO#')
  
      def get_arrow(self, character, dx, dy):
          """return arrow drawing function or None"""
          for head, ddx, ddy, function_name in self.ARROW_TYPES:
              if character == head and dx == ddx and dy == ddy:
                  return getattr(self, function_name)
      
      # - - - - - - - - - fills - - - - - - - - -
      
      def _hatch_left(self, x, y): return self._n_hatch_diagonal(x,y,1,True)
      def _hatch_right(self, x, y): return self._n_hatch_diagonal(x,y,1,False)
      def _cross_hatch(self, x, y): return self._n_hatch_diagonal(x,y,1,True) + self._n_hatch_diagonal(x,y,1,False)
      def _double_hatch_left(self, x, y): return self._n_hatch_diagonal(x,y,2,True)
      def _double_hatch_right(self, x, y): return self._n_hatch_diagonal(x,y,2,False)
      def _double_cross_hatch(self, x, y): return self._n_hatch_diagonal(x,y,2,True) + self._n_hatch_diagonal(x,y,2,False)
      def _triple_hatch_left(self, x, y): return self._n_hatch_diagonal(x,y,3,True)
      def _triple_hatch_right(self, x, y): return self._n_hatch_diagonal(x,y,3,False)
      def _triple_cross_hatch(self, x, y): return self._n_hatch_diagonal(x,y,3,True) + self._n_hatch_diagonal(x,y,3,False)
          
      def _n_hatch_diagonal(self, x, y, n, left=False):
          """hatch generator function"""
          d = 1/float(n)
          result = []
          if left:
              for i in range(n):
                  result.append(Line(Point(self.left(x), self.top(y+d*i)), Point(self.right(x-d*i), self.bottom(y))))
                  if n:
                      result.append(Line(Point(self.right(x-d*i), self.top(y)), Point(self.right(x), self.top(y+d*i))))
          else:
              for i in range(n):
                  result.append(Line(Point(self.left(x), self.top(y+d*i)), Point(self.left(x+d*i), self.top(y))))
                  if n:
                      result.append(Line(Point(self.left(x+d*i), self.bottom(y)), Point(self.right(x), self.top(y+d*i))))
          return result
  
      def _hatch_v(self, x, y): return self._n_hatch_straight(x,y,1,True)
      def _hatch_h(self, x, y): return self._n_hatch_straight(x,y,1,False)
      def _hv_hatch(self, x, y): return self._n_hatch_straight(x,y,1,True) + self._n_hatch_straight(x,y,1,False)
      def _double_hatch_v(self, x, y): return self._n_hatch_straight(x,y,2,True)
      def _double_hatch_h(self, x, y): return self._n_hatch_straight(x,y,2,False)
      def _double_hv_hatch(self, x, y): return self._n_hatch_straight(x,y,2,True) + self._n_hatch_straight(x,y,2,False)
      def _triple_hatch_v(self, x, y): return self._n_hatch_straight(x,y,3,True)
      def _triple_hatch_h(self, x, y): return self._n_hatch_straight(x,y,3,False)
      def _triple_hv_hatch(self, x, y): return self._n_hatch_straight(x,y,3,True) + self._n_hatch_straight(x,y,3,False)
  
      def _n_hatch_straight(self, x, y, n, vertical=False):
          """hatch generator function"""
          d = 1/float(n)
          offset = 1.0/(n+1)
          result = []
          if vertical:
              for i in range(n):
                  i = i + offset
                  result.append(Line(Point(self.left(x+d*i), self.top(y)), Point(self.left(x+d*i), self.bottom(y))))
                  #~ if n:
                      #~ result.append(Line(Point(self.right(x-d*i), self.top(y)), Point(self.right(x), self.top(y+d*i))))
          else:
              for i in range(n):
                  i = i + offset
                  result.append(Line(Point(self.left(x), self.top(y+d*i)), Point(self.right(x), self.top(y+d*i))))
                  #~ if n:
                      #~ result.append(Line(Point(self.left(x+d*i), self.bottom(y)), Point(self.right(x), self.top(y+d*i))))
          return result
  
      def _fill_trail(self, x, y):
          return [
              Line(Point(self.left(x+0.707), self.top(y)), Point(self.right(x), self.bottom(y-0.707))),
              Line(Point(self.left(x), self.top(y+0.707)), Point(self.right(x-0.707), self.bottom(y)))
          ]
  
      def _fill_foreground(self, x, y):
          return [
              Rectangle(Point(self.left(x), self.top(y)), Point(self.right(x), self.bottom(y)))
          ]
      
      def _fill_background(self, x, y): return []
  
      def _fill_small_circle(self, x, y):
          return [
              Circle(Point(self.left(x+0.5), self.top(y+0.5)), 0.2)
          ]
      def _fill_medium_circle(self, x, y):
          return [
              Circle(Point(self.left(x+0.5), self.top(y+0.5)), 0.4)
          ]
          
      def _fill_large_circle(self, x, y):
          return [
              Circle(Point(self.left(x+0.5), self.top(y+0.5)), 0.9)
          ]
  
      def _fill_qmark(self, x, y):
          return [
              Label(Point(self.left(x), self.bottom(y)), '?')
          ]
          
      def _fill_triangles(self, x, y):
          return [
              Line(Point(self.left(x+0.5), self.top(y+0.2)), Point(self.left(x+0.75), self.top(y+0.807))),
              Line(Point(self.left(x+0.7), self.top(y+0.807)), Point(self.left(x+0.25), self.top(y+0.807))),
              Line(Point(self.left(x+0.25), self.top(y+0.807)), Point(self.left(x+0.5), self.top(y+0.2))),
          ]
      
      FILL_TYPES = [
          ('A', '_hatch_left'),
          ('B', '_hatch_right'),
          ('C', '_cross_hatch'),
          ('D', '_double_hatch_left'),
          ('E', '_double_hatch_right'),
          ('F', '_double_cross_hatch'),
          ('G', '_triple_hatch_left'),
          ('H', '_triple_hatch_right'),
          ('I', '_triple_cross_hatch'),
          ('J', '_hatch_v'),
          ('K', '_hatch_h'),
          ('L', '_hv_hatch'),
          ('M', '_double_hatch_v'),
          ('N', '_double_hatch_h'),
          ('O', '_double_hv_hatch'),
          ('P', '_triple_hatch_v'),
          ('Q', '_triple_hatch_h'),
          ('R', '_triple_hv_hatch'),
          ('S', '_fill_qmark'),
          ('T', '_fill_trail'),
          ('U', '_fill_small_circle'),
          ('V', '_fill_medium_circle'),
          ('W', '_fill_large_circle'),
          ('X', '_fill_foreground'),
          ('Y', '_fill_triangles'),
          ('Z', '_fill_background'),
      ]
      FILL_CHARACTERS = ''.join([t+t.lower() for (t,f) in FILL_TYPES])
      
      def get_fill(self, character):
          """return fill function"""
          for head, function_name in self.FILL_TYPES:
              if character == head:
                  return getattr(self, function_name)
          raise ValueError('no such fill type')
      
      # - - - - - - - - - fixed characters and theis shapes - - - - - - - - -
      
      def _open_triangle_left(self, x, y):
          return [
              Line(Point(self.left(x), self.vcenter(y)), Point(self.right(x), self.top(y))),
              Line(Point(self.left(x), self.vcenter(y)), Point(self.right(x), self.bottom(y)))
          ]
      def _open_triangle_right(self, x, y):
          return [
              Line(Point(self.right(x), self.vcenter(y)), Point(self.left(x), self.top(y))),
              Line(Point(self.right(x), self.vcenter(y)), Point(self.left(x), self.bottom(y)))
          ]
  
      def _circle(self, x, y):
          return [
              Circle(Point(self.hcenter(x), self.vcenter(y)), NOMINAL_SIZE/2.0)
          ]
  
  
      FIXED_TYPES = [
          ('{', '_open_triangle_left'),
          ('}', '_open_triangle_right'),
          ('*', '_circle'),
      ]
      FIXED_CHARACTERS = ''.join([t for (t,f) in FIXED_TYPES])
      
      def get_fixed_character(self, character):
          """return fill function"""
          for head, function_name in self.FIXED_TYPES:
              if character == head:
                  return getattr(self, function_name)
          raise ValueError('no such character')
      
      # - - - - - - - - - helper function for shape recognition - - - - - - - - -
  
      def _follow_vertical_line(self, x, y):
          """find a vertical line with optional arrow heads"""
          #follow line to the bottom
          _, end_y, line_end_style = self._follow_line(x, y, dy=1, line_character='|')
          #follow line to the top
          _, start_y, line_start_style = self._follow_line(x, y, dy=-1, line_character='|')
          #if a '+' follows a line, then the line is streched to hit the '+' center
          start_y_fix = end_y_fix = 0
          if self.get(x, start_y-1) == '+':
              start_y_fix = -0.5
          if self.get(x, end_y+1) == '+':
              end_y_fix = 0.5
          #tag characters as used (not the arrow heads)
          self.tag([(x, y) for y in range(start_y, end_y+1)], CLASS_LINE)
          #return the new shape object with arrows etc
          p1 = complex(self.hcenter(x), self.top(start_y+start_y_fix))
          p2 = complex(self.hcenter(x), self.bottom(end_y+end_y_fix))
          shapes = []
          if line_start_style:
              p1, arrow_shapes = line_start_style(p1, p2)
              shapes.extend(arrow_shapes)
          if line_end_style:
              p2, arrow_shapes = line_end_style(p2, p1)
              shapes.extend(arrow_shapes)
          shapes.append(Line(p1, p2))
          return group(shapes)
      
      def _follow_horizontal_line(self, x, y, thick=False):
          """find a horizontal line with optional arrow heads"""
          if thick:
              line_character = '='
          else:
              line_character = '-'
          #follow line to the right
          end_x, _, line_end_style = self._follow_line(x, y, dx=1, line_character=line_character)
          #follow line to the left
          start_x, _, line_start_style = self._follow_line(x, y, dx=-1, line_character=line_character)
          start_x_fix = end_x_fix = 0
          if self.get(start_x-1, y) == '+':
              start_x_fix = -0.5
          if self.get(end_x+1, y) == '+':
              end_x_fix = 0.5
          self.tag([(x, y) for x in range(start_x, end_x+1)], CLASS_LINE)
          #return the new shape object with arrows etc
          p1 = complex(self.left(start_x+start_x_fix), self.vcenter(y))
          p2 = complex(self.right(end_x+end_x_fix), self.vcenter(y))
          shapes = []
          if line_start_style:
              p1, arrow_shapes = line_start_style(p1, p2)
              shapes.extend(arrow_shapes)
          if line_end_style:
              p2, arrow_shapes = line_end_style(p2, p1)
              shapes.extend(arrow_shapes)
          shapes.append(Line(p1, p2, thick=thick))
          return group(shapes)
  
      def _follow_lower_horizontal_line(self, x, y):
          """find a horizontal line, the line is aligned to the bottom and a bit
             wider, so that it can be used for shapes like this:
                ___
             __|   |___
          """
          #follow line to the right
          end_x, _, line_end_style = self._follow_line(x, y, dx=1, line_character='_', arrows=False)
          #follow line to the left
          start_x, _, line_start_style = self._follow_line(x, y, dx=-1, line_character='_', arrows=False)
          self.tag([(x, y) for x in range(start_x, end_x+1)], CLASS_LINE)
          #return the new shape object with arrows etc
          p1 = complex(self.hcenter(start_x-1), self.bottom(y))
          p2 = complex(self.hcenter(end_x+1), self.bottom(y))
          return [Line(p1, p2)]
  
      def _follow_line(self, x, y, dx=0, dy=0, line_character=None, arrows=True):
          """helper function for all the line functions"""
          #follow line in the given direction
          while 0 <= x < self.width and 0<= y < self.height and self.get(x+dx, y+dy) == line_character:
              x += dx
              y += dy
          if arrows:
              #check for arrow head
              following_character = self.get(x+dx, y+dy)
              if following_character in self.ARROW_HEADS:
                  line_end_style = self.get_arrow(following_character, dx, dy)
                  if line_end_style:
                      x += dx
                      y += dy
              else:
                  line_end_style = None
          else:
              line_end_style = None
          return x, y, line_end_style
      
      def _plus_joiner(self, x, y):
          """adjacent '+' signs are connected with a line from center to center
             required for images like these:
            
                +---+         The box should be closed on all sides
                |   +--->     and the arrow start should touch the box
                +---+
          """
          result = []
          #~ for dx, dy in ((1,0), (-1,0), (0,1), (0,-1)):
          #looking right and down is sufficient as the scan is done from left
          #to right, top to bottom
          for dx, dy in ((1,0), (0,1)):
              if self.get(x+dx, y+dy) == '+':
                  result.append(Line(
                      Point(self.hcenter(x), self.vcenter(y)),
                      Point(self.hcenter(x+dx), self.vcenter(y+dy))
                  ))
          self.tag([(x,y)], CLASS_JOIN)
          return result
  
  
      def _follow_fill(self, character, start_x, start_y):
          """fill shapes like the ones below with a pattern. when the character is
             upper case, draw a border too.
             
              XXX  aaa  BB
             XXX    a
          """
          fill = self.get_fill(character.upper())
          border = character.isupper()
          result = []
          #flood fill algorithm, searching for similar characters
          coordinates = []
          to_scan = [(start_x, start_y)]
          while to_scan:
              x,y = to_scan.pop()
              if self.cls(x, y) is None:
                  if self.get(x, y) == character:
                      result.extend(fill(x, y))
                      self.tag([(x,y)], CLASS_RECTANGLE)
                  if self.get(x+1, y) == character:
                      if self.cls(x+1, y) is None: to_scan.append((x+1, y))
                  elif border:
                      result.append(Line(Point(self.right(x), self.top(y)), Point(self.right(x), self.bottom(y))))
                  if self.get(x-1, y) == character:
                      if self.cls(x-1, y) is None: to_scan.append((x-1, y))
                  elif border:
                      result.append(Line(Point(self.left(x), self.top(y)), Point(self.left(x), self.bottom(y))))
                  if self.get(x, y+1) == character:
                      if self.cls(x, y+1) is None: to_scan.append((x, y+1))
                  elif border:
                      result.append(Line(Point(self.left(x), self.bottom(y)), Point(self.right(x), self.bottom(y))))
                  if self.get(x, y-1) == character:
                      if self.cls(x, y-1) is None: to_scan.append((x, y-1))
                  elif border:
                      result.append(Line(Point(self.left(x), self.top(y)), Point(self.right(x), self.top(y))))
          return group(result)
              #~ for x in range(start_x, x+1):
                  #~ result.extend(fill(x,i))
          #~ # [Rectangle(
              #~ # Point(self.left(start_x), self.top(start_y)),
              #~ # Point(self.right(x), self.bottom(y)),
          #~ # )]
          #~ for i in range(start_y, y+1):
              #~ self.tag([(x, i) for x in range(start_x, x+1)], CLASS_RECTANGLE)
              #~ for x in range(start_x, x+1):
                  #~ result.extend(fill(x,i))
          #~ return result
          
      def _follow_horizontal_string(self, start_x, y, accept_anything=False, quoted=False):
          """find a string. may contain single spaces, but the detection is
             aborted after more than one space.
             
                Text one   "Text two"
             
             accept_anything means that all non space characters are interpreted
             as text.
          """
          #follow line from left to right
          if quoted:
              quotation_character = self.get(start_x, y)
              x = start_x+1
          else:
              quotation_character = None
              x = start_x
          text = []
          if self.get(x, y) != ' ':
              text.append(self.get(x, y))
              self.tag([(x, y)], CLASS_STRING)
              is_first_space = True
              while 0 <= x+1 < self.width \
                and self.cls(x+1, y) is None:
                  if not quoted:
                      if self.get(x+1, y) == ' ' and not is_first_space:
                          break
                      if not accept_anything and not self.get(x+1, y).isalnum():
                          break
                  x += 1
                  character = self.get(x, y)
                  if character == quotation_character:
                      break
                  text.append(character)
                  if character == ' ':
                      is_first_space = False
                  else:
                      is_first_space = True
              if text[-1] == ' ':
                  del text[-1]
                  x -= 1
              self.tag([(x, y) for x in range(start_x, x+1)], CLASS_STRING)
              return [Label(
                  Point(self.left(start_x), self.bottom(y)),
                  ''.join(text)
              )]
          else:
              return []
  
      def _follow_rounded_edge(self, x, y):
          """check for rounded edges:
              /-    |     -\-    |   and also \    /  etc
              |    -/      |     \-            -  |
          """            
          result = []
          if self.get(x, y) == '/':
              # rounded rectangles
              #XXX return Arc shapes, not Lines
              if (self.get(x+1, y) == '-' and self.get(x, y+1) == '|'):
                  result.append(Line(
                      Point(self.hcenter(x), self.bottom(y)),
                      Point(self.right(x), self.vcenter(y))
                  ))
              if (self.get(x-1, y) == '-' and self.get(x, y-1) == '|'):
                  result.append(Line(
                      Point(self.hcenter(x), self.top(y)),
                      Point(self.left(x), self.vcenter(y))
                  ))
              if not result:
                  # if used as diagonal line
                  p1 = p2 = None
                  if self.get(x+1, y-1) == '|':
                      p1 = Point(self.hcenter(x+1), self.top(y))
                  elif self.get(x+1, y-1) == '+':
                      p1 = Point(self.hcenter(x+1), self.vcenter(y-1))
                  elif self.get(x+1, y-1) == '-':
                      p1 = Point(self.right(x), self.vcenter(y-1))
                  elif self.get(x+1, y-1) == '/':
                      p1 = Point(self.right(x), self.top(y))
                  elif self.get(x+1, y) == '|':
                      p1 = Point(self.hcenter(x+1), self.top(y))
                  elif self.get(x, y-1) == '-':
                      p1 = Point(self.right(x), self.vcenter(y-1))
                      
                  if self.get(x-1, y+1) == '|':
                      p2 = Point(self.hcenter(x-1), self.top(y+1))
                  elif self.get(x-1, y+1) == '+':
                      p2 = Point(self.hcenter(x-1), self.vcenter(y+1))
                  elif self.get(x-1, y+1) == '-':
                      p2 = Point(self.left(x), self.vcenter(y+1))
                  elif self.get(x-1, y+1) == '/':
                      p2 = Point(self.left(x), self.bottom(y))
                  elif self.get(x-1, y) == '|':
                      p2 = Point(self.hcenter(x-1), self.bottom(y))
                  elif self.get(x, y+1) == '-':
                      p2 = Point(self.left(x), self.vcenter(y+1))
                  
                  if p1 or p2:
                      if not p1:
                          p1 = Point(self.right(x), self.top(y))
                      if not p2:
                          p2 = Point(self.left(x), self.bottom(y))
                      result.append(Line(p1, p2))
          else: # '\'
              # rounded rectangles
              if (self.get(x-1, y) == '-' and self.get(x, y+1) == '|'):
                  result.append(Line(
                      Point(self.hcenter(x), self.bottom(y)),
                      Point(self.left(x), self.vcenter(y))
                  ))
              if (self.get(x+1, y) == '-' and self.get(x, y-1) == '|'):
                  result.append(Line(
                      Point(self.hcenter(x), self.top(y)),
                      Point(self.right(x), self.vcenter(y))
                  ))
              if not result:
                  # if used as diagonal line
                  p1 = p2 = None
                  if self.get(x-1, y-1) == '|':
                      p1 = Point(self.hcenter(x-1), self.top(y))
                  elif self.get(x-1, y-1) == '+':
                      p1 = Point(self.hcenter(x-1), self.vcenter(y-1))
                  elif self.get(x-1, y-1) == '-':
                      p1 = Point(self.left(x), self.vcenter(y-1))
                  elif self.get(x-1, y-1) == '\\':
                      p1 = Point(self.left(x), self.top(y))
                  elif self.get(x-1, y) == '|':
                      p1 = Point(self.hcenter(x-1), self.top(y))
                  elif self.get(x, y-1) == '-':
                      p1 = Point(self.left(x), self.hcenter(y-1))
                      
                  if self.get(x+1, y+1) == '|':
                      p2 = Point(self.hcenter(x+1), self.top(y+1))
                  elif self.get(x+1, y+1) == '+':
                      p2 = Point(self.hcenter(x+1), self.vcenter(y+1))
                  elif self.get(x+1, y+1) == '-':
                      p2 = Point(self.right(x), self.vcenter(y+1))
                  elif self.get(x+1, y+1) == '\\':
                      p2 = Point(self.right(x), self.bottom(y))
                  elif self.get(x+1, y) == '|':
                      p2 = Point(self.hcenter(x+1), self.bottom(y))
                  elif self.get(x, y+1) == '-':
                      p2 = Point(self.right(x), self.vcenter(y+1))
                  
                  if p1 or p2:
                      if not p1:
                          p1 = Point(self.left(x), self.top(y))
                      if not p2:
                          p2 = Point(self.right(x), self.bottom(y))
                      result.append(Line(p1, p2))
          if result:
              self.tag([(x,y)], CLASS_JOIN)
          return group(result)
  
  
  if __name__ == '__main__':
      import pprint
      import svg
      import aa
      import optparse
  
      def decode_color(color_string):
          if color_string[0] == '#':          #HTML like color syntax
              if len(color_string) == 4:      # #rgb format
                  r,g,b = [int(c+c, 16) for c in color_string[1:]]
              elif len(color_string) == 7:      # #rrggbb format
                  r,g,b = [int(color_string[n:n+2], 16) for n in range(1, len(color_string), 2)]
              else:
                  raise ValueError('not a valid color: %r' % color_string)
          #XXX add a list of named colors
          return r,g,b
  
      parser = optparse.OptionParser(
          usage="%prog [options] file1 [file2 ...]"
      )
      
      parser.add_option("-t", "--type", dest="type",
          help="filetype: png, jpg, svg", default='svg')
  
      parser.add_option("-D", "--debug", dest="debug", action="store_true",
          help="enable debug outputs", default=False)
  
      parser.add_option("-s", "--scale", dest="scale", action="store", type='float',
          help="set scale", default=1)
  
      parser.add_option("-a", "--aspect", dest="aspect", action="store", type='float',
          help="set aspect ratio", default=1)
  
      parser.add_option("-l", "--linewidth", dest="linewidth", action="store", type='float',
          help="set width, svg only", default=2)
  
      parser.add_option("-f", "--foreground", dest="foreground", action="store",
          help="foreground color default=#000000", default='#000000')
  
      parser.add_option("-x", "--fill", dest="fill", action="store",
          help="foreground color default=#000000", default='#000000')
  
      parser.add_option("-b", "--background", dest="background", action="store",
          help="foreground color default=#ffffff", default='#ffffff')
  
      (options, args) = parser.parse_args()
  
      if len(args) < 1:
          parser.error("filename required")
  
  
      #~ def render(text):
          #~ """helper function for tests. scan the given image and create svg output"""
          #~ aaimg = AsciiArtImage(text)
          #~ print text
          #~ aaimg.recognize()
          #~ aav = aa.AsciiOutputVisitor()
          #~ pprint.pprint(aaimg.shapes)
          #~ aav.visit(aaimg)
          #~ print aav
          #~ svgout = svg.SVGOutputVisitor(
              #~ file('aafigure_%x.svg' % (long(hash(text)) & 0xffffffffL,), 'w'),
              #~ scale = 10
          #~ )
          #~ svgout.visit(aaimg)
  
      #~ aaimg = AsciiArtImage("""
          #~ ---> | ^|   |
          #~ <--- | || --+--
          #~ <--> | |V   |
       #~ __             __
      #~ |  |__  +---+  |__|
              #~ |box|   ..
              #~ +---+  Xenophon
      #~ """)
      #~ print aaimg
      #~ aaimg.recognize()
      #~ print "%r" % aaimg
      #~ aav = aa.AsciiOutputVisitor()
      #~ aav.visit(aaimg)
      #~ print aav
  
      for filename in args:
          aaimg = AsciiArtImage(file(filename).read())
          print aaimg
          aaimg.recognize()
  
          output_name = filename + '.' + options.type
          if options.type.lower() == 'svg':
              pilout = svg.SVGOutputVisitor(
                  file(output_name, 'w'),
                  scale = options.scale*7,
                  line_width = options.linewidth,
                  #~ debug = options.debug,
                  foreground = decode_color(options.foreground),
                  background = decode_color(options.background),
                  fillcolor = decode_color(options.fill),
              )
              pilout.visit_image(aaimg)
          elif options.type.lower() == 'pdf':
              import pdf
              doc = pdf.PDFOutputVisitor(
                  file(output_name, 'wb'),
                  scale = options.scale,
                  line_width = options.linewidth,
                  #~ debug = options.debug,
                  foreground = decode_color(options.foreground),
                  background = decode_color(options.background),
                  fillcolor = decode_color(options.fill),
              )
              doc.visit_image(aaimg)
          else:
              import pil
              pilout = pil.PILOutputVisitor(
                  file(output_name, 'wb'),
                  scale = options.scale*7,
                  line_width = options.linewidth,
                  debug = options.debug,
                  file_type = options.type,
                  foreground = options.foreground,
                  background = options.background,
                  fillcolor = options.fill,
              )
              pilout.visit_image(aaimg)
  
  