Source code for ascii_art

"""
This module allows you to define simple images with ascii art instead of 
using a bitmap editor tool and working with binary file resources.

For example:

|  images = \'\'\'
|  /.=(0,0,0) #=(255,255,255) %=(255,0,0)
|  ..#..
|  .#.#. 
|  #...# 
|  ..%.. 
|  ..%..
|  \'\'\'

The first line starting with "/" maps ascii art characters to colors. In this
case the '.' becomes BLACK, '#' becomes WHITE, and '%' becomes RED. You can also 
use the color constant names from the 'colors' module instead of the RGB tuple.

The lines that follow define a simple 5x5 pixel image that can be copied into 
a window for a background or used as the image of a sprite.

Here is a complex example demonstrating all the art features:

|  images = \'\'\'
|  /.=TRANSPARENT #=WHITE %=RED
|  ..#.. | /0y..
|  .#.#. | .....
|  #...# | .....
|  ..%.. | .....
|  ..%.. | .....
|  _____________
|  /%=GREEN
|  ..#.. | /0x..
|  .#... | .....
|  #..%% | .....
|  .#... | .....
|  ..#.. | .....
|  \'\'\'

Multiple images can be drawn in the same ascii art string. Images on a row are separated
by a space ' '. Rows of images are separated by a blank line. The '|' and '_' characters
are optional; they are ignored. You can use them for added visual separation.

Rows of images do not have to be the same length as they are here. One row can have more
images than another row.

All the images on a single row must be the same width and height. Different rows can be
different width/height.

The first line of each row can change the color mapping. The new values remain in effect
on future rows unless changed by a later row. In this example, the '%' is mapped to GREEN 
in all images on the second row.

If the upper left corner of an image is '/', then the image is a mirror of another image.
On the first row, the second image begins with '/0y'. This indicates that the image
is a copy of image '0' on that row (the first image on the row) but is mirrored on the Y axis.

The second image on the second row is a copy of image 0 (the first) image on that row but
mirrored on the X axis.

You can mirror both directions at once. For instance '/0xy'.

"""

from bitmap import Bitmap
import colors


def _hex_bin(val):
    val = val.strip()
    if val.startswith('0x'):
        return int(val[2:], 16)
    return int(val)


def _decode_art(text, color_map=None):
    '''Convert a sheet of ascii art images into a list of Bitmaps

    Args:
        text (str): The ascii-art string.
        color_map (dict, optional): Mapping of characters to colors

    Returns:
        list: The created Bitmap objects.
    '''
    # TODO better error checking and reporting

    # Remove all "_" and "|". They are just for viewing
    text = text.replace('_', ' ')
    text = text.replace('|', ' ')

    # Split by lines and trim off leading/trailing spaces
    lines = text.split('\n')
    while not lines[0].strip():
        lines = lines[1:]
    while not lines[-1].strip():
        lines = lines[:-1]

    # Separate the lines into rows of images
    image_rows = [[]]
    for line in lines:
        line = line.strip()
        if line:
            image_rows[-1].append(line)
        else:
            if image_rows[-1]:
                # This ignores multiple blank lines
                image_rows.append([])

    # We migh start with a given character mapping
    if not color_map:
        color_map = {}

    images = []
    image_row_map_chars = []

    # By default, '.' maps to transparent and '#' to white
    cmap = {
        '.': colors.TRANSPARENT,
        '#': (255, 255, 255)
    }

    for image_row in image_rows:
        while image_row[0].startswith('/'):
            desc = image_row[0]
            desc = desc[1:].split()
            image_row = image_row[1:]
            for e in desc:
                if e[1] != '=':
                    raise Exception('Invalid character mapping "'+str(e)+'"')
                try:
                    if e[2] == '(':
                        # This is an absolute tuple
                        r, g, b = e[3:-1].split(',')
                        r = _hex_bin(r)
                        g = _hex_bin(g)
                        b = _hex_bin(b)
                        cmap[e[0]] = (r, g, b)
                    else:
                        # This is a constant from colors
                        cmap[e[0]] = colors.COLORS_BY_NAME[e[2:].strip()]
                except Exception:
                    raise Exception('Invalid character mapping "'+str(e)+'"')

        # The passed-in map gets priority
        # cmap = {**cmap, **color_map} -- not in micropython
        for k, v in color_map.items():
            cmap[k] = v
            # cmap[ord(k)] = v

        # This rows color mapping (used later when we build the grids)
        image_row_map_chars.append(cmap)

        # Build the 2D array of images from the row of images
        ir = []
        image_width = 0
        num_cols = 0
        for row in image_row:
            # Process the image columns on the row

            # Remove repeated whitespace
            #row = ' '.join(row.split())

            image_cols = row.split()
            if num_cols:
                if len(image_cols) != num_cols:
                    raise Exception('All images on a row must be the same height: '+row)
            else:
                num_cols = len(image_cols)
                for i in range(num_cols):
                    ir.append([])

            for i in range(num_cols):
                col = image_cols[i]
                if image_width:
                    if image_width != len(col):
                        raise Exception('All images on a row must be the same width: '+row)
                else:
                    image_width = len(col)

                ir[i].append(col)

        images.append(ir)

    # Handle mirroring, rotation, etc
    for row_num in range(len(images)):
        row = images[row_num]
        for col_num in range(len(row)):
            im = row[col_num]
            if im[0].startswith('/'):
                # These are special operators to reuse existing images on the row
                col = im[0][1:].replace('.', ' ').strip()
                mx = False
                my = False
                tg = 0
                for c in col.upper():
                    if c == 'X':
                        mx = True
                    elif c == 'Y':
                        my = True
                    else:
                        try:
                            tg = tg * 10 + int(c)
                        except Exception:
                            raise Exception('Invalid mirroring character: '+c)
                # This is the target image on the row to mirror
                if tg >= col_num:
                    raise Exception('Invalid image target: '+str(tg))
                tg = row[tg]
                if my:
                    l = len(tg)-1
                    for j in range(len(tg)-1, -1, -1):
                        if mx:
                            row[col_num][l-j] = ''.join(reversed(tg[j]))
                        else:
                            row[col_num][l-j] = tg[j]
                else:
                    for j in range(len(tg)):
                        if mx:
                            row[col_num][j] = ''.join(reversed(tg[j]))
                        else:
                            row[col_num][j] = tg[j]

    return (images, image_row_map_chars)


[docs]def get_images(text, transparent_color=colors.TRANSPARENT, scale=1, color_map=None): '''Convert a sheet of ascii art images into a list of Bitmaps Args text (str): The ascii-art string. transparent_color (tuple, optional): The color value to treat as transparent. Default is colors.TRANSPARENT. scale (int): Pixel scaling factor color_map (dict, optional): Mapping of characters to colors Returns: list: The created Bitmap objects. ''' images, image_row_map_chars = _decode_art(text, color_map) ret = [] for row_num in range(len(images)): row = images[row_num] map_chars = image_row_map_chars[row_num] for col in row: bm = Bitmap.from_ascii(col, transparent_color, scale, map_chars) ret.append(bm) return ret