diff --git a/ascii.py b/ascii.py index 9096176..73e56cd 100644 --- a/ascii.py +++ b/ascii.py @@ -1,195 +1,195 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -ASCII -""" -from io import BytesIO - -import requests -from PIL import Image, ImageFont, ImageDraw -#import imageio -import numpy as np -import numpngw - -ASCII_CHARS = "$@%#*+=-:. " -HEADERS = {'User-Agent': 'Gimme the ascii.'} - - -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 - if original_width > original_height: - if original_width > size[0]: - 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 = size[1] - aspect_ratio = original_width/float(original_height) - new_width = int(aspect_ratio * new_height) - else: - new_width, new_height = image.size - image = image.resize((new_width, new_height)) - return image - - -def pixels_to_chars(image, reverse=False): - """ - 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: - if reverse: - index = -int(pixel_value/range_width)-1 - else: - index = int(pixel_value/range_width) - pixels_to_chars.append(ASCII_CHARS[index]) - - return "".join(pixels_to_chars) - - -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. - """ - try: - if imagePath.startswith("http"): - 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 - except Exception as e: - return("Error opening image file: " + imagePath) - - return image - - -def image_to_ascii(image, reverse=False): - """ - 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') # convert to grayscale - - chars = pixels_to_chars(image, reverse) - - image_ascii = [] - for index in range(0, len(chars), image.size[0]): - image_ascii.append(chars[index: index + image.size[0]]) - image.close() - del(image) - return "\n".join(image_ascii) - - -def ascii_to_image(image_ascii): - """ - Creates a plain image and draws text on it. - """ - # TODO: make font type, size, and image size non-fixed - width = len(image_ascii[:image_ascii.index("\n")]) * 8 - height = (image_ascii.count("\n")+1) * 12 + 4 - - font = ImageFont.truetype("LiberationMono-Regular.ttf", 14) - 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) - return image - - -def handle_gif(output, reverse=False): - image = open_image(args.imagePath) - ascii_seq = [] - new_image = ascii_to_image(image_to_ascii(image, reverse)) - image.seek(1) - while True: - try: - im = ascii_to_image(image_to_ascii(image, reverse)) - ascii_seq.append(im) - image.seek(image.tell()+1) - except EOFError: - break # end of sequence - - #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] - #images2gif.writeGif(output, ascii_seq, nq=10, subRectangles=False) - #imageio.mimsave(output, np_ascii_seq) - with open(output, "wb") as file: - numpngw.write_apng(file, np_ascii_seq) - - -if __name__=='__main__': - import argparse - - 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.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") - - if args.animated: - handle_gif(args.output, args.reverse) - 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) +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +ASCII. +""" +from io import BytesIO + +import requests +from PIL import Image, ImageFont, ImageDraw +#import imageio +import numpy as np +import numpngw + +ASCII_CHARS = "$@%#*+=-:. " +HEADERS = {'User-Agent': 'Gimme the ascii.'} + + +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 + if original_width > original_height: + if original_width > size[0]: + 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 = size[1] + aspect_ratio = original_width/float(original_height) + new_width = int(aspect_ratio * new_height) + else: + new_width, new_height = image.size + image = image.resize((new_width, new_height)) + return image + + +def pixels_to_chars(image, reverse=False): + """ + 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: + if reverse: + index = -int(pixel_value/range_width)-1 + else: + index = int(pixel_value/range_width) + pixels_to_chars.append(ASCII_CHARS[index]) + + return "".join(pixels_to_chars) + + +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. + """ + try: + if imagePath.startswith("http"): + 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 + except Exception as e: + return("Error opening image file: " + imagePath) + + return image + + +def image_to_ascii(image, reverse=False): + """ + 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') # convert to grayscale + + chars = pixels_to_chars(image, reverse) + + image_ascii = [] + for index in range(0, len(chars), image.size[0]): + image_ascii.append(chars[index: index + image.size[0]]) + image.close() + del(image) + return "\n".join(image_ascii) + + +def ascii_to_image(image_ascii): + """ + Creates a plain image and draws text on it. + """ + # TODO: make font type, size, and image size non-fixed + width = len(image_ascii[:image_ascii.index("\n")]) * 8 + height = (image_ascii.count("\n")+1) * 12 + 4 + + font = ImageFont.truetype("LiberationMono-Regular.ttf", 14) + 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) + return image + + +def handle_gif(output, reverse=False): + image = open_image(args.imagePath) + ascii_seq = [] + new_image = ascii_to_image(image_to_ascii(image, reverse)) + image.seek(1) + while True: + try: + im = ascii_to_image(image_to_ascii(image, reverse)) + ascii_seq.append(im) + image.seek(image.tell()+1) + except EOFError: + break # end of sequence + + #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] + #images2gif.writeGif(output, ascii_seq, nq=10, subRectangles=False) + #imageio.mimsave(output, np_ascii_seq) + with open(output, "wb") as file: + numpngw.write_apng(file, np_ascii_seq) + + +if __name__=='__main__': + import argparse + + 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.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") + + if args.animated: + handle_gif(args.output, args.reverse) + 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)