aboutsummaryrefslogtreecommitdiff
path: root/pixelterm
diff options
context:
space:
mode:
authorjaseg <s@jaseg.de>2013-09-16 15:33:01 +0200
committerjaseg <s@jaseg.de>2013-09-16 15:33:01 +0200
commit82cac0a5a211d573190253d3b40f89892bda713f (patch)
treeba67fa903d6b78c182df8b155e30ca46565e360b /pixelterm
parented223febd651ea32bf784fb054ecb5082276cc6b (diff)
downloadpixelterm-82cac0a5a211d573190253d3b40f89892bda713f.tar.gz
pixelterm-82cac0a5a211d573190253d3b40f89892bda713f.tar.bz2
pixelterm-82cac0a5a211d573190253d3b40f89892bda713f.zip
Fixed script installation
Diffstat (limited to 'pixelterm')
-rwxr-xr-xpixelterm/colorcube.py25
-rwxr-xr-xpixelterm/gifterm.py53
-rw-r--r--pixelterm/pixelterm.py109
-rwxr-xr-xpixelterm/pngmeta.py16
-rwxr-xr-xpixelterm/resolvecolor.py17
-rw-r--r--pixelterm/unpixelterm.py122
-rw-r--r--pixelterm/xtermcolors.py21
7 files changed, 363 insertions, 0 deletions
diff --git a/pixelterm/colorcube.py b/pixelterm/colorcube.py
new file mode 100755
index 0000000..b7fe65c
--- /dev/null
+++ b/pixelterm/colorcube.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+# Display an xterm-256color color palette on the terminal, including color ids
+
+reset_sequence = '\033[39;49m'
+
+def esc(i):
+ return '\033[48;5;'+str(i)+'m'
+
+def main():
+ print(''.join([str(i).ljust(4) for i in range(16)]))
+ print(' '.join([esc(i) for i in range(16)])+' ' + reset_sequence)
+
+ for j in range(6):
+ for k in range(6):
+ c = 16+j*6+k*6*6
+ print(''.join([str(c+i).ljust(4) for i in range(6)]))
+ print(' '.join([esc(c+i) for i in range(6)])+' ' + reset_sequence)
+
+ print(''.join([str(i).ljust(4) for i in range(16+6*6*6, 16+6*6*6+24)]))
+ print(' '.join([esc(i) for i in range(16+6*6*6, 16+6*6*6+24)])+' ' + reset_sequence)
+
+if __name__ == '__main__':
+ main()
+
diff --git a/pixelterm/gifterm.py b/pixelterm/gifterm.py
new file mode 100755
index 0000000..252e49c
--- /dev/null
+++ b/pixelterm/gifterm.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+
+import os, sys, argparse, os.path, json, time, signal, atexit, pixelterm
+from PIL import Image, GifImagePlugin, ImageSequence
+
+clear_screen = '\033[H\033[2J'
+cursor_invisible = '\033[?25l'
+cursor_visible = '\033[?25h'
+
+def main():
+ parser = argparse.ArgumentParser(description='Render pixel images on 256-color ANSI terminals')
+ parser.add_argument('image', type=str)
+ parser.add_argument('-s', '--size', type=str, help='Terminal size, [W]x[H]')
+ args = parser.parse_args()
+
+ tw, th = os.get_terminal_size()
+ th = th*2
+ if args.size:
+ tw, th = map(int, args.size.split('x'))
+
+ img = Image.open(args.image)
+ palette = img.getpalette()
+ last_frame = Image.new("RGBA", img.size)
+ frames = []
+
+ for frame in ImageSequence.Iterator(img):
+ #This works around a known bug in Pillow
+ #See also: http://stackoverflow.com/questions/4904940/python-converting-gif-frames-to-png
+ frame.putpalette(palette)
+ c = frame.convert("RGBA")
+
+ if img.info['background'] != img.info['transparency']:
+ last_frame.paste(c, c)
+ else:
+ last_frame = c
+
+ im = last_frame.copy()
+ im.thumbnail((tw, th), Image.NEAREST)
+ frames.append(pixelterm.termify_pixels(im))
+
+ print(cursor_invisible)
+ atexit.register(lambda:print(cursor_visible))
+ signal.signal(signal.SIGTERM, lambda signum, stack_frame: exit(1))
+
+ while True:
+ for frame in frames:
+ print(clear_screen, pixelterm.reset_sequence)
+ print(frame)
+ time.sleep(img.info['duration']/1000.0)
+
+if __name__ == '__main__':
+ main()
+
diff --git a/pixelterm/pixelterm.py b/pixelterm/pixelterm.py
new file mode 100644
index 0000000..4239b98
--- /dev/null
+++ b/pixelterm/pixelterm.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+
+import xtermcolors
+
+reset_sequence = '\033[39;49m'
+
+def termify_pixels(img):
+ sx, sy = img.size
+ out = ''
+
+ fg,bg = None,None
+ fgd,bgd = {},{}
+ def bgescape(color):
+ nonlocal bg, bgd
+ if bg == color:
+ return ''
+ bg=color
+ if color == (0,0,0,0):
+ return '\033[49m'
+ if color in bgd:
+ return bgd[color]
+ r,g,b,_ = color
+ bgd[color] = '\033[48;5;'+str(xtermcolors.closest_color(r,g,b))+'m'
+ return bgd[color]
+
+ def fgescape(color):
+ nonlocal fg, fgd
+ if fg == color:
+ return ''
+ fg=color
+ r,g,b,_ = color
+ fgd[color] = '\033[38;5;'+str(xtermcolors.closest_color(r,g,b))+'m'
+ return fgd[color]
+
+ def balloon(x,y):
+ if x+1 == img.size[0] or img.im.getpixel((x+1, y)) != (0,255,0,127):
+ w = 1
+ while x-w >= 0 and img.im.getpixel((x-w, y)) == (0,255,0,127):
+ w += 1
+ return '$balloon{}$'.format(w)
+ return ''
+
+ for y in range(0, sy, 2):
+ for x in range(sx):
+ coltop = img.im.getpixel((x, y))
+ colbot = img.im.getpixel((x, y+1)) if y+1 < img.size[1] else (0,0,0,0)
+
+ if coltop[3] == 127: #Control colors
+ out += reset_sequence
+ out += {(255, 0, 0, 127): lambda x,y:'$\\$',
+ (0, 0, 255, 127): lambda x,y:'$/$',
+ (0, 255, 0, 127): balloon
+ }.get(coltop, lambda x,y:' ')(x,y)
+ continue
+
+ if coltop[3] != 255:
+ coltop = (0,0,0,0)
+ if colbot[3] != 255:
+ colbot = (0,0,0,0)
+
+ #Da magicks: ▀█▄
+ c,cf = '▀','█'
+ te,be = fgescape,bgescape
+ if coltop == (0,0,0,0) or ((coltop == bg or colbot == fg) and not colbot == (0,0,0,0)):
+ c,cf,te,be = '▄',' ',be,te
+ if colbot == coltop:
+ c,te,be = cf,te,te
+ out += te(coltop) + be(colbot) + c
+ out = (out.rstrip() if bg == (0,0,0,0) else out) + '\n'
+ return out[:-1] + reset_sequence + '\n'
+
+def main():
+ import os, sys, argparse, os.path, json
+ from multiprocessing import Pool
+ from PIL import Image, PngImagePlugin
+
+ parser = argparse.ArgumentParser(description='Render pixel images on 256-color ANSI terminals')
+ parser.add_argument('image', type=str, nargs='*')
+ parser.add_argument('-d', '--output-dir', type=str, help='Output directory (if not given, output to stdout)')
+ args = parser.parse_args()
+
+ def convert(f):
+ img = Image.open(f).convert("RGBA")
+ if args.output_dir:
+ print(f)
+ foo, _, _ = f.rpartition('.png')
+ output = os.path.join(args.output_dir, os.path.basename(foo)+'.pony')
+ metadata = json.loads(img.info.get('pixelterm-metadata'))
+ comment = metadata.get('_comment')
+ if comment is not None:
+ del metadata['_comment']
+ comment = '\n'+comment
+ else:
+ comment = ''
+ metadataarea = '$$$\n' +\
+ '\n'.join([ '\n'.join([ k.upper() + ': ' + v for v in metadata[k] ]) for k in sorted(metadata.keys()) ]) +\
+ comment + '\n$$$\n'
+ with open(output, 'w') as of:
+ of.write(metadataarea)
+ of.write(termify_pixels(img))
+ else:
+ print(termify_pixels(img))
+
+ p = Pool()
+ p.map(convert, args.image)
+
+if __name__ == '__main__':
+ main()
+
diff --git a/pixelterm/pngmeta.py b/pixelterm/pngmeta.py
new file mode 100755
index 0000000..5ddde98
--- /dev/null
+++ b/pixelterm/pngmeta.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+
+import os, sys, argparse
+from PIL import Image, PngImagePlugin
+
+def main():
+ parser = argparse.ArgumentParser(description='Print PNG metadata')
+ parser.add_argument('image', type=str)
+ args = parser.parse_args()
+ img = Image.open(args.image)
+ for k, v in img.info.items():
+ print('{:15}: {}'.format(k, v))
+
+if __name__ == '__main__':
+ main()
+
diff --git a/pixelterm/resolvecolor.py b/pixelterm/resolvecolor.py
new file mode 100755
index 0000000..83c1330
--- /dev/null
+++ b/pixelterm/resolvecolor.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+
+def main():
+ import os, sys, argparse, os.path, json, re
+ import xtermcolors
+
+ # Resolve HTML-style hex RGB color codes to xterm-256color color numbers
+
+ if len(sys.argv) != 2:
+ print('Usage: resolvecolor.py #RRGGBB')
+ exit()
+
+ print(xtermcolors.closest_color(*[int(s, 16) for s in re.match('#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})', sys.argv[1]).groups()]))
+
+if __name__ == '__main__':
+ main()
+
diff --git a/pixelterm/unpixelterm.py b/pixelterm/unpixelterm.py
new file mode 100644
index 0000000..8d1c120
--- /dev/null
+++ b/pixelterm/unpixelterm.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+
+import os, sys, os.path
+from collections import defaultdict
+import xtermcolors
+from PIL import Image, PngImagePlugin
+try:
+ import re2 as re
+except:
+ import re
+
+def parse_escape_sequence(seq):
+ codes = list(map(int, seq[2:-1].split(';')))
+ fg, bg = None, None
+ i = 0
+ while i<len(codes):
+ if codes[i] in [38, 48]:
+ if codes[i+1] == 5:
+ c = xtermcolors.xterm_colors[codes[i+2]]
+ fg, bg = (c, bg) if codes[i] == 38 else (fg, c)
+ i += 2
+ elif codes[i] == 39:
+ fg = (0,0,0,0)
+ elif codes[i] == 49:
+ bg = (0,0,0,0)
+ elif codes[i] == 0:
+ fg, bg = (0,0,0,0), (0,0,0,0)
+ i += 1
+ return fg, bg
+
+def unpixelterm(text):
+ lines = text.split('\n')
+ metadata = defaultdict(list)
+ try:
+ first = lines.index('$$$')
+ second = lines[first+1:].index('$$$')
+ metadataarea = lines[first+1:second+1]
+ for i,l in enumerate(metadataarea):
+ parts = l.split(': ')
+ if len(parts) == 2:
+ k,v = parts
+ if k not in ['WIDTH', 'HEIGHT']:
+ metadata[k.lower()] += [v]
+ else:
+ metadata['_comment'] = '\n'.join(metadataarea[i:])
+ break
+ lines[first:] = lines[first+1+second+1:]
+ except:
+ pass
+
+ if lines[-1] == '\x1b[0m':
+ lines = lines[:-1]
+
+ h = len(lines)*2
+ w = max([ len(re.sub(r'\x1b\[[0-9;]+m|\$balloon.*\$|\$', '', line)) for line in lines ])
+ bw = int(re.search(r'\$balloon([0-9]*)\$', text).group(1) or '1')
+ if bw > w: #Fuck special cases.
+ w = bw
+ img = Image.new('RGBA', (w, h))
+ fg, bg = (0,0,0,0), (0,0,0,0)
+ x, y = 0, 0
+ for line in lines:
+ for escapeseq, specialstr, char in re.findall(r'(\x1b\[[0-9;]+m)|(\$[^$]+\$)|(.)', line, re.DOTALL):
+ if escapeseq:
+ nfg, nbg = parse_escape_sequence(escapeseq)
+ fg, bg = nfg or fg, nbg or bg
+ elif specialstr:
+ if specialstr == '$\\$':
+ img.putpixel((x, y), (255, 0, 0, 127))
+ img.putpixel((x, y+1), (255, 0, 0, 127))
+ x += 1
+ elif specialstr == '$/$':
+ img.putpixel((x, y), (0, 0, 255, 127))
+ img.putpixel((x, y+1), (0, 0, 255, 127))
+ x += 1
+ else: #(should be a) balloon
+ for i in range(x, x+bw):
+ img.putpixel((i, y), (0, 255, 0, 127))
+ img.putpixel((i, y+1), (0, 255, 0, 127))
+ x += bw
+ elif char:
+ #Da magicks: ▀█▄
+ c = {' ': (bg, bg),
+ '█': (fg, fg),
+ '▀': (fg, bg),
+ '▄': (bg, fg)}[char]
+ img.putpixel((x, y), c[0])
+ img.putpixel((x, y+1), c[1])
+ x += 1
+ x, y = 0, y+2
+ return img, metadata
+
+def main():
+ import argparse, json
+
+ parser = argparse.ArgumentParser(description='Convert images rendered by pixelterm-like utilities back to PNG')
+ parser.add_argument('-v', '--verbose', action='store_true')
+ output_group = parser.add_mutually_exclusive_group()
+ output_group.add_argument('-o', '--output', type=str, help='Output file name, defaults to ${input%.pony}.png')
+ output_group.add_argument('-d', '--output-dir', type=str, help='Place output files here')
+ parser.add_argument('input', type=argparse.FileType('r'), nargs='+')
+ args = parser.parse_args()
+ if len(args.input) > 1 and args.output:
+ parser.print_help()
+ print('You probably do not want to overwrite the given output file {} times.'.format(len(args.input)))
+ sys.exit(1)
+
+ for f in args.input:
+ if len(args.input) > 1:
+ print(f.name)
+ img, metadata = unpixelterm(f.read())
+ pnginfo = PngImagePlugin.PngInfo()
+ pnginfo.add_text('pixelterm-metadata', json.dumps(metadata))
+ foo, _, _ = f.name.rpartition('.pony')
+ output = args.output or foo+'.png'
+ if args.output_dir:
+ output = os.path.join(args.output_dir, os.path.basename(output))
+ img.save(output, 'PNG', pnginfo=pnginfo)
+
+if __name__ == '__main__':
+ main()
+
diff --git a/pixelterm/xtermcolors.py b/pixelterm/xtermcolors.py
new file mode 100644
index 0000000..36cddc6
--- /dev/null
+++ b/pixelterm/xtermcolors.py
@@ -0,0 +1,21 @@
+
+xterm_colors = []
+
+# This is ripped out of pygments
+# I leave out the 16 standard colors since people tend to re-define them to their liking.
+
+# colors 16..232: the 6x6x6 color cube
+_valuerange = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff)
+for i in range(217):
+ r = _valuerange[(i // 36) % 6]
+ g = _valuerange[(i // 6) % 6]
+ b = _valuerange[i % 6]
+ xterm_colors.append((r, g, b))
+
+# colors 233..253: grayscale
+for i in range(1, 24):
+ v = 8 + i * 10
+ xterm_colors.append((v, v, v))
+
+def closest_color(r, g, b):
+ return 16+min([ ((r-rt)**2+(g-gt)**2+(b-bt)**2, i) for i,(rt,gt,bt) in enumerate(xterm_colors) ])[1]