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()