stdgif 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. stdgif -- dumb software that shows gifs on the cli.
  5. The super smart algorithm (that I had nothing to do with) that shows
  6. amazing ansi renderings of images was ported from Stefan Haustein's
  7. TerminalImageViewer <https://github.com/stefanhaustein/TerminalImageViewer>
  8. Requirements: requests <http://docs.python-requests.org>,
  9. Pillow <https://python-pillow.org/>
  10. usage: stdgif [-h] [-w WIDTH] [-f] [-d DELAY] [-o OUTPUT] [-s SEPERATOR] img
  11. positional arguments:
  12. img File to show
  13. optional arguments:
  14. -h, --help show this help message and exit
  15. -w WIDTH, --width WIDTH
  16. Width of file to show
  17. -f, --forever Loop forever
  18. -d DELAY, --delay DELAY
  19. The delay between images that make up a gif
  20. -o OUTPUT, --output OUTPUT
  21. Generated bash script path - suitable for sourcing
  22. from your .bashrc
  23. -s SEPERATOR, --seperator SEPERATOR
  24. Print the seperator between frames of a gif (this can
  25. be useful if piping output into another file or
  26. program)
  27. Copyright (c) 2016 Tyler Cipriani <tyler@tylercipriani.com>
  28. This program is free software: you can redistribute it and/or modify
  29. it under the terms of the GNU General Public License as published by
  30. the Free Software Foundation, either version 3 of the License, or
  31. (at your option) any later version.
  32. This program is distributed in the hope that it will be useful,
  33. but WITHOUT ANY WARRANTY; without even the implied warranty of
  34. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  35. GNU General Public License for more details.
  36. You should have received a copy of the GNU General Public License
  37. along with this program. If not, see <http://www.gnu.org/licenses/>.
  38. """
  39. from __future__ import print_function
  40. import argparse
  41. import math
  42. import os
  43. import sys
  44. import tempfile
  45. import time
  46. import requests
  47. from PIL import Image
  48. FRAMES = {}
  49. BITMAPS = {
  50. 0x00000000: ' ',
  51. # Block graphics
  52. 0x0000000f: u'\u2581', # lower 1/8
  53. 0x000000ff: u'\u2582', # lower 1/4
  54. 0x00000fff: u'\u2583',
  55. 0x0000ffff: u'\u2584', # lower 1/2
  56. 0x000fffff: u'\u2585',
  57. 0x00ffffff: u'\u2586', # lower 3/4
  58. 0x0fffffff: u'\u2587',
  59. 0xeeeeeeee: u'\u258a', # left 3/4
  60. 0xcccccccc: u'\u258c', # left 1/2
  61. 0x88888888: u'\u258e', # left 1/4
  62. 0x0000cccc: u'\u2596', # quadrant lower left
  63. 0x00003333: u'\u2597', # quadrant lower right
  64. 0xcccc0000: u'\u2598', # quadrant upper left
  65. 0xcccc3333: u'\u259a', # diagonal 1/2
  66. 0x33330000: u'\u259d', # quadrant upper right
  67. # Line drawing subset: no double lines, no complex light lines
  68. # Simple light lines duplicated because there is no center pixel int
  69. # the 4x8 matrix
  70. 0x000ff000: u'\u2501', # Heavy horizontal
  71. 0x66666666: u'\u2503', # Heavy vertical
  72. 0x00077666: u'\u250f', # Heavy down and right
  73. 0x000ee666: u'\u2513', # Heavy down and left
  74. 0x66677000: u'\u2517', # Heavy up and right
  75. 0x666ee000: u'\u251b', # Heavy up and left
  76. 0x66677666: u'\u2523', # Heavy vertical and right
  77. 0x666ee666: u'\u252b', # Heavy vertical and left
  78. 0x000ff666: u'\u2533', # Heavy down and horizontal
  79. 0x666ff000: u'\u253b', # Heavy up and horizontal
  80. 0x666ff666: u'\u254b', # Heavy cross
  81. 0x000cc000: u'\u2578', # Bold horizontal left
  82. 0x00066000: u'\u2579', # Bold horizontal up
  83. 0x00033000: u'\u257a', # Bold horizontal right
  84. 0x00066000: u'\u257b', # Bold horizontal down
  85. 0x06600660: u'\u254f', # Heavy double dash vertical
  86. 0x000f0000: u'\u2500', # Light horizontal
  87. 0x0000f000: u'\u2500',
  88. 0x44444444: u'\u2502', # Light vertical
  89. 0x22222222: u'\u2502',
  90. 0x000e0000: u'\u2574', # light left
  91. 0x0000e000: u'\u2574', # light left
  92. 0x44440000: u'\u2575', # light up
  93. 0x22220000: u'\u2575', # light up
  94. 0x00030000: u'\u2576', # light right
  95. 0x00003000: u'\u2576', # light right
  96. 0x00004444: u'\u2575', # light down
  97. 0x00002222: u'\u2575', # light down
  98. # Misc technical
  99. 0x44444444: u'\u23a2', # [ extension
  100. 0x22222222: u'\u23a5', # ] extension
  101. # 12345678
  102. 0x0f000000: u'\u23ba', # Horizontal scanline 1
  103. 0x00f00000: u'\u23bb', # Horizontal scanline 3
  104. 0x00000f00: u'\u23bc', # Horizontal scanline 7
  105. 0x000000f0: u'\u23bd', # Horizontal scanline 9
  106. # Geometrical shapes. Tricky because some of them are too wide.
  107. 0x00066000: u'\u25aa', # Black small square
  108. }
  109. def esc(*args):
  110. """Escape ansi codes."""
  111. return '\x1b[%sm' % ';'.join(str(arg) for arg in args)
  112. def clamp(val, small, large):
  113. """Clamp val to a range."""
  114. return min(max(int(val), small), large)
  115. def rgb_to_tput(rgb):
  116. """Convert rgb string (like "0, 0, 0") into a list (like [0, 0, 0])."""
  117. return [clamp(c, 0, 255) for c in rgb.split(',')]
  118. def make_char(c, fg, bg):
  119. """Return escaped ansi char."""
  120. if fg[0] == fg[1] == fg[2] == bg[0] == bg[1] == bg[2] == 0:
  121. return '\x1b[0m '
  122. return '{}{}{}'.format(
  123. esc(38, 2, fg[0], fg[1], fg[2]),
  124. esc(48, 2, bg[0], bg[1], bg[2]),
  125. c.encode('utf-8'))
  126. def make_percent(num, den):
  127. """Make a numberator and a denominator into a percentage."""
  128. return math.floor(100.0 * (float(num) / max(den, 1)))
  129. def handle_pixel(img, x, y):
  130. """Turn a 4x8 dict of rgb tuples into a single-ansi char."""
  131. w, h = img.size
  132. x_offset = min(x + 4, w)
  133. y_offset = min(y + 8, h)
  134. max_rgb = [0, 0, 0]
  135. min_rgb = [255, 255, 255]
  136. for i in range(x, x_offset):
  137. for j in range(y, y_offset):
  138. rgba = img.getpixel((i, j))
  139. for channel in range(0, 3):
  140. max_rgb[channel] = max(max_rgb[channel], rgba[channel])
  141. min_rgb[channel] = min(min_rgb[channel], rgba[channel])
  142. split_channel = 0
  143. best_split = 0
  144. for channel in range(0, 3):
  145. split = max_rgb[channel] - min_rgb[channel]
  146. if split > best_split:
  147. best_split = split
  148. split_channel = channel
  149. split_val = min_rgb[split_channel] + best_split / 2
  150. bits = 0
  151. bg_color = []
  152. fg_color = []
  153. for j in range(y, y_offset):
  154. for i in range(x, x_offset):
  155. rgba = img.getpixel((i, j))
  156. r, g, b, _ = rgba
  157. bits = bits << 1
  158. index = rgba[split_channel]
  159. num = (index & 255)
  160. if int(num) > split_val:
  161. bits |= 1
  162. fg_color.append((r, g, b))
  163. else:
  164. bg_color.append((r, g, b))
  165. avg_bg_rgb = [sum(color) / len(color) for color in zip(*bg_color)]
  166. avg_fg_rgb = [sum(color) / len(color) for color in zip(*fg_color)]
  167. if not avg_fg_rgb:
  168. avg_fg_rgb = [0, 0, 0]
  169. if not avg_bg_rgb:
  170. avg_fg_rgb = [0, 0, 0]
  171. best_diff = sys.maxint
  172. inverted = False
  173. for bitmap in list(BITMAPS.keys()):
  174. xor = bin(bitmap ^ bits)
  175. diff = xor.count('1')
  176. if diff < best_diff:
  177. character = BITMAPS[bitmap]
  178. best_diff = diff
  179. inverted = False
  180. # make sure to & the ~ with 0xffffffff to fill up all 32 bits
  181. not_xor = bin((~bitmap & 0xffffffff) ^ bits)
  182. diff = not_xor.count('1')
  183. if diff < best_diff:
  184. character = BITMAPS[bitmap]
  185. best_diff = diff
  186. inverted = True
  187. if best_diff > 10:
  188. inverted = False
  189. character = u' \u2591\u2592\u2593\u2588'[
  190. min(4, len(fg_color) * 5 / 32)]
  191. if inverted:
  192. tmp = avg_bg_rgb
  193. avg_bg_rgb = avg_fg_rgb
  194. avg_fg_rgb = tmp
  195. return make_char(character, avg_fg_rgb, avg_bg_rgb)
  196. def frame_to_ansi(frame):
  197. """Convert an image into 4x8 chunks and return ansi."""
  198. w, h = frame.size
  199. buf = '\x1b[0m'
  200. for y in range(0, h, 8):
  201. for x in range(0, w, 4):
  202. buf += handle_pixel(frame, x, y)
  203. buf += '\n'
  204. return buf
  205. def die(out=sys.stdout, gif=None):
  206. """Unbork the terminal."""
  207. if gif:
  208. os.remove(gif)
  209. if out.name != '<stdout>':
  210. print('printf ', file=out, end='')
  211. print('\x1b[34h\x1b[?25h\x1b[0m\x1b[0m', file=out)
  212. sys.exit(0)
  213. def is_url(img):
  214. """True if image is a url."""
  215. return img.startswith('http://') or img.startswith('https://')
  216. def main():
  217. ap = argparse.ArgumentParser()
  218. ap.add_argument('-w', '--width', type=int,
  219. help='Width of file to show', default=80)
  220. ap.add_argument('-f', '--forever', action='store_true',
  221. help='Loop forever')
  222. ap.add_argument('-d', '--delay', type=float, default=0.1,
  223. help='The delay between images that make up a gif')
  224. ap.add_argument('-o', '--output', type=argparse.FileType('wb', 0),
  225. help='Generated bash script path - suitable for sourcing '
  226. 'from your .bashrc', default=sys.stdout)
  227. ap.add_argument('-s', '--seperator', type=str, default=None,
  228. help='Print the seperator between frames of a gif '
  229. '(this can be useful if piping output into '
  230. 'another file or program)')
  231. ap.add_argument('img', type=str, help='File to show')
  232. args = ap.parse_args()
  233. img = args.img
  234. gif_path = None
  235. if is_url(img):
  236. r = requests.get(img, stream=True)
  237. r.raise_for_status()
  238. gif = tempfile.NamedTemporaryFile(prefix='gifup-', delete=False)
  239. gif_path = gif.name
  240. with open(gif_path, 'w') as f:
  241. f.write(r.raw.read())
  242. img = gif.name
  243. img = Image.open(img)
  244. img.load()
  245. w = args.width * 4
  246. ow, oh = img.size
  247. h = oh * w / ow
  248. size = (w, h)
  249. offset = 0
  250. frames_filled = False
  251. total_frames = 0
  252. while True:
  253. try:
  254. img.seek(offset)
  255. if not frames_filled:
  256. total_frames += 1
  257. offset += 1
  258. continue
  259. frame = Image.new('RGBA', img.size)
  260. frame.paste(img, (0, 0), img.convert('RGBA'))
  261. frame = frame.resize(size)
  262. fmt = '\rLoading frames: {:.2f}% ({} of {})'
  263. print(fmt.format(make_percent(offset, total_frames),
  264. offset, total_frames),
  265. file=sys.stderr,
  266. end='')
  267. if not FRAMES.get(offset):
  268. FRAMES[offset] = frame_to_ansi(frame)
  269. offset += 1
  270. except EOFError:
  271. if not frames_filled:
  272. frames_filled = True
  273. offset = 0
  274. continue
  275. break
  276. except KeyboardInterrupt:
  277. die(out=args.output, gif=gif_path)
  278. offset = 0
  279. # Clear the \r from sys.stderr
  280. print('', file=sys.stderr)
  281. # If we're not writing to stdout, we're generating a bash script
  282. if args.output.name != '<stdout>':
  283. print('#!/usr/bin/env bash', file=args.output)
  284. while True:
  285. try:
  286. if args.output.name != '<stdout>':
  287. print('cat <<FILE{}'.format(offset), file=args.output)
  288. print('\r\x1b[{}A'.format(h), end='', file=args.output)
  289. print(FRAMES[offset], end='', file=args.output)
  290. if args.seperator:
  291. print(args.seperator, file=args.output)
  292. if args.output.name != '<stdout>':
  293. print('FILE{}'.format(offset), file=args.output)
  294. if args.output.name == '<stdout>':
  295. time.sleep(args.delay)
  296. else:
  297. print('sleep {}'.format(args.delay), file=args.output)
  298. offset += 1
  299. except KeyError:
  300. if args.forever and args.output.name == '<stdout>':
  301. offset = 0
  302. continue
  303. if args.output.name != '<stdout>':
  304. print('\nFILE{}\n'.format(offset), file=args.output)
  305. print('printf \x1b[H\x1b[J', file=args.output)
  306. break
  307. except KeyboardInterrupt:
  308. break
  309. die(out=args.output, gif=gif_path)
  310. if __name__ == '__main__':
  311. main()