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 #! /usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
ASCII
"""
from io import BytesIO from io import BytesIO
from PIL import Image
import requests import requests
from PIL import Image, ImageFont, ImageDraw
#import imageio
import numpy as np
import numpngw
import module import module
from tools import web from tools import web
from PIL import ImageFont
from PIL import ImageDraw
ASCII_CHARS = "$@%#*+=-:. " ASCII_CHARS = "$@%#*+=-:. "
headers = {'User-Agent': 'we wuz ascii and shiet'} HEADERS = {'User-Agent': 'Gimme the ascii.'}
def scale_image(image, size=(100,100)): 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 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,
we weight the width twice as much.
""" """
original_width, original_height = image.size original_width, original_height = image.size
original_width = original_width * 2 # because characters are generally original_width = original_width * 2
if original_width > original_height: # displayed as a 1:2 square if original_width > original_height:
if original_width > size[0]: if original_width > size[0]:
new_width = 100 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 = 100 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:
@ -41,70 +49,130 @@ def scale_image(image, size=(100,100)):
def pixels_to_chars(image, reverse=False): def pixels_to_chars(image, reverse=False):
""" """
Maps each pixel to an ascii char based on the range Maps each pixel to an ascii char based on where it falls in the range
in which it lies. 0-255 normalized to the length of ASCII_CHARS.
0-255 is divided into 11 ranges of 25 pixels each.
""" """
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])]
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): def open_image(imagePath):
""" """
Opens the image at the supplied file path in PIL. If an internet URL 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: try:
if imagePath.startswith("http"): 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() res.raise_for_status()
image = Image.open(BytesIO(res.content)) image = Image.open(BytesIO(res.content))
else: else:
image = Image.open(imagePath) image = Image.open(imagePath)
except FileNotFoundError as e: except FileNotFoundError as e:
return e return f"File not found: {imagePath}"
except OSError:
return e
except Exception as e: except Exception as e:
return("Error opening image file: " + imagePath) return(f"Error opening image: {imagePath}\n{e}")
return image 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 Reads an image file and converts it to ascii art. Returns a
newline-delineated string. If reverse is True, the ascii scale is newline-delineated string. If reverse is True, the ascii scale is
reversed. reversed.
""" """
image = scale_image(image) 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 = [] if colors:
for index in range(0, len(chars), image.size[0]): chars = colorize(chars, image, colors)
image_ascii.append( chars[index: index + image.size[0]] )
image.close() image.close()
del image image_grey.close()
return "\n".join(image_ascii) del(image)
del(image_grey)
return chars
def ascii_to_image(image_ascii): def ascii_to_image(image_ascii):
""" """
Creates a plain image and draws text on it. 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 width = len(image_ascii[:image_ascii.index("\n")]) * 8
height = (image_ascii.count("\n")+1) * 12 + 4 height = (image_ascii.count("\n")+1) * 12 + 4
@ -115,7 +183,10 @@ def ascii_to_image(image_ascii):
return image return image
def handle_gif(output, reverse=False): def handle_gif(image, output, reverse=False):
"""
Handle gifs seperately.
"""
image = open_image(args.imagePath) image = open_image(args.imagePath)
ascii_seq = [] ascii_seq = []
new_image = ascii_to_image(image_to_ascii(image, reverse)) new_image = ascii_to_image(image_to_ascii(image, reverse))
@ -123,12 +194,17 @@ def handle_gif(output, reverse=False):
while True: while True:
try: try:
im = ascii_to_image(image_to_ascii(image, reverse)) im = ascii_to_image(image_to_ascii(image, reverse))
ascii_seq.append() ascii_seq.append(im)
image.seek(image.tell()+1) image.seek(image.tell()+1)
except EOFError: except EOFError:
break # end of sequence 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) @module.rate(user=60)
@ -140,9 +216,13 @@ def ascii(bot, trigger):
Downloads an image and converts it to ascii. Downloads an image and converts it to ascii.
""" """
reverse = False reverse = False
color = None
if trigger.group(3) == "-r": if trigger.group(3) == "-r":
imagePath = trigger.group(4) imagePath = trigger.group(4)
reverse = True reverse = True
elif trigger.group(3) == "-c":
imagePath = trigger.group(4)
color = "irc"
else: else:
imagePath = trigger.group(2) imagePath = trigger.group(2)
@ -154,20 +234,43 @@ def ascii(bot, trigger):
return return
image = open_image(imagePath) image = open_image(imagePath)
image_ascii = image_to_ascii(image, reverse) image_ascii = image_to_ascii(image, reverse, color)
bot.say(image_ascii) bot.say(image_ascii)
if __name__=='__main__': if __name__=='__main__':
import argparse import argparse
# TODO: satisfy PEP8 parser = argparse.ArgumentParser(
parser = argparse.ArgumentParser(description="Converts an image file to ascii art.") description="Converts an image file to ascii art.")
parser.add_argument("imagePath", help="The full path to the image file.") parser.add_argument(
parser.add_argument("-r", "--reverse", action="store_true", help="Reverses the ascii scale.") "imagePath",
parser.add_argument("-o", "--output", help="Outputs the ascii art into a file at the specified path.") help="The full path to the image file.")
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(
parser.add_argument("-a", "--animated", action="store_true", help="Handles animated GIFs. Includes --image.") "-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) parser.set_defaults(reverse=False, image=False, animated=False)
args = parser.parse_args() args = parser.parse_args()
@ -177,16 +280,17 @@ if __name__=='__main__':
if not args.output: if not args.output:
parser.error("--image requires --output") parser.error("--image requires --output")
image = open_image(args.imagePath)
if args.animated: if args.animated:
handle_gif(args.output, args.reverse) 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: else:
image = open_image(args.imagePath) print(image_ascii)
image_ascii = image_to_ascii(image, args.reverse)
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)

View File

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

View File

@ -16,82 +16,50 @@ from module import rule, commands, priority, example
mangle_lines = {} mangle_lines = {}
if sys.version_info.major >= 3: if sys.version_info.major >= 3:
unicode = str unicode = str
def translate(text, in_lang='auto', out_lang='en', verify_ssl=True): def translate(text, in_lang='auto', out_lang='en', verify_ssl=True):
raw = False raw = False
if unicode(out_lang).endswith('-raw'): if unicode(out_lang).endswith('-raw'):
out_lang = out_lang[:-4] out_lang = out_lang[:-4]
raw = True raw = True
headers = { headers = {
'User-Agent': 'Mozilla/5.0' + 'User-Agent': 'Mozilla/5.0' +
'(X11; U; Linux i686)' + '(X11; U; Linux i686)' +
'Gecko/20071127 Firefox/2.0.0.11' 'Gecko/20071127 Firefox/2.0.0.11'
} }
query = { query = {
"client": "gtx", "client": "gtx",
"sl": in_lang, "sl": in_lang,
"tl": out_lang, "tl": out_lang,
"dt": "t", "dt": "t",
"q": text, "q": text,
} }
url = "http://translate.googleapis.com/translate_a/single" url = "http://translate.googleapis.com/translate_a/single"
result = requests.get(url, params=query, timeout=40, headers=headers, result = requests.get(url, params=query, timeout=40, headers=headers,
verify=verify_ssl).text verify=verify_ssl).text
if result == '[,,""]': if result == '[,,""]':
return None, in_lang return None, in_lang
while ',,' in result: while ',,' in result:
result = result.replace(',,', ',null,') result = result.replace(',,', ',null,')
result = result.replace('[,', '[null,') result = result.replace('[,', '[null,')
data = json.loads(result) data = json.loads(result)
if raw: if raw:
return str(data), 'en-raw' return str(data), 'en-raw'
try: try:
language = data[2] # -2][0][0] language = data[2] # -2][0][0]
except: except:
language = '?' language = '?'
return ''.join(x[0] for x in data[0]), language 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') @commands('translate', 'tr')
@ -99,110 +67,110 @@ def tr(bot, trigger):
@example('.tr היי', '"Hey" (iw to en, translate.google.com)') @example('.tr היי', '"Hey" (iw to en, translate.google.com)')
@example('.tr mon chien', '"my dog" (fr to en, translate.google.com)') @example('.tr mon chien', '"my dog" (fr to en, translate.google.com)')
def tr2(bot, trigger): def tr2(bot, trigger):
"""Translates a phrase, with an optional language hint.""" """Translates a phrase, with an optional language hint."""
command = trigger.group(2) command = trigger.group(2)
if not command: if not command:
return bot.reply('You did not give me anything to translate') return bot.reply('You did not give me anything to translate')
def langcode(p): def langcode(p):
return p.startswith(':') and (2 < len(p) < 10) and p[1:].isalpha() return p.startswith(':') and (2 < len(p) < 10) and p[1:].isalpha()
args = ['auto', 'en'] args = ['auto', 'en']
for i in range(2): for i in range(2):
if ' ' not in command: if ' ' not in command:
break break
prefix, cmd = command.split(' ', 1) prefix, cmd = command.split(' ', 1)
if langcode(prefix): if langcode(prefix):
args[i] = prefix[1:] args[i] = prefix[1:]
command = cmd command = cmd
phrase = command phrase = command
if (len(phrase) > 350) and (not trigger.admin): if (len(phrase) > 350) and (not trigger.admin):
return bot.reply('Phrase must be under 350 characters.') return bot.reply('Phrase must be under 350 characters.')
if phrase.strip() == '': if phrase.strip() == '':
return bot.reply('You need to specify a string for me to translate!') return bot.reply('You need to specify a string for me to translate!')
src, dest = args src, dest = args
if src != dest: if src != dest:
msg, src = translate(phrase, src, dest, msg, src = translate(phrase, src, dest,
verify_ssl=bot.config.core.verify_ssl) verify_ssl=bot.config.core.verify_ssl)
if sys.version_info.major < 3 and isinstance(msg, str): if sys.version_info.major < 3 and isinstance(msg, str):
msg = msg.decode('utf-8') msg = msg.decode('utf-8')
if msg: 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) msg = '"%s" (%s to %s, translate.google.com)' % (msg, src, dest)
else: else:
msg = 'The %s to %s translation failed, are you sure you specified valid language abbreviations?' % (src, dest) msg = 'The %s to %s translation failed, are you sure you specified valid language abbreviations?' % (src, dest)
bot.reply(msg) bot.reply(msg)
else: else:
bot.reply('Language guessing failed, so try suggesting one!') bot.reply('Language guessing failed, so try suggesting one!')
def get_random_lang(long_list, short_list): def get_random_lang(long_list, short_list):
random_index = random.randint(0, len(long_list) - 1) random_index = random.randint(0, len(long_list) - 1)
random_lang = long_list[random_index] random_lang = long_list[random_index]
if random_lang not in short_list: if random_lang not in short_list:
short_list.append(random_lang) short_list.append(random_lang)
else: else:
return get_random_lang(long_list, short_list) return get_random_lang(long_list, short_list)
return short_list return short_list
@commands('mangle', 'mangle2') @commands('mangle', 'mangle2')
def mangle(bot, trigger): def mangle(bot, trigger):
"""Repeatedly translate the input until it makes absolutely no sense.""" """Repeatedly translate the input until it makes absolutely no sense."""
verify_ssl = bot.config.core.verify_ssl verify_ssl = bot.config.core.verify_ssl
global mangle_lines global mangle_lines
long_lang_list = ['fr', 'de', 'es', 'it', 'no', 'he', 'la', 'ja', 'cy', 'ar', 'yi', 'zh', 'nl', 'ru', 'fi', 'hi', 'af', 'jw', 'mr', 'ceb', 'cs', 'ga', 'sv', 'eo', 'el', 'ms', 'lv'] long_lang_list = ['fr', 'de', 'es', 'it', 'no', 'he', 'la', 'ja', 'cy', 'ar', 'yi', 'zh', 'nl', 'ru', 'fi', 'hi', 'af', 'jw', 'mr', 'ceb', 'cs', 'ga', 'sv', 'eo', 'el', 'ms', 'lv']
lang_list = [] lang_list = []
for __ in range(0, 8): for __ in range(0, 8):
lang_list = get_random_lang(long_lang_list, lang_list) lang_list = get_random_lang(long_lang_list, lang_list)
random.shuffle(lang_list) random.shuffle(lang_list)
if trigger.group(2) is None: if trigger.group(2) is None:
try: try:
phrase = (mangle_lines[trigger.sender.lower()], '') phrase = (mangle_lines[trigger.sender.lower()], '')
except: except:
bot.reply("What do you want me to mangle?") bot.reply("What do you want me to mangle?")
return return
else: else:
phrase = (trigger.group(2).strip(), '') phrase = (trigger.group(2).strip(), '')
if phrase[0] == '': if phrase[0] == '':
bot.reply("What do you want me to mangle?") bot.reply("What do you want me to mangle?")
return return
for lang in lang_list: for lang in lang_list:
backup = phrase backup = phrase
try: try:
phrase = translate(phrase[0], 'en', lang, phrase = translate(phrase[0], 'en', lang,
verify_ssl=verify_ssl) verify_ssl=verify_ssl)
except: except:
phrase = False phrase = False
if not phrase: if not phrase:
phrase = backup phrase = backup
break break
try: try:
phrase = translate(phrase[0], lang, 'en', verify_ssl=verify_ssl) phrase = translate(phrase[0], lang, 'en', verify_ssl=verify_ssl)
except: except:
phrase = backup phrase = backup
continue continue
if not phrase: if not phrase:
phrase = backup phrase = backup
break break
bot.reply(phrase[0]) bot.reply(phrase[0])
@rule('(.*)') @rule('(.*)')
@priority('low') @priority('low')
def collect_mangle_lines(bot, trigger): def collect_mangle_lines(bot, trigger):
global mangle_lines global mangle_lines
mangle_lines[trigger.sender.lower()] = "%s said '%s'" % (trigger.nick, (trigger.group(0).strip())) mangle_lines[trigger.sender.lower()] = "%s said '%s'" % (trigger.nick, (trigger.group(0).strip()))
if __name__ == "__main__": if __name__ == "__main__":
from test_tools import run_example_tests from test_tools import run_example_tests
run_example_tests(__file__) run_example_tests(__file__)