Skip to content

Commit

Permalink
Merge pull request #3381 from minrk/retina
Browse files Browse the repository at this point in the history
enable 2x (retina) display
  • Loading branch information
ivanov committed Jun 28, 2013
2 parents 0c0c1a1 + dd20955 commit d820363
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 11 deletions.
56 changes: 55 additions & 1 deletion IPython/core/display.py
Expand Up @@ -20,6 +20,7 @@
from __future__ import print_function

import os
import struct

from IPython.utils.py3compat import string_types

Expand Down Expand Up @@ -471,14 +472,40 @@ 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'
_FMT_JPEG = u'jpeg'
_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
Expand Down Expand Up @@ -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
--------
Expand Down Expand Up @@ -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:
Expand Down
29 changes: 22 additions & 7 deletions IPython/core/pylabtools.py
Expand Up @@ -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.
Expand All @@ -22,6 +22,7 @@
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
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -147,7 +159,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.
"""
Expand All @@ -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
Expand Down
Binary file added IPython/core/tests/2x2.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added IPython/core/tests/2x2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions IPython/core/tests/test_display.py
Expand Up @@ -22,6 +22,24 @@ def test_image_size():
img = display.Image(url=thisurl)
nt.assert_equal(u'<img src="%s"/>' % (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()
Expand Down
6 changes: 3 additions & 3 deletions IPython/kernel/zmq/pylab/backend_inline.py
Expand Up @@ -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

#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions setupbase.py
Expand Up @@ -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,
Expand Down

0 comments on commit d820363

Please sign in to comment.