Skip to content

Instantly share code, notes, and snippets.

@marcan
Last active September 28, 2022 19:11
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save marcan/f02cfe8f7d848b748920ca6738c4e858 to your computer and use it in GitHub Desktop.
Save marcan/f02cfe8f7d848b748920ca6738c4e858 to your computer and use it in GitHub Desktop.
Image to xterm-256 Unicode block art converter
#!/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()
Copy link

ghost commented Oct 30, 2021

Traceback (most recent call last):
  File "/mnt/c/Users/me/Desktop/../xt256img.py", line 183, in <module>
    main()
  File "/mnt/c/Users/me/Desktop/../xt256img.py", line 174, in main
    tf.write(u'\x1b[%s;%sm%s' % (cc2fg(fg), cc2bg(bg), char))
  File "/mnt/c/Users/me/Desktop/../xt256img.py", line 84, in cc2fg
    elif len(c) == 3:
TypeError: object of type 'int' has no len()

@waot
Copy link

waot commented Feb 21, 2022

Does it work with you now @ledlamp?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment