import os import sys from collections import namedtuple Attribute = namedtuple('Attribute', ['fg', 'bg', 'bold']) WIDTH = 80 HEIGHT = 25 # ANSI orders the colours black, red, green, yellow, blue, magenta, cyan, white # VGA orders them black, blue, green, cyan, red, magenta, yellow, white color_map = [0, 4, 2, 6, 1, 5, 3, 7] if len(sys.argv) != 7: print("Usage: {sys.argv[0]} outfile infile default_fgcolor default_bgcolor origin_x origin_y", file=sys.stderr) sys.exit(1) blinky_vgamode = False if 'BUILDOPTS' in os.environ: for opt in os.environ['BUILDOPTS'].split(): if opt == '-DBLINKY': blinky_vgamode = True else: print(f"Error: Unrecognized build option {opt}", file=sys.stderr) sys.exit(1) outfile = sys.argv[1] infile = sys.argv[2] default_fgcolor = int(sys.argv[3]) assert 0 <= default_fgcolor <= 15 default_bgcolor = int(sys.argv[4]) assert 0 <= default_bgcolor <= 15 origin_x = int(sys.argv[5]) assert 0 <= origin_x < WIDTH origin_y = int(sys.argv[6]) assert 0 <= origin_y < HEIGHT chars = [bytearray([0]*HEIGHT) for _ in range(WIDTH)] attributes = [[(Attribute(default_fgcolor, default_bgcolor, False))]*HEIGHT for _ in range(WIDTH)] with open(infile, 'rb') as f: ansitext = f.read() x = origin_x y = origin_y fgcolor = default_fgcolor bgcolor = default_bgcolor bold = False error = False line = 1 line_start = 0 index = 0 while index < len(ansitext): if ansitext[index:].startswith(b'\x1b['): index += len(b'\x1b[') escape_start = index while index < len(ansitext) and ansitext[index] not in b'CDHJhm': index += 1 escape_params = ansitext[escape_start:index] escape_type = ansitext[index:index+1] index += 1 if escape_type == b'C': x += int(escape_params) elif escape_type == b'D': x -= int(escape_params) elif escape_type == b'H': row, column = escape_params.split(b';') y = int(row) - 1 x = int(column) - 1 elif escape_type == b'J': # Erase display # We just ignore this, because why would you have # erase command anywhere but at the start pass elif escape_type == b'h': # Something nonstandard, ignore pass elif escape_type == b'm': for color_parameter in escape_params.split(b';'): color_parameter = int(color_parameter) if color_parameter == 0: bold = False elif color_parameter == 1: bold = True elif color_parameter == 39: fgcolor = default_fgcolor elif color_parameter == 49: bgcolor = default_bgcolor elif 30 <= color_parameter <= 37: fgcolor = color_map[color_parameter - 30] elif 40 <= color_parameter <= 47: bgcolor = color_map[color_parameter - 40] elif 90 <= color_parameter <= 97: fgcolor = color_map[color_parameter - 90] + 8 elif 100 <= color_parameter <= 107: bgcolor = color_map[color_parameter - 100] + 8 else: print(f'{line},{escape_start-line_start+1}: Unknown colour escape {color_parameter}') error = True else: print(f'{line},{escape_start-line_start+1}: Unknown escape ^[[{escape_params.decode()}{escape_type.decode()}') error = True elif ansitext[index] == 13: x = origin_x index += 1 elif ansitext[index] == 10: for i in range(x, WIDTH): attributes[i][y] = Attribute(fgcolor, bgcolor, bold) x = origin_x y += 1 index += 1 line += 1 line_start = index else: chars[x][y] = ansitext[index] attributes[x][y] = Attribute(fgcolor, bgcolor, bold) index += 1 x += 1 if error: sys.exit(1) with open(outfile, 'wb') as f: for y in range(HEIGHT): for x in range(WIDTH): fgcolor, bgcolor, bold = attributes[x][y] fgcolor = fgcolor | (8 if bold else 0) if blinky_vgamode: # VGA mode can be set up so that the highest # bit of the bg colour marks that the cell # should blink instead of intensity # Restrict the colours (including foreground # to account for swapping of the two) to 0…7 fgcolor = fgcolor & 0x7 bgcolor = bgcolor & 0x7 char = chars[x][y] f.write(bytes([char, (bgcolor<<4) | fgcolor]))