├── .gitignore ├── LICENSE ├── README.md ├── matplotlib-sixel ├── __init__.py ├── sixel.py └── xterm.py ├── requirements.txt ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.swo 3 | *.swp 4 | *.idea 5 | *.pyc 6 | build/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Markus Gräb 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Matplotlib-sixel backend 2 | ======================== 3 | 4 | A matplotlib backend which outputs sixel graphics onto the terminal. 5 | The code is inspired by the ipython-notebook matplotlib backend. 6 | 7 | 8 | TODO: 9 | 10 | * An Exception breaks the terminal 11 | * Support other terminals than xterm 12 | * Resize has still some problems. 13 | The figures are often too big for small windows 14 | 15 | Dependencies 16 | ------------ 17 | 18 | * xterm with Sixel support configured 19 | * imagemagick (for converting the graphics) 20 | * matplotlib and numpy 21 | 22 | Installation 23 | ------------- 24 | python setup.py install 25 | 26 | Usage 27 | ----- 28 | 29 | 30 | import matplotlib 31 | matplotlib.use('module://matplotlib-sixel') 32 | from pylab import * 33 | plt.plot(sin(arange(100) / 10)) 34 | show() 35 | 36 | # --> now shows the plot inside the xterm window 37 | -------------------------------------------------------------------------------- /matplotlib-sixel/__init__.py: -------------------------------------------------------------------------------- 1 | from .sixel import * 2 | -------------------------------------------------------------------------------- /matplotlib-sixel/sixel.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | """ 4 | A matplotlib backend for displaying figures via sixel terminal graphics 5 | 6 | Based on the ipykernel source code "backend_inline.py" 7 | 8 | # Copyright (c) IPython Development Team. 9 | # Distributed under the terms of the Modified BSD License. 10 | 11 | """ 12 | 13 | 14 | import matplotlib 15 | 16 | from matplotlib._pylab_helpers import Gcf 17 | from subprocess import Popen, PIPE 18 | 19 | from .xterm import xterm_pixels 20 | 21 | from matplotlib.backends.backend_agg import new_figure_manager, FigureCanvasAgg 22 | new_figure_manager # for check 23 | 24 | 25 | def resize_fig(figure): 26 | """ resize figure size, so that it fits into the terminal 27 | 28 | Checks the width and height 29 | Only makes the figure smaller 30 | 31 | """ 32 | dpi = figure.get_dpi() 33 | size = figure.get_size_inches() # w, h 34 | pixel_size = size * dpi 35 | 36 | pixel_factor = pixel_size / xterm_pixels() 37 | 38 | factor = max(max(pixel_factor), 1) 39 | 40 | size /= factor 41 | 42 | figure.set_size_inches(size) 43 | print(size) 44 | 45 | 46 | def display(figure): 47 | """ Display figure on stdout as sixel graphic """ 48 | 49 | resize_fig(figure) 50 | 51 | p = Popen(["convert", "-colors", '16', 'png:-', 'sixel:-'], stdin=PIPE) 52 | figure.savefig(p.stdin, format='png') 53 | p.stdin.close() 54 | p.wait() 55 | 56 | 57 | def show(close=False, block=None): 58 | """Show all figures as SVG/PNG payloads sent to the IPython clients. 59 | 60 | Parameters 61 | ---------- 62 | close : bool, optional 63 | If true, a ``plt.close('all')`` call is automatically issued after 64 | sending all the figures. If this is set, the figures will entirely 65 | removed from the internal list of figures. 66 | block : Not used. 67 | The `block` parameter is a Matplotlib experimental parameter. 68 | We accept it in the function signature for compatibility with other 69 | backends. 70 | """ 71 | try: 72 | for figure_manager in Gcf.get_all_fig_managers(): 73 | display(figure_manager.canvas.figure) 74 | finally: 75 | show._to_draw = [] 76 | # only call close('all') if any to close 77 | # close triggers gc.collect, which can be slow 78 | if close and Gcf.get_all_fig_managers(): 79 | matplotlib.pyplot.close('all') 80 | 81 | 82 | # This flag will be reset by draw_if_interactive when called 83 | show._draw_called = False 84 | # list of figures to draw when flush_figures is called 85 | show._to_draw = [] 86 | 87 | 88 | def draw_if_interactive(): 89 | """ 90 | Is called after every pylab drawing command 91 | """ 92 | # signal that the current active figure should be sent at the end of 93 | # execution. Also sets the _draw_called flag, signaling that there will be 94 | # something to send. At the end of the code execution, a separate call to 95 | # flush_figures() will act upon these values 96 | manager = Gcf.get_active() 97 | if manager is None: 98 | return 99 | fig = manager.canvas.figure 100 | 101 | # Hack: matplotlib FigureManager objects in interacive backends (at least 102 | # in some of them) monkeypatch the figure object and add a .show() method 103 | # to it. This applies the same monkeypatch in order to support user code 104 | # that might expect `.show()` to be part of the official API of figure 105 | # objects. 106 | # For further reference: 107 | # https://github.com/ipython/ipython/issues/1612 108 | # https://github.com/matplotlib/matplotlib/issues/835 109 | 110 | if not hasattr(fig, 'show'): 111 | # Queue up `fig` for display 112 | fig.show = lambda *a: display(fig) 113 | 114 | # If matplotlib was manually set to non-interactive mode, this function 115 | # should be a no-op (otherwise we'll generate duplicate plots, since a user 116 | # who set ioff() manually expects to make separate draw/show calls). 117 | if not matplotlib.is_interactive(): 118 | return 119 | 120 | # ensure current figure will be drawn, and each subsequent call 121 | # of draw_if_interactive() moves the active figure to ensure it is 122 | # drawn last 123 | try: 124 | show._to_draw.remove(fig) 125 | except ValueError: 126 | # ensure it only appears in the draw list once 127 | pass 128 | # Queue up the figure for drawing in next show() call 129 | show._to_draw.append(fig) 130 | show._draw_called = True 131 | 132 | 133 | def flush_figures(): 134 | """Send all figures that changed 135 | 136 | This is meant to be called automatically and will call show() if, during 137 | prior code execution, there had been any calls to draw_if_interactive. 138 | 139 | This function is meant to be used as a post_execute callback in IPython, 140 | so user-caused errors are handled with showtraceback() instead of being 141 | allowed to raise. If this function is not called from within IPython, 142 | then these exceptions will raise. 143 | """ 144 | if not show._draw_called: 145 | return 146 | 147 | try: 148 | # exclude any figures that were closed: 149 | active = set([fm.canvas.figure for fm in Gcf.get_all_fig_managers()]) 150 | for fig in [fig for fig in show._to_draw if fig in active]: 151 | display(fig) 152 | finally: 153 | # clear flags for next round 154 | show._to_draw = [] 155 | show._draw_called = False 156 | 157 | 158 | # Changes to matplotlib in version 1.2 requires a mpl backend to supply a 159 | # default figurecanvas. This is set here to a Agg canvas 160 | # See https://github.com/matplotlib/matplotlib/pull/1125 161 | FigureCanvas = FigureCanvasAgg 162 | -------------------------------------------------------------------------------- /matplotlib-sixel/xterm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import curses 3 | 4 | from sys import stdin, stdout 5 | 6 | 7 | class raw_terminal(object): 8 | """ change terminal mode that the input is unbuffered """ 9 | def __init__(self, fd): 10 | self.fd = fd 11 | 12 | def __enter__(self): 13 | curses.initscr() 14 | 15 | if not curses.termname().startswith(b'xterm'): 16 | raise Exception(f"This backend only supports xterm as terminal emulator. Got: '{curses.termname()}'.") 17 | 18 | curses.cbreak() 19 | 20 | def __exit__(self, type, value, traceback): 21 | curses.endwin() 22 | 23 | 24 | def read_until(fd, delim): 25 | out = "" 26 | c = stdin.read(1) 27 | while c != delim: 28 | out += c 29 | c = stdin.read(1) 30 | return out 31 | 32 | 33 | def xterm_pixels(): 34 | """ Get the width and heigth in pixels of the xterm window """ 35 | code = "\x1b\x5b\x31\x34\x74\x0a" 36 | 37 | with raw_terminal(stdin): 38 | stdout.write(code) 39 | stdout.flush() 40 | 41 | c = stdin.read(1) 42 | assert c == '\x1b' 43 | read_until(stdin, ';') 44 | height = read_until(stdin, ';') 45 | width = read_until(stdin, 't') 46 | 47 | return (int(width), int(height)) 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib 2 | numpy 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='matplotlib-sixel', 6 | version='0.1', 7 | description='Matplotlib backend for showing sixel graphics', 8 | author='Markus Gräb', 9 | author_email='markus.graeb@gmail.com', 10 | url='https://github.com/koppa/matplotlib-sixel', 11 | packages=['matplotlib-sixel']) 12 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import matplotlib 4 | 5 | matplotlib.use('module://matplotlib-sixel') 6 | 7 | from pylab import * 8 | 9 | plot([1, 2, 3]) 10 | show() 11 | --------------------------------------------------------------------------------