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,11 +280,12 @@ if __name__=='__main__':
if not args.output: if not args.output:
parser.error("--image requires --output") parser.error("--image requires --output")
if args.animated:
handle_gif(args.output, args.reverse)
else:
image = open_image(args.imagePath) 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: if args.image:
image = ascii_to_image(image_ascii) image = ascii_to_image(image_ascii)
image.save(args.output, "PNG") image.save(args.output, "PNG")

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

@ -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 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')
@example('.tr :en :fr my dog', '"mon chien" (en to fr, translate.google.com)') @example('.tr :en :fr my dog', '"mon chien" (en to fr, translate.google.com)')
@example('.tr היי', '"Hey" (iw to en, 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): 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)