Last active
September 28, 2022 19:11
-
-
Save marcan/f02cfe8f7d848b748920ca6738c4e858 to your computer and use it in GitHub Desktop.
Image to xterm-256 Unicode block art converter
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
#!/usr/bin/env python3 | |
from __future__ import print_function | |
import sys, argparse, codecs | |
from PIL import Image, ImagePalette | |
xterm256colors = [ # http://pln.jonas.me/xterm-colors | |
0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x80, 0x00, | |
0x00, 0x00, 0x80, 0x80, 0x00, 0x80, 0x00, 0x80, 0x80, 0xc0, 0xc0, 0xc0, | |
0x80, 0x80, 0x80, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, | |
0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, | |
0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x87, 0x00, 0x00, 0xaf, | |
0x00, 0x00, 0xd7, 0x00, 0x00, 0xff, 0x00, 0x5f, 0x00, 0x00, 0x5f, 0x5f, | |
0x00, 0x5f, 0x87, 0x00, 0x5f, 0xaf, 0x00, 0x5f, 0xd7, 0x00, 0x5f, 0xff, | |
0x00, 0x87, 0x00, 0x00, 0x87, 0x5f, 0x00, 0x87, 0x87, 0x00, 0x87, 0xaf, | |
0x00, 0x87, 0xd7, 0x00, 0x87, 0xff, 0x00, 0xaf, 0x00, 0x00, 0xaf, 0x5f, | |
0x00, 0xaf, 0x87, 0x00, 0xaf, 0xaf, 0x00, 0xaf, 0xd7, 0x00, 0xaf, 0xff, | |
0x00, 0xd7, 0x00, 0x00, 0xd7, 0x5f, 0x00, 0xd7, 0x87, 0x00, 0xd7, 0xaf, | |
0x00, 0xd7, 0xd7, 0x00, 0xd7, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0x5f, | |
0x00, 0xff, 0x87, 0x00, 0xff, 0xaf, 0x00, 0xff, 0xd7, 0x00, 0xff, 0xff, | |
0x5f, 0x00, 0x00, 0x5f, 0x00, 0x5f, 0x5f, 0x00, 0x87, 0x5f, 0x00, 0xaf, | |
0x5f, 0x00, 0xd7, 0x5f, 0x00, 0xff, 0x5f, 0x5f, 0x00, 0x5f, 0x5f, 0x5f, | |
0x5f, 0x5f, 0x87, 0x5f, 0x5f, 0xaf, 0x5f, 0x5f, 0xd7, 0x5f, 0x5f, 0xff, | |
0x5f, 0x87, 0x00, 0x5f, 0x87, 0x5f, 0x5f, 0x87, 0x87, 0x5f, 0x87, 0xaf, | |
0x5f, 0x87, 0xd7, 0x5f, 0x87, 0xff, 0x5f, 0xaf, 0x00, 0x5f, 0xaf, 0x5f, | |
0x5f, 0xaf, 0x87, 0x5f, 0xaf, 0xaf, 0x5f, 0xaf, 0xd7, 0x5f, 0xaf, 0xff, | |
0x5f, 0xd7, 0x00, 0x5f, 0xd7, 0x5f, 0x5f, 0xd7, 0x87, 0x5f, 0xd7, 0xaf, | |
0x5f, 0xd7, 0xd7, 0x5f, 0xd7, 0xff, 0x5f, 0xff, 0x00, 0x5f, 0xff, 0x5f, | |
0x5f, 0xff, 0x87, 0x5f, 0xff, 0xaf, 0x5f, 0xff, 0xd7, 0x5f, 0xff, 0xff, | |
0x87, 0x00, 0x00, 0x87, 0x00, 0x5f, 0x87, 0x00, 0x87, 0x87, 0x00, 0xaf, | |
0x87, 0x00, 0xd7, 0x87, 0x00, 0xff, 0x87, 0x5f, 0x00, 0x87, 0x5f, 0x5f, | |
0x87, 0x5f, 0x87, 0x87, 0x5f, 0xaf, 0x87, 0x5f, 0xd7, 0x87, 0x5f, 0xff, | |
0x87, 0x87, 0x00, 0x87, 0x87, 0x5f, 0x87, 0x87, 0x87, 0x87, 0x87, 0xaf, | |
0x87, 0x87, 0xd7, 0x87, 0x87, 0xff, 0x87, 0xaf, 0x00, 0x87, 0xaf, 0x5f, | |
0x87, 0xaf, 0x87, 0x87, 0xaf, 0xaf, 0x87, 0xaf, 0xd7, 0x87, 0xaf, 0xff, | |
0x87, 0xd7, 0x00, 0x87, 0xd7, 0x5f, 0x87, 0xd7, 0x87, 0x87, 0xd7, 0xaf, | |
0x87, 0xd7, 0xd7, 0x87, 0xd7, 0xff, 0x87, 0xff, 0x00, 0x87, 0xff, 0x5f, | |
0x87, 0xff, 0x87, 0x87, 0xff, 0xaf, 0x87, 0xff, 0xd7, 0x87, 0xff, 0xff, | |
0xaf, 0x00, 0x00, 0xaf, 0x00, 0x5f, 0xaf, 0x00, 0x87, 0xaf, 0x00, 0xaf, | |
0xaf, 0x00, 0xd7, 0xaf, 0x00, 0xff, 0xaf, 0x5f, 0x00, 0xaf, 0x5f, 0x5f, | |
0xaf, 0x5f, 0x87, 0xaf, 0x5f, 0xaf, 0xaf, 0x5f, 0xd7, 0xaf, 0x5f, 0xff, | |
0xaf, 0x87, 0x00, 0xaf, 0x87, 0x5f, 0xaf, 0x87, 0x87, 0xaf, 0x87, 0xaf, | |
0xaf, 0x87, 0xd7, 0xaf, 0x87, 0xff, 0xaf, 0xaf, 0x00, 0xaf, 0xaf, 0x5f, | |
0xaf, 0xaf, 0x87, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xd7, 0xaf, 0xaf, 0xff, | |
0xaf, 0xd7, 0x00, 0xaf, 0xd7, 0x5f, 0xaf, 0xd7, 0x87, 0xaf, 0xd7, 0xaf, | |
0xaf, 0xd7, 0xd7, 0xaf, 0xd7, 0xff, 0xaf, 0xff, 0x00, 0xaf, 0xff, 0x5f, | |
0xaf, 0xff, 0x87, 0xaf, 0xff, 0xaf, 0xaf, 0xff, 0xd7, 0xaf, 0xff, 0xff, | |
0xd7, 0x00, 0x00, 0xd7, 0x00, 0x5f, 0xd7, 0x00, 0x87, 0xd7, 0x00, 0xaf, | |
0xd7, 0x00, 0xd7, 0xd7, 0x00, 0xff, 0xd7, 0x5f, 0x00, 0xd7, 0x5f, 0x5f, | |
0xd7, 0x5f, 0x87, 0xd7, 0x5f, 0xaf, 0xd7, 0x5f, 0xd7, 0xd7, 0x5f, 0xff, | |
0xd7, 0x87, 0x00, 0xd7, 0x87, 0x5f, 0xd7, 0x87, 0x87, 0xd7, 0x87, 0xaf, | |
0xd7, 0x87, 0xd7, 0xd7, 0x87, 0xff, 0xd7, 0xaf, 0x00, 0xd7, 0xaf, 0x5f, | |
0xd7, 0xaf, 0x87, 0xd7, 0xaf, 0xaf, 0xd7, 0xaf, 0xd7, 0xd7, 0xaf, 0xff, | |
0xd7, 0xd7, 0x00, 0xd7, 0xd7, 0x5f, 0xd7, 0xd7, 0x87, 0xd7, 0xd7, 0xaf, | |
0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xff, 0xd7, 0xff, 0x00, 0xd7, 0xff, 0x5f, | |
0xd7, 0xff, 0x87, 0xd7, 0xff, 0xaf, 0xd7, 0xff, 0xd7, 0xd7, 0xff, 0xff, | |
0xff, 0x00, 0x00, 0xff, 0x00, 0x5f, 0xff, 0x00, 0x87, 0xff, 0x00, 0xaf, | |
0xff, 0x00, 0xd7, 0xff, 0x00, 0xff, 0xff, 0x5f, 0x00, 0xff, 0x5f, 0x5f, | |
0xff, 0x5f, 0x87, 0xff, 0x5f, 0xaf, 0xff, 0x5f, 0xd7, 0xff, 0x5f, 0xff, | |
0xff, 0x87, 0x00, 0xff, 0x87, 0x5f, 0xff, 0x87, 0x87, 0xff, 0x87, 0xaf, | |
0xff, 0x87, 0xd7, 0xff, 0x87, 0xff, 0xff, 0xaf, 0x00, 0xff, 0xaf, 0x5f, | |
0xff, 0xaf, 0x87, 0xff, 0xaf, 0xaf, 0xff, 0xaf, 0xd7, 0xff, 0xaf, 0xff, | |
0xff, 0xd7, 0x00, 0xff, 0xd7, 0x5f, 0xff, 0xd7, 0x87, 0xff, 0xd7, 0xaf, | |
0xff, 0xd7, 0xd7, 0xff, 0xd7, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x5f, | |
0xff, 0xff, 0x87, 0xff, 0xff, 0xaf, 0xff, 0xff, 0xd7, 0xff, 0xff, 0xff, | |
0x08, 0x08, 0x08, 0x12, 0x12, 0x12, 0x1c, 0x1c, 0x1c, 0x26, 0x26, 0x26, | |
0x30, 0x30, 0x30, 0x3a, 0x3a, 0x3a, 0x44, 0x44, 0x44, 0x4e, 0x4e, 0x4e, | |
0x58, 0x58, 0x58, 0x62, 0x62, 0x62, 0x6c, 0x6c, 0x6c, 0x76, 0x76, 0x76, | |
0x80, 0x80, 0x80, 0x8a, 0x8a, 0x8a, 0x94, 0x94, 0x94, 0x9e, 0x9e, 0x9e, | |
0xa8, 0xa8, 0xa8, 0xb2, 0xb2, 0xb2, 0xbc, 0xbc, 0xbc, 0xc6, 0xc6, 0xc6, | |
0xd0, 0xd0, 0xd0, 0xda, 0xda, 0xda, 0xe4, 0xe4, 0xe4, 0xee, 0xee, 0xee, | |
] | |
def cc2bg(c): | |
if c == -1: | |
return u"49" | |
elif isinstance(c, int): | |
return u"48;5;%d" % c | |
elif len(c) == 3: | |
return u"48;2;%d;%d;%d" % (c[0], c[1], c[2]) | |
else: | |
return u"48;5;%d" % c | |
def cc2fg(c): | |
if c == -1: | |
return u"38;5;0" | |
elif isinstance(c, int): | |
return u"38;5;%d" % c | |
elif len(c) == 3: | |
return u"38;2;%d;%d;%d" % (c[0], c[1], c[2]) | |
else: | |
return u"38;5;%d" % c | |
def main(): | |
try: | |
sys.stdin = sys.stdin.buffer | |
except AttributeError: | |
pass | |
parser = argparse.ArgumentParser(description='Convert images into xterm-256color maps.') | |
parser.add_argument('input', metavar='INPUT', type=argparse.FileType('rb'), | |
help='the input image to process (- for stdin)') | |
parser.add_argument('output', metavar='OUTPUT', type=argparse.FileType('wb'), | |
nargs='?', default=sys.stdout, | |
help='output file to process (defaults to stdout)') | |
parser.add_argument('--dither', '-d', action='store_true', | |
help='use floyd-steinberg dithering') | |
parser.add_argument('--truecolor', '-t', action='store_true', | |
help='use true-color output') | |
parser.add_argument('--upper', '-u', action='store_true', | |
help=('use U+2580 UPPER HALF BLOCK for lower half-transparent pixels' | |
' - this doesn\'t always render correctly')) | |
args = parser.parse_args() | |
img = Image.open(args.input).convert('RGBA') | |
if img.size[0] * img.size[1] > 280 * 160: | |
print('Warning: Big image, did you forget to scale it down?', file=sys.stderr) | |
w, h = img.size | |
if h & 1: | |
h += 1 | |
i2 = img | |
img = Image.new('RGBA', (w, h)) | |
img.paste(i2, (0, 0)) | |
if not args.truecolor: | |
palette_img = Image.new('P', (1, 1)) | |
palette_img.putpalette(xterm256colors) | |
# evil undocumented PIL internals to get it to use our palette | |
img8 = img._new(img.im.convert('P', args.dither, palette_img.im)) | |
# make the encoding stuff work for both stdout and files in Python2 and 3. | |
tf = args.output | |
try: | |
if tf.encoding is None: | |
tf = codecs.getwriter('utf8')(tf) | |
except AttributeError: | |
tf = codecs.getwriter('utf8')(tf) | |
lastcc, lastfg, lastbg = None, None, None | |
for y in range(0, h, 2): | |
for x in range(w): | |
if args.truecolor: | |
cc = [img.getpixel((x, y))[:3], img.getpixel((x, y+1))[:3]] | |
else: | |
cc = [img8.getpixel((x, y)), img8.getpixel((x, y+1))] | |
a0, a1 = img.getpixel((x, y))[3], img.getpixel((x, y+1))[3] | |
if a0 == 0: | |
cc[0] = -1 | |
if a1 == 0: | |
cc[1] = -1 | |
if cc == lastcc: | |
tf.write(char) | |
elif cc[0] == cc[1]: | |
char = u' ' | |
if lastbg != cc[0]: | |
tf.write(u'\x1b[%sm%s' % (cc2bg(cc[0]), char)) | |
else: | |
tf.write(char) | |
lastbg = cc[0] | |
# Due to font rendering stupidity, this doesn't work as well as it | |
# should (see -u). This is for optimization only, so meh. | |
#elif lastcc == cc[::-1]: | |
#char = u'\u2584' if char == u'\u2580' else u'\u2580' | |
#tf.write(char) | |
else: | |
if cc[1] == -1 and args.upper: | |
fg, bg = cc | |
char = u'\u2580' | |
else: | |
bg, fg = cc | |
char = u'\u2584' | |
if lastfg == fg: | |
tf.write(u'\x1b[%sm%s' % (cc2bg(bg), char)) | |
elif lastbg == bg: | |
tf.write(u'\x1b[%sm%s' % (cc2fg(fg), char)) | |
else: | |
tf.write(u'\x1b[%s;%sm%s' % (cc2fg(fg), cc2bg(bg), char)) | |
lastfg, lastbg = fg, bg | |
lastcc = cc | |
tf.write(u'\x1b[0m\n') | |
lastcc, lastfg, lastbg = (-1, -1), None, -1 | |
tf.close() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Does it work with you now @ledlamp?