From ffd5a62241cbff723568b709a4c1c764257f1dbd Mon Sep 17 00:00:00 2001 From: iou1name Date: Sun, 26 Nov 2017 13:54:54 -0500 Subject: [PATCH] added color to ascii.py --- modules/ascii.py | 216 ++++++++++++++++++++++++++--------- modules/help.py | 1 + modules/translate.py | 262 +++++++++++++++++++------------------------ 3 files changed, 276 insertions(+), 203 deletions(-) diff --git a/modules/ascii.py b/modules/ascii.py index 965566d..7ea5848 100755 --- a/modules/ascii.py +++ b/modules/ascii.py @@ -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,16 +280,17 @@ if __name__=='__main__': if not args.output: parser.error("--image requires --output") + image = open_image(args.imagePath) 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: - image = open_image(args.imagePath) - 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) + print(image_ascii) diff --git a/modules/help.py b/modules/help.py index 8ee9412..e803ae8 100755 --- a/modules/help.py +++ b/modules/help.py @@ -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)) diff --git a/modules/translate.py b/modules/translate.py index 1fbde30..f5a1c28 100755 --- a/modules/translate.py +++ b/modules/translate.py @@ -16,82 +16,50 @@ from module import rule, commands, priority, example mangle_lines = {} if sys.version_info.major >= 3: - unicode = str + unicode = str def translate(text, in_lang='auto', out_lang='en', verify_ssl=True): - raw = False - if unicode(out_lang).endswith('-raw'): - out_lang = out_lang[:-4] - raw = True + raw = False + if unicode(out_lang).endswith('-raw'): + out_lang = out_lang[:-4] + raw = True - headers = { - 'User-Agent': 'Mozilla/5.0' + - '(X11; U; Linux i686)' + - 'Gecko/20071127 Firefox/2.0.0.11' - } + headers = { + 'User-Agent': 'Mozilla/5.0' + + '(X11; U; Linux i686)' + + 'Gecko/20071127 Firefox/2.0.0.11' + } - query = { - "client": "gtx", - "sl": in_lang, - "tl": out_lang, - "dt": "t", - "q": text, - } - url = "http://translate.googleapis.com/translate_a/single" - result = requests.get(url, params=query, timeout=40, headers=headers, - verify=verify_ssl).text + query = { + "client": "gtx", + "sl": in_lang, + "tl": out_lang, + "dt": "t", + "q": text, + } + url = "http://translate.googleapis.com/translate_a/single" + result = requests.get(url, params=query, timeout=40, headers=headers, + verify=verify_ssl).text - if result == '[,,""]': - return None, in_lang + if result == '[,,""]': + return None, in_lang - while ',,' in result: - result = result.replace(',,', ',null,') - result = result.replace('[,', '[null,') + while ',,' in result: + result = result.replace(',,', ',null,') + result = result.replace('[,', '[null,') - data = json.loads(result) + data = json.loads(result) - if raw: - return str(data), 'en-raw' + if raw: + return str(data), 'en-raw' - try: - language = data[2] # -2][0][0] - except: - language = '?' + try: + language = data[2] # -2][0][0] + except: + 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(''', "'") - 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!') + return ''.join(x[0] for x in data[0]), language @commands('translate', 'tr') @@ -99,110 +67,110 @@ def tr(bot, trigger): @example('.tr היי', '"Hey" (iw to en, translate.google.com)') @example('.tr mon chien', '"my dog" (fr to en, translate.google.com)') def tr2(bot, trigger): - """Translates a phrase, with an optional language hint.""" - command = trigger.group(2) + """Translates a phrase, with an optional language hint.""" + command = trigger.group(2) - if not command: - return bot.reply('You did not give me anything to translate') + if not command: + return bot.reply('You did not give me anything to translate') - def langcode(p): - return p.startswith(':') and (2 < len(p) < 10) and p[1:].isalpha() + def langcode(p): + return p.startswith(':') and (2 < len(p) < 10) and p[1:].isalpha() - args = ['auto', 'en'] + args = ['auto', 'en'] - for i in range(2): - if ' ' not in command: - break - prefix, cmd = command.split(' ', 1) - if langcode(prefix): - args[i] = prefix[1:] - command = cmd - phrase = command + for i in range(2): + if ' ' not in command: + break + prefix, cmd = command.split(' ', 1) + if langcode(prefix): + args[i] = prefix[1:] + command = cmd + phrase = command - if (len(phrase) > 350) and (not trigger.admin): - return bot.reply('Phrase must be under 350 characters.') + 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!') + if phrase.strip() == '': + return bot.reply('You need to specify a string for me to translate!') - src, dest = args - if src != dest: - msg, src = translate(phrase, src, dest, - 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(''', "'") - 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) + src, dest = args + if src != dest: + msg, src = translate(phrase, src, dest, + 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(''', "'") + 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) - bot.reply(msg) - else: - bot.reply('Language guessing failed, so try suggesting one!') + bot.reply(msg) + else: + bot.reply('Language guessing failed, so try suggesting one!') def get_random_lang(long_list, short_list): - random_index = random.randint(0, len(long_list) - 1) - random_lang = long_list[random_index] - if random_lang not in short_list: - short_list.append(random_lang) - else: - return get_random_lang(long_list, short_list) - return short_list + random_index = random.randint(0, len(long_list) - 1) + random_lang = long_list[random_index] + if random_lang not in short_list: + short_list.append(random_lang) + else: + return get_random_lang(long_list, short_list) + return short_list @commands('mangle', 'mangle2') def mangle(bot, trigger): - """Repeatedly translate the input until it makes absolutely no sense.""" - verify_ssl = bot.config.core.verify_ssl - 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'] - lang_list = [] - for __ in range(0, 8): - lang_list = get_random_lang(long_lang_list, lang_list) - random.shuffle(lang_list) - if trigger.group(2) is None: - try: - phrase = (mangle_lines[trigger.sender.lower()], '') - except: - bot.reply("What do you want me to mangle?") - return - else: - phrase = (trigger.group(2).strip(), '') - if phrase[0] == '': - bot.reply("What do you want me to mangle?") - return - for lang in lang_list: - backup = phrase - try: - phrase = translate(phrase[0], 'en', lang, - verify_ssl=verify_ssl) - except: - phrase = False - if not phrase: - phrase = backup - break + """Repeatedly translate the input until it makes absolutely no sense.""" + verify_ssl = bot.config.core.verify_ssl + 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'] + lang_list = [] + for __ in range(0, 8): + lang_list = get_random_lang(long_lang_list, lang_list) + random.shuffle(lang_list) + if trigger.group(2) is None: + try: + phrase = (mangle_lines[trigger.sender.lower()], '') + except: + bot.reply("What do you want me to mangle?") + return + else: + phrase = (trigger.group(2).strip(), '') + if phrase[0] == '': + bot.reply("What do you want me to mangle?") + return + for lang in lang_list: + backup = phrase + try: + phrase = translate(phrase[0], 'en', lang, + verify_ssl=verify_ssl) + except: + phrase = False + if not phrase: + phrase = backup + break - try: - phrase = translate(phrase[0], lang, 'en', verify_ssl=verify_ssl) - except: - phrase = backup - continue + try: + phrase = translate(phrase[0], lang, 'en', verify_ssl=verify_ssl) + except: + phrase = backup + continue - if not phrase: - phrase = backup - break - bot.reply(phrase[0]) + if not phrase: + phrase = backup + break + bot.reply(phrase[0]) @rule('(.*)') @priority('low') def collect_mangle_lines(bot, trigger): - global mangle_lines - mangle_lines[trigger.sender.lower()] = "%s said '%s'" % (trigger.nick, (trigger.group(0).strip())) + global mangle_lines + mangle_lines[trigger.sender.lower()] = "%s said '%s'" % (trigger.nick, (trigger.group(0).strip())) if __name__ == "__main__": - from test_tools import run_example_tests - run_example_tests(__file__) + from test_tools import run_example_tests + run_example_tests(__file__)