Compare commits

..

No commits in common. "6cb6d15820f57c7f8e973eb1bc379e2eda3b1800" and "0e482e413f153de3a277d7ffeef05ae37db21737" have entirely different histories.

2 changed files with 295 additions and 363 deletions

View File

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

View File

@ -28,8 +28,7 @@ def text(html):
def wikt(word): def wikt(word):
bytes = requests.get(uri.format(word)) bytes = requests.get(uri.format(word))
# bytes = r_ul.sub('', bytes) bytes = r_ul.sub('', bytes)
bytes = bytes.text
mode = None mode = None
etymology = None etymology = None