diff --git a/IPython/core/display.py b/IPython/core/display.py index 2733e9f..0e64c11 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -20,6 +20,7 @@ Authors: from __future__ import print_function import os +import struct from IPython.utils.py3compat import string_types @@ -471,6 +472,32 @@ class Javascript(DisplayObject): _PNG = b'\x89PNG\r\n\x1a\n' _JPEG = b'\xff\xd8' +def _pngxy(data): + """read the (width, height) from a PNG header""" + ihdr = data.index(b'IHDR') + # next 8 bytes are width/height + w4h4 = data[ihdr+4:ihdr+12] + return struct.unpack('>ii', w4h4) + +def _jpegxy(data): + """read the (width, height) from a JPEG header""" + # adapted from http://www.64lines.com/jpeg-width-height + + idx = 4 + while True: + block_size = struct.unpack('>H', data[idx:idx+2])[0] + idx = idx + block_size + if data[idx:idx+2] == b'\xFF\xC0': + # found Start of Frame + iSOF = idx + break + else: + # read another block + idx += 2 + + h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9]) + return w, h + class Image(DisplayObject): _read_flags = 'rb' @@ -478,7 +505,7 @@ class Image(DisplayObject): _FMT_PNG = u'png' _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG] - def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None): + def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None, retina=False): """Create a display an PNG/JPEG image given raw data. When this object is returned by an expression or passed to the @@ -512,6 +539,13 @@ class Image(DisplayObject): Width to which to constrain the image in html height : int Height to which to constrain the image in html + retina : bool + Automatically set the width and height to half of the measured + width and height. + This only works for embedded images because it reads the width/height + from image data. + For non-embedded images, you can just set the desired display width + and height directly. Examples -------- @@ -561,12 +595,32 @@ class Image(DisplayObject): raise ValueError("Cannot embed the '%s' image format" % (self.format)) self.width = width self.height = height + self.retina = retina super(Image, self).__init__(data=data, url=url, filename=filename) + + if retina: + self._retina_shape() + + def _retina_shape(self): + """load pixel-doubled width and height from image data""" + if not self.embed: + return + if self.format == 'png': + w, h = _pngxy(self.data) + elif self.format == 'jpeg': + w, h = _jpegxy(self.data) + else: + # retina only supports png + return + self.width = w // 2 + self.height = h // 2 def reload(self): """Reload the raw data from file or URL.""" if self.embed: super(Image,self).reload() + if self.retina: + self._retina_shape() def _repr_html_(self): if not self.embed: diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index 52c2388..ac6976f 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -9,7 +9,7 @@ Authors """ #----------------------------------------------------------------------------- -# Copyright (C) 2009-2011 The IPython Development Team +# Copyright (C) 2009 The IPython Development Team # # Distributed under the terms of the BSD License. The full license is in # the file COPYING, distributed as part of this software. @@ -22,6 +22,7 @@ Authors import sys from io import BytesIO +from IPython.core.display import _pngxy from IPython.utils.decorators import flag_calls # If user specifies a GUI, that dictates the backend, otherwise we read the @@ -90,6 +91,7 @@ def figsize(sizex, sizey): def print_figure(fig, fmt='png'): """Convert a figure to svg or png for inline display.""" + from matplotlib import rcParams # When there's an empty figure, we shouldn't return anything, otherwise we # get big blank areas in the qt console. if not fig.axes and not fig.lines: @@ -98,11 +100,21 @@ def print_figure(fig, fmt='png'): fc = fig.get_facecolor() ec = fig.get_edgecolor() bytes_io = BytesIO() + dpi = rcParams['savefig.dpi'] + if fmt == 'retina': + dpi = dpi * 2 + fmt = 'png' fig.canvas.print_figure(bytes_io, format=fmt, bbox_inches='tight', - facecolor=fc, edgecolor=ec) + facecolor=fc, edgecolor=ec, dpi=dpi) data = bytes_io.getvalue() return data - + +def retina_figure(fig): + """format a figure as a pixel-doubled (retina) PNG""" + pngdata = print_figure(fig, fmt='retina') + w, h = _pngxy(pngdata) + metadata = dict(width=w//2, height=h//2) + return pngdata, metadata # We need a little factory function here to create the closure where # safe_execfile can live. @@ -147,7 +159,7 @@ def mpl_runner(safe_execfile): def select_figure_format(shell, fmt): - """Select figure format for inline backend, either 'png' or 'svg'. + """Select figure format for inline backend, can be 'png', 'retina', or 'svg'. Using this method ensures only one figure format is active at a time. """ @@ -157,14 +169,17 @@ def select_figure_format(shell, fmt): svg_formatter = shell.display_formatter.formatters['image/svg+xml'] png_formatter = shell.display_formatter.formatters['image/png'] - if fmt=='png': + if fmt == 'png': svg_formatter.type_printers.pop(Figure, None) png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png')) - elif fmt=='svg': + elif fmt in ('png2x', 'retina'): + svg_formatter.type_printers.pop(Figure, None) + png_formatter.for_type(Figure, retina_figure) + elif fmt == 'svg': png_formatter.type_printers.pop(Figure, None) svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg')) else: - raise ValueError("supported formats are: 'png', 'svg', not %r"%fmt) + raise ValueError("supported formats are: 'png', 'retina', 'svg', not %r" % fmt) # set the format to be used in the backend() backend_inline._figure_format = fmt diff --git a/IPython/core/tests/2x2.jpg b/IPython/core/tests/2x2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3d176625fc53435042273e971c42a0daf855aaf3 GIT binary patch literal 331 zc%1ux^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<Jp%s%2(oVqs=wWnlrz)&k`jSOi&x6b&8O zgaZ@Vl?p|S8YeE~Pwh=DOELf4NWZ*Q!{f5ODks=S2uSL zPp{yR(6I1`$f)F$)U@=B%&g*)(z5c3%Btp;*0%PJ&aO$5r%atTea6gLixw|gx@`H1 zm8&*w-m-Pu_8mKS9XfpE=&|D`PM*4S`O4L6*Kgds_3+W-Cr_U}fAR9w$4{TXeEs(Q T$IoB?Z!vIy{A16c{r@HaOTTFm literal 0 Hc$@' % (thisurl), img._repr_html_()) +def test_retina_png(): + here = os.path.dirname(__file__) + img = display.Image(os.path.join(here, "2x2.png"), retina=True) + nt.assert_equal(img.height, 1) + nt.assert_equal(img.width, 1) + data, md = img._repr_png_() + nt.assert_equal(md['width'], 1) + nt.assert_equal(md['height'], 1) + +def test_retina_jpeg(): + here = os.path.dirname(__file__) + img = display.Image(os.path.join(here, "2x2.jpg"), retina=True) + nt.assert_equal(img.height, 1) + nt.assert_equal(img.width, 1) + data, md = img._repr_jpeg_() + nt.assert_equal(md['width'], 1) + nt.assert_equal(md['height'], 1) + def test_image_filename_defaults(): '''test format constraint, and validity of jpeg and png''' tpath = ipath.get_ipython_package_dir() diff --git a/IPython/kernel/zmq/pylab/backend_inline.py b/IPython/kernel/zmq/pylab/backend_inline.py index e20871a..0c08ea8 100644 --- a/IPython/kernel/zmq/pylab/backend_inline.py +++ b/IPython/kernel/zmq/pylab/backend_inline.py @@ -18,7 +18,7 @@ from IPython.config.configurable import SingletonConfigurable from IPython.core.display import display from IPython.core.displaypub import publish_display_data from IPython.core.pylabtools import print_figure, select_figure_format -from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, CBool +from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, Bool from IPython.utils.warn import warn #----------------------------------------------------------------------------- @@ -56,7 +56,7 @@ class InlineBackend(InlineBackendConfig): inline backend.""" ) - figure_format = CaselessStrEnum(['svg', 'png'], default_value='png', config=True, + figure_format = CaselessStrEnum(['svg', 'png', 'retina'], default_value='png', config=True, help="The image format for figures with the inline backend.") def _figure_format_changed(self, name, old, new): @@ -65,7 +65,7 @@ class InlineBackend(InlineBackendConfig): else: select_figure_format(self.shell, new) - close_figures = CBool(True, config=True, + close_figures = Bool(True, config=True, help="""Close all figures at the end of each cell. When True, ensures that each cell starts with no active figures, but it diff --git a/setupbase.py b/setupbase.py index 315848e..fee8dc8 100644 --- a/setupbase.py +++ b/setupbase.py @@ -146,6 +146,7 @@ def find_package_data(): package_data = { 'IPython.config.profile' : ['README*', '*/*.py'], + 'IPython.core.tests' : ['*.png', '*.jpg'], 'IPython.testing' : ['*.txt'], 'IPython.testing.plugin' : ['*.txt'], 'IPython.html' : ['templates/*'] + static_data,