New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ASCII-art emulator #15
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright (c) 2017 Richard Hull and contributors | ||
# See LICENSE.rst for details. | ||
|
||
# From a comment by @TerrorBite on https://gist.github.com/MicahElliott/719710 | ||
|
||
# Default color levels for the color cube | ||
cubelevels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] | ||
|
||
# Generate a list of midpoints of the above list | ||
snaps = [(x + y) / 2 for x, y in list(zip(cubelevels, [0] + cubelevels))[1:]] | ||
|
||
|
||
def rgb2short(r, g, b): | ||
""" | ||
Converts RGB values to the nearest equivalent xterm-256 color. | ||
""" | ||
# Using list of snap points, convert RGB value to cube indexes | ||
r, g, b = [len(tuple(s for s in snaps if s < x)) for x in (r, g, b)] | ||
|
||
# Simple colorcube transform | ||
return (r * 36) + (g * 6) + b + 16 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,17 +6,25 @@ | |
import sys | ||
import atexit | ||
import logging | ||
import string | ||
import curses | ||
import collections | ||
try: | ||
from StringIO import StringIO | ||
except ImportError: | ||
from io import StringIO | ||
|
||
from PIL import Image | ||
from PIL import Image, ImageFont, ImageDraw | ||
|
||
from luma.core.device import device | ||
from luma.core.serial import noop | ||
from luma.emulator.render import transformer | ||
from luma.emulator.clut import rgb2short | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
__all__ = ["capture", "gifanim", "pygame"] | ||
__all__ = ["capture", "gifanim", "pygame", "asciiart"] | ||
|
||
|
||
class emulator(device): | ||
|
@@ -197,3 +205,116 @@ def contrast(self, value): | |
assert(0 <= value <= 255) | ||
self._contrast = value / 255.0 | ||
self.display(self._last_image) | ||
|
||
|
||
class asciiart(emulator): | ||
""" | ||
Pseudo-device that acts like a physical display, except that it converts the | ||
image to display into an ASCII-art representation and downscales colors to | ||
match the xterm-256 color scheme. Supports 24-bit color depth. | ||
|
||
This device takes hold of the terminal window (using curses), and any output | ||
for sysout and syserr is captured and stored, and is replayed when the | ||
cleanup method is called. | ||
|
||
Loosely based on https://github.com/ajalt/pyasciigen/blob/master/asciigen.py | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add 'since' version nr. |
||
def __init__(self, width=128, height=64, rotate=0, mode="RGB", transform="scale2x", | ||
scale=2, **kwargs): | ||
|
||
super(asciiart, self).__init__(width, height, rotate, mode, transform, scale) | ||
self._stdscr = curses.initscr() | ||
curses.start_color() | ||
curses.use_default_colors() | ||
for i in range(0, curses.COLORS): | ||
curses.init_pair(i, i, -1) | ||
curses.noecho() | ||
curses.cbreak() | ||
|
||
# Capture all stdout, stderr | ||
self._old_stdX = (sys.stdout, sys.stderr) | ||
self._captured = (StringIO(), StringIO()) | ||
sys.stdout, sys.stderr = self._captured | ||
|
||
# Sort printable characters according to the number of black pixels present. | ||
# Don't use string.printable, since we don't want any whitespace except spaces. | ||
charset = (string.letters + string.digits + string.punctuation + " ") | ||
self._chars = list(reversed(sorted(charset, key=self._char_density))) | ||
self._char_width, self._char_height = ImageFont.load_default().getsize("X") | ||
self._contrast = 1.0 | ||
self._last_image = Image.new(mode, (width, height)) | ||
|
||
def _char_density(self, c, font=ImageFont.load_default()): | ||
""" | ||
Count the number of black pixels in a rendered character. | ||
""" | ||
image = Image.new('1', font.getsize(c), color=255) | ||
draw = ImageDraw.Draw(image) | ||
draw.text((0, 0), c, fill="white", font=font) | ||
return collections.Counter(image.getdata())[0] # 0 is black | ||
|
||
def _generate_art(self, image, width, height): | ||
""" | ||
Return an iterator that produces the ascii art. | ||
""" | ||
# Characters aren't square, so scale the output by the aspect ratio of a charater | ||
height = int(height * self._char_width / float(self._char_height)) | ||
image = image.resize((width, height), Image.ANTIALIAS).convert("RGB") | ||
|
||
for (r, g, b) in image.getdata(): | ||
greyscale = int(0.299 * r + 0.587 * g + 0.114 * b) | ||
ch = self._chars[int(greyscale / 255. * (len(self._chars) - 1) + 0.5)] | ||
yield (ch, rgb2short(r, g, b)) | ||
|
||
def display(self, image): | ||
""" | ||
Takes a :py:mod:`PIL.Image` and renders it to the current terminal as | ||
ASCII-art. | ||
""" | ||
assert(image.size == self.size) | ||
self._last_image = image | ||
|
||
surface = self.to_surface(self.preprocess(image), alpha=self._contrast) | ||
rawbytes = self._pygame.image.tostring(surface, "RGB", False) | ||
image = Image.frombytes("RGB", (self._w * self.scale, self._h * self.scale), rawbytes) | ||
|
||
scr_height, scr_width = self._stdscr.getmaxyx() | ||
scale = float(scr_width) / image.width | ||
|
||
self._stdscr.erase() | ||
self._stdscr.move(0, 0) | ||
try: | ||
for (ch, color) in self._generate_art(image, int(image.width * scale), int(image.height * scale)): | ||
self._stdscr.addch(ch, curses.color_pair(color)) | ||
|
||
except curses.error: | ||
# End of screen reached | ||
pass | ||
|
||
self._stdscr.refresh() | ||
|
||
def show(self): | ||
self.contrast(0xFF) | ||
|
||
def hide(self): | ||
self.contrast(0x00) | ||
|
||
def contrast(self, value): | ||
assert(0 <= value <= 255) | ||
self._contrast = value / 255.0 | ||
self.display(self._last_image) | ||
|
||
def cleanup(self): | ||
super(asciiart, self).cleanup() | ||
|
||
# Stty sane | ||
curses.nocbreak() | ||
curses.echo() | ||
curses.endwin() | ||
|
||
# Print out captured stdout/stderr | ||
sys.stdout, sys.stderr = self._old_stdX | ||
sys.stdout.write(self._captured[0].getvalue()) | ||
sys.stdout.flush() | ||
sys.stderr.write(self._captured[1].getvalue()) | ||
sys.stderr.flush() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be avoided, we use py27 and newer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you be more specific?
Not sure what you're referring to with 'this'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the import workaround. simply
from io import StringIO
is enough, isn't it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly sure, but in py2, I think:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A test would show the difference.