Show More
|
1 | NO CONTENT: new file 100644, binary diff hidden |
|
1 | NO CONTENT: new file 100644, binary diff hidden |
@@ -20,6 +20,7 b' Authors:' | |||
|
20 | 20 | from __future__ import print_function |
|
21 | 21 | |
|
22 | 22 | import os |
|
23 | import struct | |
|
23 | 24 | |
|
24 | 25 | from IPython.utils.py3compat import string_types |
|
25 | 26 | |
@@ -471,6 +472,32 b' class Javascript(DisplayObject):' | |||
|
471 | 472 | _PNG = b'\x89PNG\r\n\x1a\n' |
|
472 | 473 | _JPEG = b'\xff\xd8' |
|
473 | 474 | |
|
475 | def _pngxy(data): | |
|
476 | """read the (width, height) from a PNG header""" | |
|
477 | ihdr = data.index(b'IHDR') | |
|
478 | # next 8 bytes are width/height | |
|
479 | w4h4 = data[ihdr+4:ihdr+12] | |
|
480 | return struct.unpack('>ii', w4h4) | |
|
481 | ||
|
482 | def _jpegxy(data): | |
|
483 | """read the (width, height) from a JPEG header""" | |
|
484 | # adapted from http://www.64lines.com/jpeg-width-height | |
|
485 | ||
|
486 | idx = 4 | |
|
487 | while True: | |
|
488 | block_size = struct.unpack('>H', data[idx:idx+2])[0] | |
|
489 | idx = idx + block_size | |
|
490 | if data[idx:idx+2] == b'\xFF\xC0': | |
|
491 | # found Start of Frame | |
|
492 | iSOF = idx | |
|
493 | break | |
|
494 | else: | |
|
495 | # read another block | |
|
496 | idx += 2 | |
|
497 | ||
|
498 | h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9]) | |
|
499 | return w, h | |
|
500 | ||
|
474 | 501 | class Image(DisplayObject): |
|
475 | 502 | |
|
476 | 503 | _read_flags = 'rb' |
@@ -478,7 +505,7 b' class Image(DisplayObject):' | |||
|
478 | 505 | _FMT_PNG = u'png' |
|
479 | 506 | _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG] |
|
480 | 507 | |
|
481 | def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None): | |
|
508 | def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None, retina=False): | |
|
482 | 509 | """Create a display an PNG/JPEG image given raw data. |
|
483 | 510 | |
|
484 | 511 | When this object is returned by an expression or passed to the |
@@ -512,6 +539,13 b' class Image(DisplayObject):' | |||
|
512 | 539 | Width to which to constrain the image in html |
|
513 | 540 | height : int |
|
514 | 541 | Height to which to constrain the image in html |
|
542 | retina : bool | |
|
543 | Automatically set the width and height to half of the measured | |
|
544 | width and height. | |
|
545 | This only works for embedded images because it reads the width/height | |
|
546 | from image data. | |
|
547 | For non-embedded images, you can just set the desired display width | |
|
548 | and height directly. | |
|
515 | 549 | |
|
516 | 550 | Examples |
|
517 | 551 | -------- |
@@ -561,12 +595,32 b' class Image(DisplayObject):' | |||
|
561 | 595 | raise ValueError("Cannot embed the '%s' image format" % (self.format)) |
|
562 | 596 | self.width = width |
|
563 | 597 | self.height = height |
|
598 | self.retina = retina | |
|
564 | 599 | super(Image, self).__init__(data=data, url=url, filename=filename) |
|
565 | 600 | |
|
601 | if retina: | |
|
602 | self._retina_shape() | |
|
603 | ||
|
604 | def _retina_shape(self): | |
|
605 | """load pixel-doubled width and height from image data""" | |
|
606 | if not self.embed: | |
|
607 | return | |
|
608 | if self.format == 'png': | |
|
609 | w, h = _pngxy(self.data) | |
|
610 | elif self.format == 'jpeg': | |
|
611 | w, h = _jpegxy(self.data) | |
|
612 | else: | |
|
613 | # retina only supports png | |
|
614 | return | |
|
615 | self.width = w // 2 | |
|
616 | self.height = h // 2 | |
|
617 | ||
|
566 | 618 | def reload(self): |
|
567 | 619 | """Reload the raw data from file or URL.""" |
|
568 | 620 | if self.embed: |
|
569 | 621 | super(Image,self).reload() |
|
622 | if self.retina: | |
|
623 | self._retina_shape() | |
|
570 | 624 | |
|
571 | 625 | def _repr_html_(self): |
|
572 | 626 | if not self.embed: |
@@ -9,7 +9,7 b' Authors' | |||
|
9 | 9 | """ |
|
10 | 10 | |
|
11 | 11 | #----------------------------------------------------------------------------- |
|
12 |
# Copyright (C) 2009 |
|
|
12 | # Copyright (C) 2009 The IPython Development Team | |
|
13 | 13 | # |
|
14 | 14 | # Distributed under the terms of the BSD License. The full license is in |
|
15 | 15 | # the file COPYING, distributed as part of this software. |
@@ -22,6 +22,7 b' Authors' | |||
|
22 | 22 | import sys |
|
23 | 23 | from io import BytesIO |
|
24 | 24 | |
|
25 | from IPython.core.display import _pngxy | |
|
25 | 26 | from IPython.utils.decorators import flag_calls |
|
26 | 27 | |
|
27 | 28 | # If user specifies a GUI, that dictates the backend, otherwise we read the |
@@ -90,6 +91,7 b' def figsize(sizex, sizey):' | |||
|
90 | 91 | |
|
91 | 92 | def print_figure(fig, fmt='png'): |
|
92 | 93 | """Convert a figure to svg or png for inline display.""" |
|
94 | from matplotlib import rcParams | |
|
93 | 95 | # When there's an empty figure, we shouldn't return anything, otherwise we |
|
94 | 96 | # get big blank areas in the qt console. |
|
95 | 97 | if not fig.axes and not fig.lines: |
@@ -98,11 +100,21 b" def print_figure(fig, fmt='png'):" | |||
|
98 | 100 | fc = fig.get_facecolor() |
|
99 | 101 | ec = fig.get_edgecolor() |
|
100 | 102 | bytes_io = BytesIO() |
|
103 | dpi = rcParams['savefig.dpi'] | |
|
104 | if fmt == 'retina': | |
|
105 | dpi = dpi * 2 | |
|
106 | fmt = 'png' | |
|
101 | 107 | fig.canvas.print_figure(bytes_io, format=fmt, bbox_inches='tight', |
|
102 | facecolor=fc, edgecolor=ec) | |
|
108 | facecolor=fc, edgecolor=ec, dpi=dpi) | |
|
103 | 109 | data = bytes_io.getvalue() |
|
104 | 110 | return data |
|
105 | 111 | |
|
112 | def retina_figure(fig): | |
|
113 | """format a figure as a pixel-doubled (retina) PNG""" | |
|
114 | pngdata = print_figure(fig, fmt='retina') | |
|
115 | w, h = _pngxy(pngdata) | |
|
116 | metadata = dict(width=w//2, height=h//2) | |
|
117 | return pngdata, metadata | |
|
106 | 118 | |
|
107 | 119 | # We need a little factory function here to create the closure where |
|
108 | 120 | # safe_execfile can live. |
@@ -147,7 +159,7 b' def mpl_runner(safe_execfile):' | |||
|
147 | 159 | |
|
148 | 160 | |
|
149 | 161 | def select_figure_format(shell, fmt): |
|
150 |
"""Select figure format for inline backend, e |
|
|
162 | """Select figure format for inline backend, can be 'png', 'retina', or 'svg'. | |
|
151 | 163 | |
|
152 | 164 | Using this method ensures only one figure format is active at a time. |
|
153 | 165 | """ |
@@ -160,11 +172,14 b' def select_figure_format(shell, fmt):' | |||
|
160 | 172 | if fmt=='png': |
|
161 | 173 | svg_formatter.type_printers.pop(Figure, None) |
|
162 | 174 | png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png')) |
|
175 | elif fmt in ('png2x', 'retina'): | |
|
176 | svg_formatter.type_printers.pop(Figure, None) | |
|
177 | png_formatter.for_type(Figure, retina_figure) | |
|
163 | 178 | elif fmt=='svg': |
|
164 | 179 | png_formatter.type_printers.pop(Figure, None) |
|
165 | 180 | svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg')) |
|
166 | 181 | else: |
|
167 | raise ValueError("supported formats are: 'png', 'svg', not %r"%fmt) | |
|
182 | raise ValueError("supported formats are: 'png', 'retina', 'svg', not %r" % fmt) | |
|
168 | 183 | |
|
169 | 184 | # set the format to be used in the backend() |
|
170 | 185 | backend_inline._figure_format = fmt |
@@ -22,6 +22,24 b' def test_image_size():' | |||
|
22 | 22 | img = display.Image(url=thisurl) |
|
23 | 23 | nt.assert_equal(u'<img src="%s"/>' % (thisurl), img._repr_html_()) |
|
24 | 24 | |
|
25 | def test_retina_png(): | |
|
26 | here = os.path.dirname(__file__) | |
|
27 | img = display.Image(os.path.join(here, "2x2.png"), retina=True) | |
|
28 | nt.assert_equal(img.height, 1) | |
|
29 | nt.assert_equal(img.width, 1) | |
|
30 | data, md = img._repr_png_() | |
|
31 | nt.assert_equal(md['width'], 1) | |
|
32 | nt.assert_equal(md['height'], 1) | |
|
33 | ||
|
34 | def test_retina_jpeg(): | |
|
35 | here = os.path.dirname(__file__) | |
|
36 | img = display.Image(os.path.join(here, "2x2.jpg"), retina=True) | |
|
37 | nt.assert_equal(img.height, 1) | |
|
38 | nt.assert_equal(img.width, 1) | |
|
39 | data, md = img._repr_jpeg_() | |
|
40 | nt.assert_equal(md['width'], 1) | |
|
41 | nt.assert_equal(md['height'], 1) | |
|
42 | ||
|
25 | 43 | def test_image_filename_defaults(): |
|
26 | 44 | '''test format constraint, and validity of jpeg and png''' |
|
27 | 45 | tpath = ipath.get_ipython_package_dir() |
@@ -18,7 +18,7 b' from IPython.config.configurable import SingletonConfigurable' | |||
|
18 | 18 | from IPython.core.display import display |
|
19 | 19 | from IPython.core.displaypub import publish_display_data |
|
20 | 20 | from IPython.core.pylabtools import print_figure, select_figure_format |
|
21 |
from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, |
|
|
21 | from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, Bool | |
|
22 | 22 | from IPython.utils.warn import warn |
|
23 | 23 | |
|
24 | 24 | #----------------------------------------------------------------------------- |
@@ -56,7 +56,7 b' class InlineBackend(InlineBackendConfig):' | |||
|
56 | 56 | inline backend.""" |
|
57 | 57 | ) |
|
58 | 58 | |
|
59 | figure_format = CaselessStrEnum(['svg', 'png'], default_value='png', config=True, | |
|
59 | figure_format = CaselessStrEnum(['svg', 'png', 'retina'], default_value='png', config=True, | |
|
60 | 60 | help="The image format for figures with the inline backend.") |
|
61 | 61 | |
|
62 | 62 | def _figure_format_changed(self, name, old, new): |
@@ -65,7 +65,7 b' class InlineBackend(InlineBackendConfig):' | |||
|
65 | 65 | else: |
|
66 | 66 | select_figure_format(self.shell, new) |
|
67 | 67 | |
|
68 |
close_figures = |
|
|
68 | close_figures = Bool(True, config=True, | |
|
69 | 69 | help="""Close all figures at the end of each cell. |
|
70 | 70 | |
|
71 | 71 | When True, ensures that each cell starts with no active figures, but it |
@@ -146,6 +146,7 b' def find_package_data():' | |||
|
146 | 146 | |
|
147 | 147 | package_data = { |
|
148 | 148 | 'IPython.config.profile' : ['README*', '*/*.py'], |
|
149 | 'IPython.core.tests' : ['*.png', '*.jpg'], | |
|
149 | 150 | 'IPython.testing' : ['*.txt'], |
|
150 | 151 | 'IPython.testing.plugin' : ['*.txt'], |
|
151 | 152 | 'IPython.html' : ['templates/*'] + static_data, |
General Comments 0
You need to be logged in to leave comments.
Login now