added color

This commit is contained in:
iou1name 2017-11-26 14:04:47 -05:00
parent 2eb6dd716c
commit 72211f6688

457
ascii.py Normal file → Executable file
View File

@ -1,195 +1,262 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
ASCII ASCII
""" """
from io import BytesIO from io import BytesIO
import requests import requests
from PIL import Image, ImageFont, ImageDraw from PIL import Image, ImageFont, ImageDraw
#import imageio #import imageio
import numpy as np import numpy as np
import numpngw import numpngw
ASCII_CHARS = "$@%#*+=-:. " ASCII_CHARS = "$@%#*+=-:. "
HEADERS = {'User-Agent': 'bix nood where dah ascii tiddies at'} HEADERS = {'User-Agent': 'Gimme the ascii.'}
def scale_image(image, size=(100,100)): def scale_image(image, size=(100,100)):
""" """
Resizes an image while preserving the aspect ratio. Chooses the Resizes an image while preserving the aspect ratio. Chooses the
dimension to scale by based on whichever is larger, ensuring that dimension to scale by based on whichever is larger, ensuring that
neither width or height is ever larger than the accepted size tuple. neither width or height is ever larger than the accepted size tuple.
Because text characters are typically pretty close to a 1:2 rectangle, Because text characters are typically pretty close to a 1:2 rectangle,
we weight the width twice as much. we weight the width twice as much.
""" """
original_width, original_height = image.size original_width, original_height = image.size
original_width = original_width * 2 original_width = original_width * 2
if original_width > original_height: if original_width > original_height:
if original_width > size[0]: if original_width > size[0]:
new_width = size[0] new_width = size[0]
aspect_ratio = original_height/float(original_width) aspect_ratio = original_height/float(original_width)
new_height = int(aspect_ratio * new_width) new_height = int(aspect_ratio * new_width)
else: else:
new_width, new_height = image.size new_width, new_height = image.size
else: else:
if original_height > size[1]: if original_height > size[1]:
new_height = size[1] new_height = size[1]
aspect_ratio = original_width/float(original_height) aspect_ratio = original_width/float(original_height)
new_width = int(aspect_ratio * new_height) new_width = int(aspect_ratio * new_height)
else: else:
new_width, new_height = image.size new_width, new_height = image.size
image = image.resize((new_width, new_height)) image = image.resize((new_width, new_height))
return image return image
def pixels_to_chars(image, reverse=False): def pixels_to_chars(image, reverse=False):
""" """
Maps each pixel to an ascii char based on where it falls in the range 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 ASCII_CHARS.
""" """
range_width = int(255 / len(ASCII_CHARS)) + (255 % len(ASCII_CHARS) > 0) range_width = int(255 / len(ASCII_CHARS)) + (255 % len(ASCII_CHARS) > 0)
pixels_in_image = list(image.getdata()) pixels = list(image.getdata())
pixels_to_chars = [] chars = []
for pixel_value in pixels_in_image: for pixel in pixels:
if reverse: if reverse:
index = -int(pixel_value/range_width)-1 index = -int(pixel/range_width)-1
else: else:
index = int(pixel_value/range_width) index = int(pixel/range_width)
pixels_to_chars.append(ASCII_CHARS[index]) chars.append(ASCII_CHARS[index])
return "".join(pixels_to_chars) chars = "".join(chars)
chars = [chars[i:i + image.size[0]] for i in range(0, len(chars),
image.size[0])]
def open_image(imagePath): return "\n".join(chars)
"""
Opens the image at the supplied file path in PIL. If an internet URL
is supplied, it will download the image and then open it. def char_color(pixel, code="irc"):
""" """
try: Maps a color to a character based on the original pixel color.
if imagePath.startswith("http"): Calculates the distance from the provided color to each of the 16
res = requests.get(imagePath, headers=HEADERS, verify=True, colors in the IRC and ANSI color codes and selects the closest match.
timeout=20) """
res.raise_for_status() colors_index = ["white", "black", "blue", "green", "red", "brown", "purple",
image = Image.open(BytesIO(res.content)) "orange", "yellow", "light green", "teal", "cyan", "light blue", "pink",
else: "grey", "silver"]
image = Image.open(imagePath)
except FileNotFoundError as e: colors_rgb = {"white": (0,0,0), "black": (255,255,255), "blue": (0,0,255),
return e "green": (0,255,0), "red": (255,0,0), "brown": (150,75,0),
except OSError: "purple": (128,0,128), "orange": (255,128,0), "yellow": (255,255,0),
return e "light green": (191,255,0), "teal": (0,128,128), "cyan": (0,255,255),
except Exception as e: "light blue": (65,105,225), "pink":(255,192,203), "grey": (128,128,128),
return("Error opening image file: " + imagePath) "silver": (192,192,192)}
return image colors_irc = {"white": "0", "black": "1", "blue": "2", "green": "3", "red": "4",
"brown": "5", "purple": "6", "orange": "7", "yellow": "8", "light green": "9",
"teal": "10", "cyan": "11", "light blue": "12", "pink": "13", "grey": "14",
def image_to_ascii(image, reverse=False): "silver": "15"}
"""
Reads an image file and converts it to ascii art. Returns a colors_ansi = {"white": "[1;37m", "black": "[0;30m", "blue": "[0;34m",
newline-delineated string. If reverse is True, the ascii scale is "green": "[0;32m", "red": "[0;31m", "brown": "[0;33m",
reversed. "purple": "[0;35m", "orange": "[1;31m", "yellow": "[1;33m",
""" "light green": "[1;32m", "teal": "[0;36m", "cyan": "[1;36m",
image = scale_image(image) "light blue": "[1;34m", "pink": "[1;35m", "grey": "[1;30m",
image = image.convert('L') # convert to grayscale "silver": "[0;37m"}
chars = pixels_to_chars(image, reverse) dist = [(abs(pixel[0] - colors_rgb[color][0])**2
+ abs(pixel[1] - colors_rgb[color][1])**2
image_ascii = [] + abs(pixel[2] - colors_rgb[color][2])**2)**0.5
for index in range(0, len(chars), image.size[0]): for color in colors_index]
image_ascii.append(chars[index: index + image.size[0]])
image.close() color = colors_index[dist.index(min(dist))]
del(image)
return "\n".join(image_ascii) if code == "irc":
return colors_irc[color]
elif code == "ansi":
def ascii_to_image(image_ascii): return colors_ansi[color]
"""
Creates a plain image and draws text on it.
""" def open_image(imagePath):
# TODO: make font type, size, and image size non-fixed """
width = len(image_ascii[:image_ascii.index("\n")]) * 8 Opens the image at the supplied file path in PIL. If an internet URL
height = (image_ascii.count("\n")+1) * 12 + 4 is supplied, it will download the image and then open it. Returns a
PIL image object.
font = ImageFont.truetype("LiberationMono-Regular.ttf", 14) """
image = Image.new("RGB", (width, height), (255,255,255)) try:
draw = ImageDraw.Draw(image) if imagePath.startswith("http"):
draw.text((0,0), image_ascii, (0,0,0), font=font, spacing=0) res = requests.get(imagePath, headers=HEADERS, verify=True,
return image timeout=20)
res.raise_for_status()
image = Image.open(BytesIO(res.content))
def handle_gif(output, reverse=False): else:
image = open_image(args.imagePath) image = Image.open(imagePath)
ascii_seq = [] except FileNotFoundError as e:
new_image = ascii_to_image(image_to_ascii(image, reverse)) return f"File not found: {imagePath}"
image.seek(1) except Exception as e:
while True: return(f"Error opening image: {imagePath}\n{e}")
try:
im = ascii_to_image(image_to_ascii(image, reverse)) return image
ascii_seq.append(im)
image.seek(image.tell()+1)
except EOFError: def colorize(chars, image, code):
break # end of sequence """
Colorizes the ascii matrix.
#new_image.save(output, save_all=True, append_images=ascii_seq, """
# duration=60, loop=0, optimize=True) prefix = {"irc": "\03", "ansi":"\033"}
ascii_seq = [new_image] + ascii_seq chars = chars.split("\n")
np_ascii_seq = [np.array(im) for im in ascii_seq] for j in range(0, image.size[1]):
#images2gif.writeGif(output, ascii_seq, nq=10, subRectangles=False) new_row = ""
#imageio.mimsave(output, np_ascii_seq) for k in range(0, image.size[0]):
with open(output, "wb") as file: new_row += prefix[code] + char_color(image.getpixel((k,j)), code)
numpngw.write_apng(file, np_ascii_seq) new_row += chars[j][k]
chars[j] = new_row
return "\n".join(chars)
if __name__=='__main__':
import argparse
def image_to_ascii(image, reverse=False, colors=None):
parser = argparse.ArgumentParser( """
description="Converts an image file to ascii art.") Reads an image file and converts it to ascii art. Returns a
parser.add_argument( newline-delineated string. If reverse is True, the ascii scale is
"imagePath", reversed.
help="The full path to the image file.") """
parser.add_argument( image = scale_image(image)
"-r", image_grey = image.convert('L') # convert to grayscale
"--reverse",
action="store_true", chars = pixels_to_chars(image_grey, reverse)
help="Reverses the ascii scale.")
parser.add_argument( if colors:
"-o", chars = colorize(chars, image, colors)
"--output", image.close()
help="Outputs the ascii art into a file at the specified path.") image_grey.close()
parser.add_argument( del(image)
"-i", del(image_grey)
"--image", return chars
action="store_true",
help="Outputs the ascii art as an image rather than plain text. \
Requires --output.") def ascii_to_image(image_ascii):
parser.add_argument( """
"-a", Creates a plain image and draws text on it.
"--animated", """
action="store_true", # TODO: make font type and size non-fixed
help="Handles animated GIFs. Includes --image.") width = len(image_ascii[:image_ascii.index("\n")]) * 8
parser.set_defaults(reverse=False, image=False, animated=False) height = (image_ascii.count("\n")+1) * 12 + 4
args = parser.parse_args()
font = ImageFont.truetype("LiberationMono-Regular.ttf", 14)
if args.animated: # --animated includes --image image = Image.new("RGB", (width, height), (255,255,255))
args.image = True draw = ImageDraw.Draw(image)
if args.image: # --image requires --output draw.text((0,0), image_ascii, (0,0,0), font=font, spacing=0)
if not args.output: return image
parser.error("--image requires --output")
if args.animated: def handle_gif(image, output, reverse=False):
handle_gif(args.output, args.reverse) """
else: Handle gifs seperately.
image = open_image(args.imagePath) """
image_ascii = image_to_ascii(image, args.reverse) image = open_image(args.imagePath)
if args.image: ascii_seq = []
image = ascii_to_image(image_ascii) new_image = ascii_to_image(image_to_ascii(image, reverse))
image.save(args.output, "PNG") image.seek(1)
elif args.output: while True:
with open(args.output, "w+") as file: try:
file.write(image_ascii) im = ascii_to_image(image_to_ascii(image, reverse))
else: ascii_seq.append(im)
print(image_ascii) image.seek(image.tell()+1)
except EOFError:
break # end of sequence
#new_image.save(output, save_all=True, append_images=ascii_seq,
# 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:
numpngw.write_apng(file, np_ascii_seq)
if __name__=='__main__':
import argparse
parser = argparse.ArgumentParser(
description="Converts an image file to ascii art.")
parser.add_argument(
"imagePath",
help="The full path to the image file.")
parser.add_argument(
"-r",
"--reverse",
action="store_true",
help="Reverses the ascii scale.")
parser.add_argument(
"-o",
"--output",
help="Outputs the ascii art into a file at the specified path.")
parser.add_argument(
"-i",
"--image",
action="store_true",
help="Outputs the ascii art as an image rather than plain text. \
Requires --output.")
parser.add_argument(
"-a",
"--animated",
action="store_true",
help="Handles animated GIFs. Includes --image.")
parser.add_argument(
"-c",
"--color",
type=str,
help="Colorizes the ascii matrix.")
parser.set_defaults(reverse=False, image=False, animated=False)
args = parser.parse_args()
if args.animated: # --animated includes --image
args.image = True
if args.image: # --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)
exit()
image_ascii = image_to_ascii(image, args.reverse, args.color)
if args.image:
image = ascii_to_image(image_ascii)
image.save(args.output, "PNG")
elif args.output:
with open(args.output, "w+") as file:
file.write(image_ascii)
else:
print(image_ascii)