From b10d881a5885ca6af82adf1683bc947113a957fa Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 29 May 2013 13:04:04 -0700 Subject: [PATCH 1/4] enable retina matplotlib figures --- IPython/core/pylabtools.py | 33 ++++++++++++++++++---- IPython/kernel/zmq/pylab/backend_inline.py | 6 ++-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index 52c2388a12b..126b05bdbf4 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -19,6 +19,7 @@ # Imports #----------------------------------------------------------------------------- +import struct import sys from io import BytesIO @@ -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,27 @@ 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 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 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 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 +165,7 @@ def mpl_execfile(fname,*where,**kw): 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 +175,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/kernel/zmq/pylab/backend_inline.py b/IPython/kernel/zmq/pylab/backend_inline.py index e20871ae22b..0c08ea8fad3 100644 --- a/IPython/kernel/zmq/pylab/backend_inline.py +++ b/IPython/kernel/zmq/pylab/backend_inline.py @@ -18,7 +18,7 @@ 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 @@ def _config_changed(self, name, old, new): 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 @@ def _figure_format_changed(self, name, old, new): 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 From 706c8b7c12cf8ac864effd2bb9b094e5b0bdb4e5 Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 29 May 2013 13:59:48 -0700 Subject: [PATCH 2/4] enable retina display of Image objects only works for embedded images --- IPython/core/display.py | 56 +++++++++++++++++++++++++++++++++++++- IPython/core/pylabtools.py | 14 +++------- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index 2733e9fc6ea..0e64c11570a 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -20,6 +20,7 @@ from __future__ import print_function import os +import struct from IPython.utils.py3compat import string_types @@ -471,6 +472,32 @@ def _repr_javascript_(self): _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 @@ def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None 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 @@ def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None 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 126b05bdbf4..ac6976f1f24 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -9,7 +9,7 @@ """ #----------------------------------------------------------------------------- -# 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. @@ -19,10 +19,10 @@ # Imports #----------------------------------------------------------------------------- -import struct 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 @@ -103,22 +103,16 @@ def print_figure(fig, fmt='png'): 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, dpi=dpi) data = bytes_io.getvalue() return data -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 retina_figure(fig): """format a figure as a pixel-doubled (retina) PNG""" pngdata = print_figure(fig, fmt='retina') - w, h = pngxy(pngdata) + w, h = _pngxy(pngdata) metadata = dict(width=w//2, height=h//2) return pngdata, metadata From 4023856376685b60dff5709b1758d380fae68f2a Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 29 May 2013 14:09:27 -0700 Subject: [PATCH 3/4] test Image(retina=True) --- IPython/core/tests/2x2.jpg | Bin 0 -> 331 bytes IPython/core/tests/2x2.png | Bin 0 -> 71 bytes IPython/core/tests/test_display.py | 18 ++++++++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 IPython/core/tests/2x2.jpg create mode 100644 IPython/core/tests/2x2.png 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 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<Jp%s%2(oVqs=wWnlrTt_8|7un4jWDH=Mm z2?r*!D;0_uHBMZ}q3pErplHy=4=Tnkx|JhscGpMnOVgprDf$6l~v6xt!?ccon4bAPnkMx`iz;g7A;<~blLJ1 zD_3pWyk+aQ?K^hvI&}ER(PPI?oIG{u@|COCuHU$M>*1rvPo6$|{^I4UkDoq&`TFhq TkDtH(-(uhZ1)DvC_WzpzOTTFm literal 0 HcmV?d00001 diff --git a/IPython/core/tests/2x2.png b/IPython/core/tests/2x2.png new file mode 100644 index 0000000000000000000000000000000000000000..99d131317bbbbe26a0836b70954232053c1f0281 GIT binary patch literal 71 zcmeAS@N?(olHy`uVBq!ia0vp^Od!kwBpAZ)2K@k1e4Z|jAr*6ya}pAg5`c1(7+Sv9 RERF!F@O1TaS?83{1OVi15On|m literal 0 HcmV?d00001 diff --git a/IPython/core/tests/test_display.py b/IPython/core/tests/test_display.py index c34674dfc69..cf0f682981e 100644 --- a/IPython/core/tests/test_display.py +++ b/IPython/core/tests/test_display.py @@ -22,6 +22,24 @@ def test_image_size(): img = display.Image(url=thisurl) nt.assert_equal(u'' % (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() From dd20955c0ae6b18b7da3d55c94f24ba4cc52f7f4 Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 29 May 2013 14:10:57 -0700 Subject: [PATCH 4/4] make sure test files are installed --- setupbase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setupbase.py b/setupbase.py index 218d5cb1b0a..7e04880abc1 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.frontend.html.notebook' : ['templates/*'] + static_data,