added color to ascii.py
This commit is contained in:
parent
d7e63993ee
commit
ffd5a62241
202
modules/ascii.py
202
modules/ascii.py
|
@ -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")
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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(''', "'")
|
|
||||||
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(''', "'")
|
#msg = web.decode(msg) # msg.replace(''', "'")
|
||||||
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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user