added brail and animated gif support

This commit is contained in:
iou1name 2017-12-03 20:46:10 -05:00
parent 0e482e413f
commit 6e67a63d73

View File

@ -1,294 +1,361 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
ASCII ASCII
""" """
from io import BytesIO from io import BytesIO
import argparse import argparse
import requests import requests
from PIL import Image, ImageFont, ImageDraw from PIL import Image, ImageFont, ImageDraw
#import imageio #import imageio
import numpy as np import numpy as np
import numpngw import numpngw
import module import module
from tools import web from tools import web
ASCII_CHARS = "$@%#*+=-:. " ASCII_CHARS = "$@%#*+=-:. "
HEADERS = {'User-Agent': 'Gimme the ascii.'} BRAIL_CHARS = "⠿⠾⠼⠸⠰⠠ "
HEADERS = {'User-Agent': 'Gimme the ascii.'}
def scale_image(image, size=(100,100)):
""" 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 Resizes an image while preserving the aspect ratio. Chooses the
neither width or height is ever larger than the accepted size tuple. 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. 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 original_width, original_height = image.size
if original_width > original_height: original_width = original_width * 2
if original_width > size[0]: if original_width > original_height:
new_width = size[0] if original_width > size[0]:
aspect_ratio = original_height/float(original_width) new_width = size[0]
new_height = int(aspect_ratio * new_width) aspect_ratio = original_height/float(original_width)
else: new_height = int(aspect_ratio * new_width)
new_width, new_height = image.size else:
else: new_width, new_height = image.size
if original_height > size[1]: else:
new_height = size[1] if original_height > size[1]:
aspect_ratio = original_width/float(original_height) new_height = size[1]
new_width = int(aspect_ratio * new_height) aspect_ratio = original_width/float(original_height)
else: new_width = int(aspect_ratio * new_height)
new_width, new_height = image.size else:
image = image.resize((new_width, new_height)) new_width, new_height = image.size
return image image = image.resize((new_width, new_height))
return image
def pixels_to_chars(image, reverse=False):
""" def pixels_to_chars(image, scale="ascii"):
Maps each pixel to an ascii char based on where it falls in the range """
0-255 normalized to the length of ASCII_CHARS. 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) """
scales = {"ascii": ASCII_CHARS,
pixels = list(image.getdata()) "ascii_reverse": "".join(list(reversed(ASCII_CHARS))),
chars = [] "brail": BRAIL_CHARS,
for pixel in pixels: "brail_reverse": "".join(list(reversed(BRAIL_CHARS)))}
if reverse: range_width = int(255 / len(scales[scale])) + (255 % len(scales[scale]) > 0)
index = -int(pixel/range_width)-1
else: pixels = list(image.getdata())
index = int(pixel/range_width) chars = []
chars.append(ASCII_CHARS[index]) for pixel in pixels:
index = int(pixel/range_width)
chars = "".join(chars) chars.append(scales[scale][index])
chars = [chars[i:i + image.size[0]] for i in range(0, len(chars),
image.size[0])] chars = "".join(chars)
return "\n".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. def char_color(pixel, code="irc"):
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. 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_index = ["white", "black", "blue", "green", "red", "brown", "purple", colors in the IRC and ANSI color codes and selects the closest match.
"orange", "yellow", "light green", "teal", "cyan", "light blue", "pink", """
"grey", "silver"] colors_index = ["white", "black", "blue", "green", "red", "brown", "purple",
"orange", "yellow", "light green", "teal", "cyan", "light blue", "pink",
colors_rgb = {"white": (0,0,0), "black": (255,255,255), "blue": (0,0,255), "grey", "silver"]
"green": (0,255,0), "red": (255,0,0), "brown": (150,75,0),
"purple": (128,0,128), "orange": (255,128,0), "yellow": (255,255,0), colors_rgb = {"white": (0,0,0), "black": (255,255,255), "blue": (0,0,255),
"light green": (191,255,0), "teal": (0,128,128), "cyan": (0,255,255), "green": (0,255,0), "red": (255,0,0), "brown": (150,75,0),
"light blue": (65,105,225), "pink":(255,192,203), "grey": (128,128,128), "purple": (128,0,128), "orange": (255,128,0), "yellow": (255,255,0),
"silver": (192,192,192)} "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),
colors_irc = {"white": "0", "black": "1", "blue": "2", "green": "3", "silver": (192,192,192)}
"red": "4", "brown": "5", "purple": "6", "orange": "7", "yellow": "8",
"light green": "9", "teal": "10", "cyan": "11", "light blue": "12", colors_irc = {"white": "0", "black": "1", "blue": "2", "green": "3",
"pink": "13", "grey": "14", "silver": "15"} "red": "4", "brown": "5", "purple": "6", "orange": "7", "yellow": "8",
"light green": "9", "teal": "10", "cyan": "11", "light blue": "12",
colors_ansi = {"white": "[1;37m", "black": "[0;30m", "blue": "[0;34m", "pink": "13", "grey": "14", "silver": "15"}
"green": "[0;32m", "red": "[0;31m", "brown": "[0;33m",
"purple": "[0;35m", "orange": "[1;31m", "yellow": "[1;33m", colors_ansi = {"white": "[1;37m", "black": "[0;30m", "blue": "[0;34m",
"light green": "[1;32m", "teal": "[0;36m", "cyan": "[1;36m", "green": "[0;32m", "red": "[0;31m", "brown": "[0;33m",
"light blue": "[1;34m", "pink": "[1;35m", "grey": "[1;30m", "purple": "[0;35m", "orange": "[1;31m", "yellow": "[1;33m",
"silver": "[0;37m"} "light green": "[1;32m", "teal": "[0;36m", "cyan": "[1;36m",
"light blue": "[1;34m", "pink": "[1;35m", "grey": "[1;30m",
dist = [(abs(pixel[0] - colors_rgb[color][0])**2 "silver": "[0;37m"}
+ abs(pixel[1] - colors_rgb[color][1])**2
+ abs(pixel[2] - colors_rgb[color][2])**2)**0.5 dist = [(abs(pixel[0] - colors_rgb[color][0])**2
for color in colors_index] + abs(pixel[1] - colors_rgb[color][1])**2
+ abs(pixel[2] - colors_rgb[color][2])**2)**0.5
color = colors_index[dist.index(min(dist))] for color in colors_index]
if code == "irc": color = colors_index[dist.index(min(dist))]
return colors_irc[color]
elif code == "ansi": if code == "irc":
return colors_ansi[color] 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 def open_image(imagePath):
is supplied, it will download the image and then open it. Returns a """
PIL image object. 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. Returns a
try: PIL image object.
if imagePath.startswith("http"): """
res = requests.get(imagePath, headers=HEADERS, verify=True, try:
timeout=20) if imagePath.startswith("http"):
res.raise_for_status() res = requests.get(imagePath, headers=HEADERS, verify=True,
image = Image.open(BytesIO(res.content)) timeout=20)
else: res.raise_for_status()
image = Image.open(imagePath) image = Image.open(BytesIO(res.content))
except FileNotFoundError as e: else:
return f"File not found: {imagePath}" image = Image.open(imagePath)
except Exception as e: except FileNotFoundError as e:
return(f"Error opening image: {imagePath}\n{e}") return f"File not found: {imagePath}"
except Exception as e:
return image return(f"Error opening image: {imagePath}\n{e}")
return image
def colorize(chars, image, code):
"""
Colorizes the ascii matrix. def colorize(chars, image, code):
""" """
prefix = {"irc": "\03", "ansi":"\033"} Colorizes the ascii matrix.
chars = chars.split("\n") """
for j in range(0, image.size[1]): prefix = {"irc": "\03", "ansi":"\033"}
new_row = "" chars = chars.split("\n")
for k in range(0, image.size[0]): for j in range(0, image.size[1]):
new_row += prefix[code] + char_color(image.getpixel((k,j)), code) new_row = ""
new_row += chars[j][k] for k in range(0, image.size[0]):
chars[j] = new_row new_row += prefix[code] + char_color(image.getpixel((k,j)), code)
return "\n".join(chars) 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 def alpha_composite(front, back):
newline-delineated string. If reverse is True, the ascii scale is """Alpha composite two RGBA images.
reversed.
""" Source: http://stackoverflow.com/a/9166671/284318
image = scale_image(image)
image_grey = image.convert('L') # convert to grayscale Keyword Arguments:
front -- PIL RGBA Image object
chars = pixels_to_chars(image_grey, reverse) back -- PIL RGBA Image object
if colors: """
chars = colorize(chars, image, colors) front = np.asarray(front)
image.close() back = np.asarray(back)
image_grey.close() result = np.empty(front.shape, dtype='float')
del(image) alpha = np.index_exp[:, :, 3:]
del(image_grey) rgb = np.index_exp[:, :, :3]
return chars falpha = front[alpha] / 255.0
balpha = back[alpha] / 255.0
result[alpha] = falpha + balpha * (1 - falpha)
def ascii_to_image(image_ascii): old_setting = np.seterr(invalid='ignore')
""" result[rgb] = (front[rgb] * falpha + back[rgb] * balpha * (1 - falpha)) / result[alpha]
Creates a plain image and draws text on it. np.seterr(**old_setting)
""" result[alpha] *= 255
# TODO: make font type and size non-fixed np.clip(result, 0, 255)
width = len(image_ascii[:image_ascii.index("\n")]) * 8 # astype('uint8') maps np.nan and np.inf to 0
height = (image_ascii.count("\n")+1) * 12 + 4 result = result.astype('uint8')
result = Image.fromarray(result, 'RGBA')
font = ImageFont.truetype("LiberationMono-Regular.ttf", 14) return result
image = Image.new("RGB", (width, height), (255,255,255))
draw = ImageDraw.Draw(image)
draw.text((0,0), image_ascii, (0,0,0), font=font, spacing=0) def alpha_composite_with_color(image, color=(255, 255, 255)):
return image """Alpha composite an RGBA image with a single color image of the
specified color and the same size as the original image.
def handle_gif(image, output, reverse=False): Keyword Arguments:
""" image -- PIL RGBA Image object
Handle gifs seperately. color -- Tuple r, g, b (default 255, 255, 255)
"""
image = open_image(args.imagePath) """
ascii_seq = [] back = Image.new('RGBA', size=image.size, color=color + (255,))
new_image = ascii_to_image(image_to_ascii(image, reverse)) return alpha_composite(image, back)
image.seek(1)
while True:
try: def image_to_ascii(image, reverse=False, colors=None, brail=False):
im = ascii_to_image(image_to_ascii(image, reverse)) """
ascii_seq.append(im) Reads an image file and converts it to ascii art. Returns a
image.seek(image.tell()+1) newline-delineated string. If reverse is True, the ascii scale is
except EOFError: reversed.
break # end of sequence """
if image.mode == "RGBA":
#new_image.save(output, save_all=True, append_images=ascii_seq, image = alpha_composite_with_color(image).convert("RGB")
# duration=60, loop=0, optimize=True)
ascii_seq = [new_image] + ascii_seq image = scale_image(image)
np_ascii_seq = [np.array(im) for im in ascii_seq] image_grey = image.convert('L') # convert to grayscale
with open(output, "wb") as file:
numpngw.write_apng(file, np_ascii_seq) if reverse:
if brail:
scale = "brail_reverse"
@module.rate(user=60) else:
@module.require_chanmsg(message="It's impolite to whisper.") scale = "ascii_reverse"
@module.commands('ascii') else:
@module.example('.ascii [-rc] https://www.freshports.org/images/freshports.jpg') if brail:
def ascii(bot, trigger): scale = "brail"
""" else:
Downloads an image and converts it to ascii. scale = "ascii"
""" chars = pixels_to_chars(image_grey, scale)
parser = argparse.ArgumentParser()
parser.add_argument("imagePath") if colors:
parser.add_argument("-r", "--reverse", action="store_true") chars = colorize(chars, image, colors)
parser.add_argument("-c", "--color", action="store_true") image.close()
parser.set_defaults(reverse=False, color=False) image_grey.close()
args = parser.parse_args(trigger.group(2).split()) del(image)
del(image_grey)
if args.color: return chars
args.color = "irc"
if not web.secCheck(bot, args.imagePath): def ascii_to_image(image_ascii):
return bot.reply("Known malicious site. Ignoring.") """
Creates a plain image and draws text on it.
if not args.imagePath.startswith("http"): """
bot.reply("Internet requests only.") # TODO: make font type and size non-fixed
return width = len(image_ascii[:image_ascii.index("\n")]) * 8
height = (image_ascii.count("\n")+1) * 12 + 4
image = open_image(args.imagePath)
image_ascii = image_to_ascii(image, args.reverse, args.color) font = ImageFont.truetype("LiberationMono-Regular.ttf", 14)
bot.say(image_ascii) image = Image.new("RGB", (width, height), (255,255,255))
draw = ImageDraw.Draw(image)
draw.text((0,0), image_ascii, (0,0,0), font=font, spacing=0)
if __name__=='__main__': return image
parser = argparse.ArgumentParser(
description="Converts an image file to ascii art.")
parser.add_argument( def handle_gif(image, output, reverse=False):
"imagePath", """
help="The full path to the image file.") Handle gifs seperately.
parser.add_argument( """
"-r", # image = open_image(args.imagePath)
"--reverse", ascii_seq = []
action="store_true", new_image = ascii_to_image(image_to_ascii(image, reverse))
help="Reverses the ascii scale.") image.seek(1)
parser.add_argument( while True:
"-o", try:
"--output", im = ascii_to_image(image_to_ascii(image, reverse))
help="Outputs the ascii art into a file at the specified path.") ascii_seq.append(im)
parser.add_argument( image.seek(image.tell()+1)
"-i", except EOFError:
"--image", break # end of sequence
action="store_true",
help="Outputs the ascii art as an image rather than plain text. \ #new_image.save(output, save_all=True, append_images=ascii_seq,
Requires --output.") # duration=60, loop=0, optimize=True)
parser.add_argument( ascii_seq = [new_image] + ascii_seq
"-a", np_ascii_seq = [np.array(im) for im in ascii_seq]
"--animated", with open(output, "wb") as file:
action="store_true", numpngw.write_apng(file, np_ascii_seq)
help="Handles animated GIFs. Includes --image.")
parser.add_argument(
"-c", @module.rate(user=60)
"--color", @module.require_chanmsg(message="It's impolite to whisper.")
type=str, @module.commands('ascii')
help="Colorizes the ascii matrix.") @module.example('.ascii [-rc] https://www.freshports.org/images/freshports.jpg')
parser.set_defaults(reverse=False, image=False, animated=False) def ascii(bot, trigger):
args = parser.parse_args() """
Downloads an image and converts it to ascii.
if args.animated: # --animated includes --image """
args.image = True if not trigger.group(2):
if args.image: # --image requires --output return bot.say()
if not args.output: parser = argparse.ArgumentParser()
parser.error("--image requires --output") parser.add_argument("imagePath")
parser.add_argument("-r", "--reverse", action="store_true")
image = open_image(args.imagePath) parser.add_argument("-c", "--color", action="store_true")
if args.animated: parser.add_argument("-b", "--brail", action="store_true")
handle_gif(image, args.output, args.reverse) parser.add_argument("-a", "--animated", action="store_true")
exit() args = parser.parse_args(trigger.group(2).split())
image_ascii = image_to_ascii(image, args.reverse, args.color) if args.color:
if args.image: args.color = "irc"
image = ascii_to_image(image_ascii)
image.save(args.output, "PNG") if not web.secCheck(bot, args.imagePath):
elif args.output: return bot.reply("Known malicious site. Ignoring.")
with open(args.output, "w+") as file:
file.write(image_ascii) if not args.imagePath.startswith("http"):
else: bot.reply("Internet requests only.")
print(image_ascii) return
image = open_image(args.imagePath)
if args.animated:
handle_gif(image, "temp.png", args.reverse)
file = {"file": open("temp.png", "rb")}
res = requests.post("https://uguu.se/api.php?d=upload-tool", files=file)
# print(res.text)
bot.say(res.text)
else:
image_ascii = image_to_ascii(image, args.reverse, args.color, args.brail)
bot.say(image_ascii)
if __name__=='__main__':
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()
if args.animated: # --animated includes --image
args.image = True
if args.image: # --image requires --output
if not args.output:
parser.error("--image requires --output")
image = open_image(args.imagePath)
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")
elif args.output:
with open(args.output, "w+") as file:
file.write(image_ascii)
else:
print(image_ascii)