asc2svg

asc2svg was intended to be an ASCII diagrams to SVG, like `ditaa` and Svgbob
Log | Files | Refs

a2svg.py (16760B)


      1 #!/bin/python
      2 
      3 font_size = 12
      4 x_scaling = 0.5
      5 
      6 
      7 class Figure:
      8     def __init__(self, ascii_text):
      9         self._elements = []
     10         self._ascii_text = [line.strip('\n') for line in ascii_text]
     11         self._height = len(ascii_text)
     12         self._width = max([len(line) for line in ascii_text])
     13 
     14         # NEW KNOWLEDGE: Using [[' ']*width]*height just makes `height` number of *copies* (!) of the inner list
     15         # In other words, every change you make to one of the lists will be applied to *all* of them.. :(
     16         # Using append instead, to avoid this
     17 
     18         # Make a '2d list' to keep track of processed characters
     19         self._processed = [[False]*self._width]
     20         for y in range(self._height - 1):
     21             self._processed.append([False]*self._width)
     22 
     23         # Make a '2d list' of the asci figure
     24         self._figarray = [[' ']*self._width]
     25         for y in range(self._height - 1):
     26             self._figarray.append([' ']*self._width)
     27 
     28         for y, line in enumerate(self._ascii_text):
     29             for x, cell in enumerate(line):
     30                 self._figarray[y][x] = cell
     31 
     32     def process_all(self):
     33         # Process rectangles first
     34         for y in range(self._height):
     35             for x in range(self._width):
     36                 if not self._processed[y][x]:
     37                     rects = self.rectangle(y, x)
     38                     if rects:
     39                         self._elements += rects
     40 
     41         # then process line. (VERY TEMPORARY SOLUTION)
     42         for y in range(self._height):
     43             for x in range(self._width):
     44                 if not self._processed[y][x]:
     45                     self.process_char(y, x)
     46 
     47     def get_elements(self):
     48         return self._elements
     49 
     50     def get_array(self):
     51         return self._figarray
     52 
     53     def process_char(self, y, x):
     54         new_elements = None
     55         symbol = self._figarray[y][x]
     56         if symbol == '+':
     57             rects = self.rectangle(y, x)
     58             if rects:
     59                 # TODO: Refactor this text extraction
     60                 for r in rects:
     61                     r.extract_text(self)
     62                 new_elements = rects
     63             # If not a rectangle, it must be a line joint
     64             else:
     65                 new_elements = self.polyline(y, x, "FIND")
     66         elif symbol == '^':
     67             new_elements = self.polyline(y, x, "DOWN")
     68             new_elements.append(Arrow('^', (y, x)))
     69         elif symbol == '<':
     70             new_elements = self.polyline(y, x, "RIGHT")
     71             new_elements.append(Arrow('<', (y, x)))
     72 
     73         if new_elements:
     74             self._elements += new_elements
     75         self._processed[y][x] = True
     76 
     77     def __str__(self):
     78         figure_string = ""
     79         for row in self._figarray:
     80             for cell in row:
     81                 figure_string += cell
     82             figure_string += '\n'
     83         return figure_string
     84 
     85     def __has_char(self, y, x, char):
     86         if y < 0 or y >= self._height:
     87             return False
     88         if x < 0 or x >= self._width:
     89             return False
     90         # Processed characters should not be considered
     91         if self._processed[y][x]:
     92             return False
     93         if self._figarray[y][x] == char:
     94             return True
     95         return False
     96 
     97     def __joint_in(self, y, x):
     98         return self.__has_char(y, x, '+')
     99 
    100     def __arrow_in(self, y, x):
    101         if self.__has_char(y, x, '<'):
    102             return True
    103         if self.__has_char(y, x, '>'):
    104             return True
    105         if self.__has_char(y, x, '^'):
    106             return True
    107         if self.__has_char(y, x, 'v'):
    108             return True
    109         return False
    110 
    111     """
    112     Finds all lines in a polyline recursively
    113     MAYBE: The best option is to just use SVG 'Line'. Polylines do not handle
    114     crossing lines well, I think.  (So TODO : refactor name)
    115     """
    116     # TODO: Refactor this mess entirely
    117     def polyline(self, y, x, direction):
    118         # TODO : Deal with arrows. Only one arrow is allowed for each direction, so this shouldn't be too hard
    119         # Find lines
    120         line_list = []
    121         # TODO: No reason for this horrible recursive mess! Just make a function
    122         # for all the directions.
    123         if direction == "FIND":
    124             if self.__has_char(y+1, x, '|'):
    125                 line_list += self.polyline(y, x, "DOWN")
    126             if self.__has_char(y-1, x, '|'):
    127                 line_list += self.polyline(y, x, "UP")
    128             if self.__has_char(y, x-1, '-'):
    129                 line_list += self.polyline(y, x, "LEFT")
    130             if self.__has_char(y, x+1, '-'):
    131                 line_list += self.polyline(y, x, "RIGHT")
    132             return line_list
    133 
    134         elif direction == "LEFT":
    135             x_test = x - 1
    136             while self.__has_char(y, x_test, '-'):
    137                 self._processed[y][x_test] = True
    138                 x_test -= 1
    139             if self.__arrow_in(y, x_test):
    140                 line_list.append(Line((y, x_test - 1), (y, x)))
    141                 line_list.append(Arrow('<', (y, x_test)))
    142                 self._processed[y][x_test]
    143             elif self.__joint_in(y, x_test):
    144                 line_list.append(Line((y, x_test), (y, x)))
    145                 self._processed[y][x_test]
    146             else:
    147                 line_list.append(Line((y, x_test + 1), (y, x)))
    148             x = x_test
    149         elif direction == "RIGHT":
    150             x_test = x
    151             if self.__has_char(y, x_test, '<'):
    152                 self._processed[y][x_test] = True
    153                 x -= 1
    154                 x_test += 1
    155             x_test += 1
    156             while self.__has_char(y, x_test, '-'):
    157                 self._processed[y][x_test] = True
    158                 x_test += 1
    159             if self.__arrow_in(y, x_test):
    160                 line_list.append(Line((y, x), (y, x_test + 1)))
    161                 line_list.append(Arrow('>', (y, x_test)))
    162             elif self.__joint_in(y, x_test):
    163                 line_list.append(Line((y, x), (y, x_test)))
    164             else:
    165                 line_list.append(Line((y, x), (y, x_test - 1)))
    166             x = x_test
    167 
    168         # WORKING MESS
    169         elif direction == "DOWN":
    170             y_test = y
    171             if self.__has_char(y_test, x, '^'):
    172                 self._processed[y_test][x] = True
    173                 y -= 1
    174                 y_test = y + 1
    175             y_test += 1
    176             while self.__has_char(y_test, x, '|'):
    177                 self._processed[y_test][x] = True
    178                 y_test += 1
    179             if self.__arrow_in(y_test, x):
    180                 line_list.append(Line((y, x), (y_test+1, x)))
    181                 line_list.append(Arrow('v', (y_test, x)))
    182             elif self.__joint_in(y_test, x):
    183                 line_list.append(Line((y, x), (y_test, x)))
    184             else:
    185                 line_list.append(Line((y, x), (y_test-1, x)))
    186             y = y_test
    187         else:
    188             pass
    189 
    190         # If the line we're testing has a '+' char, find new lines recursively
    191         if self.__has_char(y, x, '+'):                     # This should deffo be moved to the bottom
    192             line_list += self.polyline(y, x, "FIND")
    193 
    194         return line_list
    195 
    196 
    197 
    198     """
    199     Checks if a joint belongs to a rectangle (and if there are subrectangles)
    200     If it does, the rectangles are added as an element, and all associated characters
    201     are registered as processed, *except* joints on the rectangles.
    202     """
    203     def rectangle(self, y_start, x_start):
    204         # Temporary list of processed chars. Must dismiss if this is not a rectangle
    205         processed = [[y_start, x_start]]
    206         process_later = []
    207         if x_start + 1 >= self._width:
    208             return None
    209 
    210         # TODO: Refactor this into helper method 'find_y_start'?
    211         # 1. Find the vertical stop of the rectangle, if any
    212         y = y_start + 1
    213         y_stop = None
    214         while not y_stop and y < self._height:
    215             cell = self._figarray[y][x_start]
    216             if cell == '|':
    217                 processed.append([y, x_start])
    218                 y += 1
    219             elif cell == '+':
    220                 # If '-' to the right, this should be the y_stop
    221                 if self._figarray[y][x_start+1] == '-':
    222                     y_stop = y
    223                     processed.append([y, x_start])
    224                     break
    225                 # If not, the '+' might be a connection to a line on the left
    226                 process_later.append([y, x_start])
    227                 y += 1
    228             else:
    229                 break
    230 
    231         if not y_stop:
    232             return None
    233 
    234         # TODO: Clean up
    235         # 2. Find the horizontal stop, if any
    236         x_stop = None
    237         x = x_start + 1
    238         while not x_stop and x < self._width:
    239             upper_cell = self._figarray[y_start][x]
    240             lower_cell = self._figarray[y_stop][x]
    241             if upper_cell == '+' and lower_cell == '+':
    242                 x_stop = x
    243                 # Check that there's a line from upper cell to lower
    244                 for y_test in range(y_start+1, y_stop):
    245                     cell = self._figarray[y_test][x]
    246                     if cell == '+':
    247                         process_later.append([y_test, x])
    248                     elif cell != '|':
    249                         x_stop = None
    250                         break
    251                 if x_stop:
    252                     break
    253                 # If x_stop is *not* defined, we probably found an x where both 'y's are connected to lines,
    254                 # so we need to continue and add them
    255             if upper_cell != '-' and upper_cell != '+':
    256                 break
    257             if lower_cell != '-' and lower_cell != '+':
    258                 break
    259             if upper_cell == '+':
    260                 process_later.append([y_start, x])
    261             if lower_cell == '+':
    262                 process_later.append([y_stop, x])
    263             x += 1
    264 
    265         if not x_stop:
    266             return None
    267 
    268         # By this point, we know we have a rectangle, so we can register the
    269         # entire section as processed (except the bottom line, in case of
    270         # sub-rectangles)
    271         # TODO : Make Pythonic. (That goes for a lot of this code, though)
    272         for y in range(y_start, y_stop):
    273             for x in range(x_start, x_stop+1):
    274                 if [y, x] not in process_later:
    275                     self._processed[y][x] = True
    276 
    277         new_rect = Rectangle((x_start, y_start), (x_stop, y_stop))
    278         new_rect.extract_text(self)
    279         rect_list = [new_rect]
    280         # Check if we're dealing with a rectangle chain
    281         sub_rects = self.rectangle(y_stop, x_start)
    282         if sub_rects:
    283             rect_list += sub_rects
    284         # If not, we can register the bottom line as processed
    285         else:
    286             for x in range(x_start, x_stop+1):
    287                 if [y_stop, x] not in process_later:
    288                     self._processed[y_stop][x] = True
    289 
    290         return rect_list
    291 
    292     """Return SVG text of the entire figure"""
    293     def get_svg(self):
    294         svg_text = ""
    295         for el in self._elements:
    296             svg_text += el.draw() + "\n"
    297         return svg_text
    298 
    299 
    300 class Rectangle:
    301     def __init__(self, pos_start, pos_stop):
    302         (self._x_start, self._y_start) = pos_start
    303         (self._x_stop, self._y_stop) = pos_stop
    304         self._text_lines = [None]
    305 
    306     def extract_text(self, fig):
    307         self._text_lines = []
    308         figarray = fig.get_array()
    309         for y in range(self._y_start+1, self._y_stop):
    310             line = ''.join(figarray[y][self._x_start+1:self._x_stop])
    311             self._text_lines.append(line)
    312 
    313     def __str__(self):
    314         return_string = "Rect, ({}, {}) to ({}, {}):".format(
    315                      self._y_start,
    316                      self._x_start,
    317                      self._y_stop,
    318                      self._x_stop,
    319                      )
    320         for line in self._text_lines:
    321             return_string += line + "\n"
    322 
    323         return return_string
    324 
    325     def draw(self):
    326         svg_text = "\t<rect x=\"{}\" y=\"{}\" ".format(
    327                 x_scaling * self._x_start * font_size,
    328                 self._y_start * font_size)
    329         rect_width = (self._x_stop - self._x_start) * font_size
    330         rect_height = (self._y_stop - self._y_start) * font_size
    331         svg_text += "width=\"{}\" height=\"{}\" ".format(
    332                 x_scaling * rect_width,
    333                 rect_height)
    334         svg_text += "fill=\"white\" stroke=\"black\"></rect>\n"
    335 
    336         for ln, line in enumerate(self._text_lines):
    337             svg_text += "\t<text x=\"{}\" y=\"{}\" font-family=\"Arial\" font-size=\"{}\" fill=\"black\">{}</text>\n".format(
    338                         x_scaling * self._x_start*font_size+15,
    339                         (self._y_start+ln)*font_size+15,
    340                         font_size,
    341                         line
    342                         )
    343 
    344         return svg_text
    345 
    346 
    347 class Text:
    348     pass
    349 
    350 
    351 # Not sure if Joint is useful in this code
    352 class Joint:
    353     def __init__(self, position):
    354         # (pos should be a tuple)
    355         (self._x, self._y) = position
    356         pass
    357 
    358     def get_position(self):
    359         return (self._x, self._y)
    360 
    361 
    362 # ~~The best alternative here is to use a *polyline*~~ nah
    363 #   https://www.w3schools.com/graphics/svg_polyline.asp
    364 class Line:
    365     def __init__(self, pos_start, pos_stop):
    366         (self._y_start, self._x_start) = pos_start
    367         (self._y_stop, self._x_stop) = pos_stop
    368 
    369     def draw(self):
    370         svg_text = "\t<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" ".format(
    371             x_scaling * self._x_start * font_size,
    372             self._y_start * font_size,
    373             x_scaling * self._x_stop * font_size,
    374             self._y_stop * font_size)
    375         svg_text += "style=\"stroke:rgb(0,0,0);stroke-width:1\" />"
    376         return svg_text
    377 
    378     def __str__(self):
    379         text = "Line, ({}, {}) to ({}, {})\n".format(
    380             self._y_start,
    381             self._x_start,
    382             self._y_stop,
    383             self._x_stop
    384         )
    385         return text
    386 
    387 
    388 # TODO : Replace all of the string representations and comparison with 'enums'
    389 class Arrow():
    390     def __init__(self, symbol, position):
    391         self._y, self._x = position
    392         self._direction = symbol
    393         self._dir_func_map = {
    394                 'v': self.__gen_coords_down,
    395                 '^': self.__gen_coords_up,
    396                 '<': self.__gen_coords_left,
    397                 '>': self.__gen_coords_right,
    398             }
    399 
    400     # TODO : These *can* be replaced by class methods that take x and y as
    401     # arguments instead? Not sure if one is better than the other...
    402     # More important TODO : Use some brain power to make this prettier (by
    403     # rotating arrows and setting an offset or something, instead of copy-pasting
    404     # functions and changing 'manually')
    405     def __gen_coords_down(self):
    406             return [
    407             x_scaling * self._x * font_size,
    408             (self._y+1) * font_size - 1,
    409             x_scaling * (self._x-1) * font_size,
    410             self._y * font_size,
    411             x_scaling * (self._x+1) * font_size,
    412             self._y * font_size ]
    413 
    414     def __gen_coords_up(self):
    415             return [
    416             x_scaling * self._x * font_size,
    417             (self._y-1) * font_size - 1,
    418             x_scaling * (self._x-1) * font_size,
    419             self._y * font_size,
    420             x_scaling * (self._x+1) * font_size,
    421             self._y * font_size ]
    422 
    423     def __gen_coords_left(self):
    424             return [
    425             x_scaling * self._x * font_size,
    426             (self._y+1) * font_size - 1,
    427             x_scaling * (self._x-1) * font_size,
    428             self._y * font_size,
    429             x_scaling * (self._x+1) * font_size,
    430             self._y * font_size ]
    431 
    432     def __gen_coords_right(self):
    433             return [
    434             x_scaling * self._x * font_size,
    435             (self._y+1) * font_size - 1,
    436             x_scaling * (self._x-1) * font_size,
    437             self._y * font_size,
    438             x_scaling * (self._x+1) * font_size,
    439             self._y * font_size ]
    440 
    441     def draw(self):
    442         try:
    443             svg_text = "\t<polygon points=\"{},{} {},{} {},{}\" ".format(
    444                     *self._dir_func_map[self._direction]())
    445             svg_text += "style=\"stroke:rgb(0,0,0);stroke-width:1\" />"
    446             return svg_text
    447         except Exception as e:
    448             print("Invalid arrow detected")
    449             print(e)
    450             return ""
    451 
    452     def __str__(self):
    453         text = "Arrow, {}, ({}, {})\n".format(
    454             self._direction,
    455             self._x,
    456             self._y,
    457         )
    458         return text
    459 
    460 ### TESTING ###
    461 
    462 with open("ascii.txt") as f:
    463     ascii_text = f.readlines()
    464     f.close()
    465 
    466 drawing = Figure(ascii_text)
    467 print(drawing)
    468 drawing.process_all()
    469 for el in drawing.get_elements():
    470     print(el)
    471 
    472 svg_text = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox = \"0 0 500 500\">\n"
    473 svg_text += drawing.get_svg()
    474 svg_text += "</svg>"
    475 
    476 with open("test.svg", "w+") as f:
    477     f.write(svg_text)
    478     f.close()