added color to ascii.py

This commit is contained in:
iou1name 2017-11-26 13:54:54 -05:00
parent d7e63993ee
commit ffd5a62241
3 changed files with 276 additions and 203 deletions

View File

@ -1,16 +1,21 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ASCII
"""
from io import BytesIO
from PIL import Image
import requests
from PIL import Image, ImageFont, ImageDraw
#import imageio
import numpy as np
import numpngw
import module
from tools import web
from PIL import ImageFont
from PIL import ImageDraw
ASCII_CHARS = "$@%#*+=-:. "
headers = {'User-Agent': 'we wuz ascii and shiet'}
HEADERS = {'User-Agent': 'Gimme the ascii.'}
def scale_image(image, size=(100,100)):
@ -18,19 +23,22 @@ def scale_image(image, size=(100,100)):
Resizes an image while preserving the aspect ratio. Chooses the
dimension to scale by based on whichever is larger, ensuring that
neither width or height is ever larger than the accepted size tuple.
Because text characters are typically pretty close to a 1:2 rectangle,
we weight the width twice as much.
"""
original_width, original_height = image.size
original_width = original_width * 2 # because characters are generally
if original_width > original_height: # displayed as a 1:2 square
original_width = original_width * 2
if original_width > original_height:
if original_width > size[0]:
new_width = 100
new_width = size[0]
aspect_ratio = original_height/float(original_width)
new_height = int(aspect_ratio * new_width)
else:
new_width, new_height = image.size
else:
if original_height > size[1]:
new_height = 100
new_height = size[1]
aspect_ratio = original_width/float(original_height)
new_width = int(aspect_ratio * new_height)
else:
@ -41,70 +49,130 @@ def scale_image(image, size=(100,100)):
def pixels_to_chars(image, reverse=False):
"""
Maps each pixel to an ascii char based on the range
in which it lies.
0-255 is divided into 11 ranges of 25 pixels each.
Maps each pixel to an ascii char based on where it falls in the range
0-255 normalized to the length of ASCII_CHARS.
"""
range_width = int(255 / len(ASCII_CHARS)) + (255 % len(ASCII_CHARS) > 0)
pixels_in_image = list(image.getdata())
pixels_to_chars = []
for pixel_value in pixels_in_image:
pixels = list(image.getdata())
chars = []
for pixel in pixels:
if reverse:
index = -int(pixel_value/range_width)-1
index = -int(pixel/range_width)-1
else:
index = int(pixel_value/range_width)
pixels_to_chars.append(ASCII_CHARS[ index ])
index = int(pixel/range_width)
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])]
return "\n".join(chars)
def char_color(pixel, code="irc"):
"""
Maps a color to a character based on the original pixel color.
Calculates the distance from the provided color to each of the 16
colors in the IRC and ANSI color codes and selects the closest match.
"""
colors_index = ["white", "black", "blue", "green", "red", "brown", "purple",
"orange", "yellow", "light green", "teal", "cyan", "light blue", "pink",
"grey", "silver"]
colors_rgb = {"white": (0,0,0), "black": (255,255,255), "blue": (0,0,255),
"green": (0,255,0), "red": (255,0,0), "brown": (150,75,0),
"purple": (128,0,128), "orange": (255,128,0), "yellow": (255,255,0),
"light green": (191,255,0), "teal": (0,128,128), "cyan": (0,255,255),
"light blue": (65,105,225), "pink":(255,192,203), "grey": (128,128,128),
"silver": (192,192,192)}
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",
"silver": "15"}
colors_ansi = {"white": "[1;37m", "black": "[0;30m", "blue": "[0;34m",
"green": "[0;32m", "red": "[0;31m", "brown": "[0;33m",
"purple": "[0;35m", "orange": "[1;31m", "yellow": "[1;33m",
"light green": "[1;32m", "teal": "[0;36m", "cyan": "[1;36m",
"light blue": "[1;34m", "pink": "[1;35m", "grey": "[1;30m",
"silver": "[0;37m"}
dist = [(abs(pixel[0] - colors_rgb[color][0])**2
+ abs(pixel[1] - colors_rgb[color][1])**2
+ abs(pixel[2] - colors_rgb[color][2])**2)**0.5
for color in colors_index]
color = colors_index[dist.index(min(dist))]
if code == "irc":
return colors_irc[color]
elif code == "ansi":
return colors_ansi[color]
def open_image(imagePath):
"""
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.
is supplied, it will download the image and then open it. Returns a
PIL image object.
"""
try:
if imagePath.startswith("http"):
res = requests.get(imagePath, headers=headers, verify=True, timeout=20)
res = requests.get(imagePath, headers=HEADERS, verify=True,
timeout=20)
res.raise_for_status()
image = Image.open(BytesIO(res.content))
else:
image = Image.open(imagePath)
except FileNotFoundError as e:
return e
except OSError:
return e
return f"File not found: {imagePath}"
except Exception as e:
return("Error opening image file: " + imagePath)
return(f"Error opening image: {imagePath}\n{e}")
return image
def image_to_ascii(image, reverse=False):
def colorize(chars, image, code):
"""
Colorizes the ascii matrix.
"""
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]):
new_row += prefix[code] + char_color(image.getpixel((k,j)), code)
new_row += chars[j][k]
chars[j] = new_row
return "\n".join(chars)
def image_to_ascii(image, reverse=False, colors=None):
"""
Reads an image file and converts it to ascii art. Returns a
newline-delineated string. If reverse is True, the ascii scale is
reversed.
"""
image = scale_image(image)
image = image.convert('L')
image_grey = image.convert('L') # convert to grayscale
chars = pixels_to_chars(image, reverse)
chars = pixels_to_chars(image_grey, reverse)
image_ascii = []
for index in range(0, len(chars), image.size[0]):
image_ascii.append( chars[index: index + image.size[0]] )
if colors:
chars = colorize(chars, image, colors)
image.close()
del image
return "\n".join(image_ascii)
image_grey.close()
del(image)
del(image_grey)
return chars
def ascii_to_image(image_ascii):
"""
Creates a plain image and draws text on it.
"""
# TODO: make font type and size non-fixed
width = len(image_ascii[:image_ascii.index("\n")]) * 8
height = (image_ascii.count("\n")+1) * 12 + 4
@ -115,7 +183,10 @@ def ascii_to_image(image_ascii):
return image
def handle_gif(output, reverse=False):
def handle_gif(image, output, reverse=False):
"""
Handle gifs seperately.
"""
image = open_image(args.imagePath)
ascii_seq = []
new_image = ascii_to_image(image_to_ascii(image, reverse))
@ -123,12 +194,17 @@ def handle_gif(output, reverse=False):
while True:
try:
im = ascii_to_image(image_to_ascii(image, reverse))
ascii_seq.append()
ascii_seq.append(im)
image.seek(image.tell()+1)
except EOFError:
break # end of sequence
new_image.save(args.output, save_all=True, append_images=ascii_seq, duration=60, loop=0, optimize=True)
#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)
@module.rate(user=60)
@ -140,9 +216,13 @@ def ascii(bot, trigger):
Downloads an image and converts it to ascii.
"""
reverse = False
color = None
if trigger.group(3) == "-r":
imagePath = trigger.group(4)
reverse = True
elif trigger.group(3) == "-c":
imagePath = trigger.group(4)
color = "irc"
else:
imagePath = trigger.group(2)
@ -154,20 +234,43 @@ def ascii(bot, trigger):
return
image = open_image(imagePath)
image_ascii = image_to_ascii(image, reverse)
image_ascii = image_to_ascii(image, reverse, color)
bot.say(image_ascii)
if __name__=='__main__':
import argparse
# TODO: satisfy PEP8
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 = 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()
@ -177,11 +280,12 @@ if __name__=='__main__':
if not args.output:
parser.error("--image requires --output")
if args.animated:
handle_gif(args.output, args.reverse)
else:
image = open_image(args.imagePath)
image_ascii = image_to_ascii(image, args.reverse)
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")

View File

@ -31,6 +31,7 @@ def help(bot, trigger):
name = name.lower()
if name in bot.doc:
print(bot.doc[name])
newlines = ['']
lines = list(filter(None, bot.doc[name][0]))
lines = list(map(str.strip, lines))

View File

@ -62,38 +62,6 @@ def translate(text, in_lang='auto', out_lang='en', verify_ssl=True):
return ''.join(x[0] for x in data[0]), language
@rule(u'$nickname[,:]\s+(?:([a-z]{2}) +)?(?:([a-z]{2}|en-raw) +)?["“](.+?)["”]\? *$')
@example('$nickname: "mon chien"? or $nickname: fr "mon chien"?')
@priority('low')
def tr(bot, trigger):
"""Translates a phrase, with an optional language hint."""
in_lang, out_lang, phrase = trigger.groups()
if (len(phrase) > 350) and (not trigger.admin):
return bot.reply('Phrase must be under 350 characters.')
if phrase.strip() == '':
return bot.reply('You need to specify a string for me to translate!')
in_lang = in_lang or 'auto'
out_lang = out_lang or 'en'
if in_lang != out_lang:
msg, in_lang = translate(phrase, in_lang, out_lang,
verify_ssl=bot.config.core.verify_ssl)
if sys.version_info.major < 3 and isinstance(msg, str):
msg = msg.decode('utf-8')
if msg:
msg = web.decode(msg) # msg.replace('&#39;', "'")
msg = '"%s" (%s to %s, translate.google.com)' % (msg, in_lang, out_lang)
else:
msg = 'The %s to %s translation failed, are you sure you specified valid language abbreviations?' % (in_lang, out_lang)
bot.reply(msg)
else:
bot.reply('Language guessing failed, so try suggesting one!')
@commands('translate', 'tr')
@example('.tr :en :fr my dog', '"mon chien" (en to fr, translate.google.com)')
@example('.tr היי', '"Hey" (iw to en, translate.google.com)')
@ -132,7 +100,7 @@ def tr2(bot, trigger):
if sys.version_info.major < 3 and isinstance(msg, str):
msg = msg.decode('utf-8')
if msg:
msg = web.decode(msg) # msg.replace('&#39;', "'")
#msg = web.decode(msg) # msg.replace('&#39;', "'")
msg = '"%s" (%s to %s, translate.google.com)' % (msg, src, dest)
else:
msg = 'The %s to %s translation failed, are you sure you specified valid language abbreviations?' % (src, dest)