diff --git a/ascii.py b/ascii.py index 81620f4..4732105 100755 --- a/ascii.py +++ b/ascii.py @@ -12,6 +12,7 @@ import numpy as np import numpngw ASCII_CHARS = "$@%#*+=-:. " +BRAIL_CHARS = "⠿⠾⠼⠸⠰⠠ " HEADERS = {'User-Agent': 'Gimme ascii.'} @@ -26,43 +27,51 @@ def scale_image(image, maxDim=100): """ original_width, original_height = image.size original_width = original_width * 2 - if original_width > original_height: - if original_width > maxDim: - new_width = maxDim - aspect_ratio = original_height/float(original_width) - new_height = int(aspect_ratio * new_width) - else: - new_width, new_height = image.size + if original_width <= maxDim and original_height <= maxDim: + new_width, new_height = image.size + elif original_width > original_height: + new_width = maxDim + aspect_ratio = original_height/float(original_width) + new_height = int(aspect_ratio * new_width) else: - if original_height > maxDim: - new_height = maxDim - aspect_ratio = original_width/float(original_height) - new_width = int(aspect_ratio * new_height) - else: - new_width, new_height = image.size + new_height = maxDim + aspect_ratio = original_width/float(original_height) + new_width = int(aspect_ratio * new_height) image = image.resize((new_width, new_height)) return image -def pixels_to_chars(image, reverse=False): +def pixels_to_chars(image, scale="ascii", color_code=None): """ Maps each pixel to an ascii char based on where it falls in the range - 0-255 normalized to the length of ASCII_CHARS. + 0-255 normalized to the length of the chosen scale. """ - range_width = int(255 / len(ASCII_CHARS)) + (255 % len(ASCII_CHARS) > 0) + scales = {"ascii": ASCII_CHARS, + "ascii_reverse": "".join(reversed(ASCII_CHARS)), + "brail": BRAIL_CHARS, + "brail_reverse": "".join(reversed(BRAIL_CHARS))} + + color_prefix = {"irc": "\03", "ansi":"\033"} + + range_width = int(255 / len(scales[scale])) + (255 % len(scales[scale]) > 0) pixels = list(image.getdata()) - chars = [] - for pixel in pixels: - if reverse: - index = -int(pixel/range_width)-1 - else: - index = int(pixel/range_width) - chars.append(ASCII_CHARS[index]) - - chars = "".join(chars) - chars = [chars[i:i + image.size[0]] for i in range(0, len(chars), + pixels = [pixels[i:i + image.size[0]] for i in range(0, len(pixels), image.size[0])] + + chars = [] + for row in pixels: + new_row = "" + for pixel in row: + R, G, B = pixel + L = R * 299/1000 + G * 587/1000 + B * 114/1000 + index = int(L/range_width) + char = scales[scale][index] + if color_code and char is not " ": + prefix = color_prefix[color_code] + char_color(pixel,color_code) + char = prefix + char + new_row += char + chars.append(new_row) return "\n".join(chars) @@ -132,46 +141,75 @@ def open_image(imagePath): return image -def colorize(chars, image, code): +def alpha_composite(front, back): + """Alpha composite two RGBA images. + + Source: http://stackoverflow.com/a/9166671/284318 + + Keyword Arguments: + front -- PIL RGBA Image object + back -- PIL RGBA Image object + """ - Colorizes the ascii matrix. Moves iteratively through the matrix and - calls char_color on each pixel/character. Spaces are skipped to save - time and CPU cycles. + front = np.asarray(front) + back = np.asarray(back) + result = np.empty(front.shape, dtype='float') + alpha = np.index_exp[:, :, 3:] + rgb = np.index_exp[:, :, :3] + falpha = front[alpha] / 255.0 + balpha = back[alpha] / 255.0 + result[alpha] = falpha + balpha * (1 - falpha) + old_setting = np.seterr(invalid='ignore') + result[rgb] = (front[rgb] * falpha + back[rgb] * balpha * (1 - falpha)) / result[alpha] + np.seterr(**old_setting) + result[alpha] *= 255 + np.clip(result, 0, 255) + # astype('uint8') maps np.nan and np.inf to 0 + result = result.astype('uint8') + result = Image.fromarray(result, 'RGBA') + return result + + +def alpha_composite_with_color(image, color=(255, 255, 255)): + """Alpha composite an RGBA image with a single color image of the + specified color and the same size as the original image. + + Keyword Arguments: + image -- PIL RGBA Image object + color -- Tuple r, g, b (default 255, 255, 255) + """ - prefix = {"irc": "\03", "ansi":"\033"} - chars = chars.split("\n") - for j in range(0, image.size[1]): - new_row = "" - for k in range(0, image.size[0]): - if chars[j][k] == " ": - continue - new_row += prefix[code] + char_color(image.getpixel((k,j)), code) - new_row += chars[j][k] - chars[j] = new_row - - chars = "\n".join(chars) - if code == "ansi": - chars += "\033[0m" # to avoid lingering effects in terminals - return chars + back = Image.new('RGBA', size=image.size, color=color + (255,)) + return alpha_composite(image, back) -def image_to_ascii(image, reverse=False, colors=None): +def image_to_ascii(image=None, **kwargs): """ Reads an image file and converts it to ascii art. Returns a newline-delineated string. If reverse is True, the ascii scale is reversed. """ + if not image: + image = open_image(kwargs["imagePath"]) + + if image.mode == "P": + image = image.convert(image.palette.mode) + if image.mode == "RGBA": + image = alpha_composite_with_color(image).convert("RGB") + image = scale_image(image) - image_grey = image.convert('L') # convert to grayscale - chars = pixels_to_chars(image_grey, reverse) + if kwargs["brail"]: + scale = "brail" + else: + scale = "ascii" + if kwargs["reverse"]: + scale += "_reverse" + + chars = pixels_to_chars(image, scale, kwargs["color"]) - if colors: - chars = colorize(chars, image, colors) image.close() - image_grey.close() del(image) - del(image_grey) return chars @@ -190,17 +228,17 @@ def ascii_to_image(image_ascii): return image -def handle_gif(image, output, reverse=False): +def handle_gif(**kwargs): """ Handle gifs seperately. """ - image = open_image(args.imagePath) + image = open_image(kwargs["imagePath"]) ascii_seq = [] - new_image = ascii_to_image(image_to_ascii(image, reverse)) + new_image = ascii_to_image(image_to_ascii(image, **kwargs)) image.seek(1) while True: try: - im = ascii_to_image(image_to_ascii(image, reverse)) + im = ascii_to_image(image_to_ascii(image, **kwargs)) ascii_seq.append(im) image.seek(image.tell()+1) except EOFError: @@ -210,7 +248,7 @@ def handle_gif(image, output, reverse=False): # duration=60, loop=0, optimize=True) ascii_seq = [new_image] + ascii_seq np_ascii_seq = [np.array(im) for im in ascii_seq] - with open(output, "wb") as file: + with open(kwargs["output"], "wb") as file: numpngw.write_apng(file, np_ascii_seq) @@ -234,6 +272,7 @@ if __name__=='__main__': parser.add_argument( "-i", "--image", + dest="drawImage", action="store_true", help="Outputs the ascii art as an image rather than text. Requires \ --output.") @@ -260,21 +299,25 @@ if __name__=='__main__': action="store_const", const="irc", help="Shortcut for '--color irc'.") + parser.add_argument( + "-b", + "--brail", + action="store_true", + help="Uses brail unicode characters instead of ascii characters.") args = parser.parse_args() if args.animated: # --animated includes --image - args.image = True - if args.image: # --image requires --output + args.drawImage = True + if args.drawImage: # --image requires --output if not args.output: parser.error("--image requires --output") - image = open_image(args.imagePath) if args.animated: - handle_gif(image, args.output, args.reverse) + handle_gif(**vars(args)) exit() - image_ascii = image_to_ascii(image, args.reverse, args.color) - if args.image: + image_ascii = image_to_ascii(None, **vars(args)) + if args.drawImage: image = ascii_to_image(image_ascii) image.save(args.output, "PNG") elif args.output: