├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── img └── screenshot.png └── src └── ascii.py /.gitignore: -------------------------------------------------------------------------------- 1 | data/** 2 | **.sw* 3 | **.pyc 4 | **__pycache__** 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Software Copyright License Agreement (BSD License) 2 | ================================================== 3 | 4 | Copyright (c) 2013, Joshua Komoroske 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of the owner nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL JOSHUA KOMOROSKE BE LIABLE FOR ANY 23 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCE="./src/ascii.py" 2 | TARGET="/usr/bin/ascii" 3 | 4 | 5 | all: install 6 | 7 | install: 8 | install -m 755 $(SOURCE) $(TARGET) 9 | 10 | uninstall: 11 | rm $(TARGET) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ascii 2 | ===== 3 | 4 | An image viewer for your terminal 5 | 6 | 7 | ![ascii running inside urxvt](https://raw.github.com/joshdk/ascii/master/img/screenshot.png "ascii running inside urxvt") 8 | 9 | 10 | Setup 11 | ----- 12 | 13 | ### Install 14 | # make install 15 | 16 | 17 | ### Uninstall 18 | # make uninstall 19 | 20 | 21 | Running 22 | ------- 23 | 24 | $ ascii IMAGE [CHARSET] 25 | 26 | $ ascii picture.jpg 27 | 28 | $ ascii picture.jpg " -+=$#" 29 | 30 | 31 | Notes 32 | ----- 33 | 34 | ### Dependencies 35 | * Python2 36 | * PIL 37 | -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdk/ascii/58debb571ceb9cc5c17408b5b5e606281fbc3ca9/img/screenshot.png -------------------------------------------------------------------------------- /src/ascii.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """ 3 | A library & standalone executable for displaying images as ascii art. 4 | """ 5 | 6 | 7 | from __future__ import print_function, division 8 | from PIL import Image as img 9 | import sys 10 | 11 | 12 | 13 | 14 | #{{{ Custom colors 15 | COLORS = [ 16 | (0x21, 0x21, 0x21), 17 | (0xC7, 0x23, 0x41), 18 | (0x85, 0xC6, 0x00), 19 | (0xFF, 0x91, 0x00), 20 | (0x33, 0x71, 0xBB), 21 | (0x9B, 0x2A, 0x65), 22 | (0x44, 0x6A, 0x6B), 23 | (0xB0, 0xB0, 0xB0), 24 | (0x48, 0x53, 0x56), 25 | (0xFF, 0x5D, 0x4A), 26 | (0xC2, 0xF4, 0x56), 27 | (0xF6, 0x9D, 0x3C), 28 | (0x68, 0x9A, 0xD6), 29 | (0x9A, 0x4E, 0x76), 30 | (0x76, 0xB2, 0xB3), 31 | (0xE2, 0xE2, 0xE2) 32 | ] 33 | #}}} 34 | 35 | 36 | #{{{ Resize image 37 | def _resize(im): 38 | """ 39 | Take a PIL Image, and return a PIL Image, resized to the current terminal width. 40 | """ 41 | import os 42 | def ioctl_GWINSZ(fd): 43 | try: 44 | import fcntl, termios, struct, os 45 | cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) 46 | except: 47 | return 48 | return cr 49 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 50 | if not cr: 51 | try: 52 | fd = os.open(os.ctermid(), os.O_RDONLY) 53 | cr = ioctl_GWINSZ(fd) 54 | os.close(fd) 55 | except: 56 | pass 57 | if not cr: 58 | cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80)) 59 | 60 | (theight, twidth) = int(cr[0]), int(cr[1]) 61 | (iwidth, iheight) = im.size 62 | 63 | nwidth = twidth 64 | nheight = iheight / (iwidth/twidth) 65 | nheight /= 2 66 | nwidth = int(nwidth) 67 | nheight = int(nheight) 68 | 69 | return im.resize((nwidth, nheight)) 70 | #}}} 71 | 72 | 73 | #{{{ Grayscale image 74 | def _grayscale(im): 75 | """ 76 | Take a PIL Image, and return a PIL Image, that had been converted to grayscale 77 | """ 78 | im = im.convert('F') 79 | data = im.getdata() 80 | 81 | cmin, cmax = min(data), max(data) 82 | scale = 255.0 / (cmax-cmin or 1) 83 | 84 | pixels = [int((value-cmin)*scale+0.449) for value in data] 85 | 86 | gim = img.new('L', im.size) 87 | gim.putdata(pixels) 88 | return gim 89 | #}}} 90 | 91 | 92 | #{{{ Partition image into color groups 93 | def _partition(im, colors): 94 | """ 95 | Take a PIL Image, and return an array of colors, that map to terminal colors. 96 | """ 97 | import math 98 | im = im.convert('RGB') 99 | 100 | (width, height) = im.size 101 | data = [] 102 | line = [] 103 | 104 | for i, pixel in enumerate(im.getdata()): 105 | cbest = None 106 | for j, color in enumerate(colors): 107 | 108 | cdist = ( 109 | (pixel[0] - color[0])**2 + 110 | (pixel[1] - color[1])**2 + 111 | (pixel[2] - color[2])**2 112 | ) 113 | 114 | if cbest is None or cdist < cbest[0]: 115 | cbest = (cdist, j) 116 | line.append(cbest[1]) 117 | 118 | if i % width == width - 1: 119 | data.append(line) 120 | line = [] 121 | return data 122 | #}}} 123 | 124 | 125 | #{{{ Convert an image to ascii 126 | def ascii_map(im, chars=' .,-:;!*=$#@'): 127 | """ 128 | Take a PIL Image, and return an array of strings, that contain ascii data. 129 | """ 130 | colors = [] 131 | chunk = 255.0 / len(chars) 132 | offset = chunk / 2.0 133 | for i in range(len(chars)): 134 | val = chunk * i + offset 135 | colors.append((val, val, val)) 136 | 137 | return _partition(im, colors) 138 | #}}} 139 | 140 | 141 | def color_map(im, colors): 142 | return _partition(im, colors) 143 | 144 | 145 | #{{{ Display ascii data 146 | def render(char_map, characters, color_map=None): 147 | """ 148 | Takes ascii & color data, and display it on the screen 149 | """ 150 | import curses 151 | 152 | curses.setupterm() 153 | fg_normal = curses.tigetstr('sgr0') 154 | fg_colors = [curses.tparm(curses.tigetstr('setaf'), i) for i in range(8)] 155 | attr_bold = curses.tparm(curses.tigetstr('bold'), curses.A_BOLD) 156 | attr_normal = curses.tparm(curses.tigetstr('sgr0'), curses.A_NORMAL) 157 | 158 | def set_color(fg=None): 159 | if fg is None: 160 | return fg_normal + attr_normal 161 | if fg in range(0, 8): 162 | return fg_colors[fg] 163 | if fg in range(8, 16): 164 | return fg_colors[fg-8] + attr_bold 165 | return '' 166 | 167 | for y in range(len(char_map)): 168 | for x in range(len(char_map[y])): 169 | if color_map is not None: 170 | print(set_color(color_map[y][x]), end='') 171 | print(characters[char_map[y][x]], end='') 172 | print('') 173 | 174 | if color_map is not None: 175 | print(set_color(), end='') 176 | #}}} 177 | 178 | 179 | 180 | 181 | #{{{ Main 182 | def main(argv=None): 183 | """ 184 | A main function to be used when ascii is run from the command line. 185 | """ 186 | if argv is None: 187 | argv = sys.argv 188 | argc = len(argv) 189 | 190 | if argc <= 1: 191 | print('%s: error: insufficient arguments' % (argv[0])) 192 | return 1 193 | 194 | im = None 195 | chars = u' .,-:;!*=$#@' 196 | 197 | if argc >= 2: 198 | try: 199 | im = img.open(argv[1]) 200 | except: 201 | print('%s: error: could not open %s' % (argv[0], argv[1])) 202 | return 1 203 | 204 | if argc >= 3: 205 | chars = argv[2] 206 | chars = chars.decode('utf-8') 207 | if len(chars) < 1: 208 | print('%s: error: chars must contain at least one character' % (argv[0])) 209 | return 1 210 | 211 | rim = _resize(im) 212 | render(ascii_map(_grayscale(rim), chars), chars, color_map(rim, COLORS)) 213 | 214 | return 0 215 | 216 | 217 | if __name__ == '__main__': 218 | sys.exit(main()) 219 | #}}} 220 | --------------------------------------------------------------------------------