Show More
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
@@ -20,6 +20,7 b' Authors:' | |||||
20 | from __future__ import print_function |
|
20 | from __future__ import print_function | |
21 |
|
21 | |||
22 | import os |
|
22 | import os | |
|
23 | import struct | |||
23 |
|
24 | |||
24 | from IPython.utils.py3compat import string_types |
|
25 | from IPython.utils.py3compat import string_types | |
25 |
|
26 | |||
@@ -471,6 +472,32 b' class Javascript(DisplayObject):' | |||||
471 | _PNG = b'\x89PNG\r\n\x1a\n' |
|
472 | _PNG = b'\x89PNG\r\n\x1a\n' | |
472 | _JPEG = b'\xff\xd8' |
|
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 | class Image(DisplayObject): |
|
501 | class Image(DisplayObject): | |
475 |
|
502 | |||
476 | _read_flags = 'rb' |
|
503 | _read_flags = 'rb' | |
@@ -478,7 +505,7 b' class Image(DisplayObject):' | |||||
478 | _FMT_PNG = u'png' |
|
505 | _FMT_PNG = u'png' | |
479 | _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG] |
|
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 | """Create a display an PNG/JPEG image given raw data. |
|
509 | """Create a display an PNG/JPEG image given raw data. | |
483 |
|
510 | |||
484 | When this object is returned by an expression or passed to the |
|
511 | When this object is returned by an expression or passed to the | |
@@ -512,6 +539,13 b' class Image(DisplayObject):' | |||||
512 | Width to which to constrain the image in html |
|
539 | Width to which to constrain the image in html | |
513 | height : int |
|
540 | height : int | |
514 | Height to which to constrain the image in html |
|
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 | Examples |
|
550 | Examples | |
517 | -------- |
|
551 | -------- | |
@@ -561,12 +595,32 b' class Image(DisplayObject):' | |||||
561 | raise ValueError("Cannot embed the '%s' image format" % (self.format)) |
|
595 | raise ValueError("Cannot embed the '%s' image format" % (self.format)) | |
562 | self.width = width |
|
596 | self.width = width | |
563 | self.height = height |
|
597 | self.height = height | |
|
598 | self.retina = retina | |||
564 | super(Image, self).__init__(data=data, url=url, filename=filename) |
|
599 | super(Image, self).__init__(data=data, url=url, filename=filename) | |
|
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 | |||
565 |
|
617 | |||
566 | def reload(self): |
|
618 | def reload(self): | |
567 | """Reload the raw data from file or URL.""" |
|
619 | """Reload the raw data from file or URL.""" | |
568 | if self.embed: |
|
620 | if self.embed: | |
569 | super(Image,self).reload() |
|
621 | super(Image,self).reload() | |
|
622 | if self.retina: | |||
|
623 | self._retina_shape() | |||
570 |
|
624 | |||
571 | def _repr_html_(self): |
|
625 | def _repr_html_(self): | |
572 | if not self.embed: |
|
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 | # Distributed under the terms of the BSD License. The full license is in |
|
14 | # Distributed under the terms of the BSD License. The full license is in | |
15 | # the file COPYING, distributed as part of this software. |
|
15 | # the file COPYING, distributed as part of this software. | |
@@ -22,6 +22,7 b' Authors' | |||||
22 | import sys |
|
22 | import sys | |
23 | from io import BytesIO |
|
23 | from io import BytesIO | |
24 |
|
24 | |||
|
25 | from IPython.core.display import _pngxy | |||
25 | from IPython.utils.decorators import flag_calls |
|
26 | from IPython.utils.decorators import flag_calls | |
26 |
|
27 | |||
27 | # If user specifies a GUI, that dictates the backend, otherwise we read the |
|
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 | def print_figure(fig, fmt='png'): |
|
92 | def print_figure(fig, fmt='png'): | |
92 | """Convert a figure to svg or png for inline display.""" |
|
93 | """Convert a figure to svg or png for inline display.""" | |
|
94 | from matplotlib import rcParams | |||
93 | # When there's an empty figure, we shouldn't return anything, otherwise we |
|
95 | # When there's an empty figure, we shouldn't return anything, otherwise we | |
94 | # get big blank areas in the qt console. |
|
96 | # get big blank areas in the qt console. | |
95 | if not fig.axes and not fig.lines: |
|
97 | if not fig.axes and not fig.lines: | |
@@ -98,11 +100,21 b" def print_figure(fig, fmt='png'):" | |||||
98 | fc = fig.get_facecolor() |
|
100 | fc = fig.get_facecolor() | |
99 | ec = fig.get_edgecolor() |
|
101 | ec = fig.get_edgecolor() | |
100 | bytes_io = BytesIO() |
|
102 | bytes_io = BytesIO() | |
|
103 | dpi = rcParams['savefig.dpi'] | |||
|
104 | if fmt == 'retina': | |||
|
105 | dpi = dpi * 2 | |||
|
106 | fmt = 'png' | |||
101 | fig.canvas.print_figure(bytes_io, format=fmt, bbox_inches='tight', |
|
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 | data = bytes_io.getvalue() |
|
109 | data = bytes_io.getvalue() | |
104 | return data |
|
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 | # We need a little factory function here to create the closure where |
|
119 | # We need a little factory function here to create the closure where | |
108 | # safe_execfile can live. |
|
120 | # safe_execfile can live. | |
@@ -147,7 +159,7 b' def mpl_runner(safe_execfile):' | |||||
147 |
|
159 | |||
148 |
|
160 | |||
149 | def select_figure_format(shell, fmt): |
|
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 | Using this method ensures only one figure format is active at a time. |
|
164 | Using this method ensures only one figure format is active at a time. | |
153 | """ |
|
165 | """ | |
@@ -157,14 +169,17 b' def select_figure_format(shell, fmt):' | |||||
157 | svg_formatter = shell.display_formatter.formatters['image/svg+xml'] |
|
169 | svg_formatter = shell.display_formatter.formatters['image/svg+xml'] | |
158 | png_formatter = shell.display_formatter.formatters['image/png'] |
|
170 | png_formatter = shell.display_formatter.formatters['image/png'] | |
159 |
|
171 | |||
160 | if fmt=='png': |
|
172 | if fmt == 'png': | |
161 | svg_formatter.type_printers.pop(Figure, None) |
|
173 | svg_formatter.type_printers.pop(Figure, None) | |
162 | png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png')) |
|
174 | png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png')) | |
163 | elif fmt=='svg': |
|
175 | elif fmt in ('png2x', 'retina'): | |
|
176 | svg_formatter.type_printers.pop(Figure, None) | |||
|
177 | png_formatter.for_type(Figure, retina_figure) | |||
|
178 | elif fmt == 'svg': | |||
164 | png_formatter.type_printers.pop(Figure, None) |
|
179 | png_formatter.type_printers.pop(Figure, None) | |
165 | svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg')) |
|
180 | svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg')) | |
166 | else: |
|
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 | # set the format to be used in the backend() |
|
184 | # set the format to be used in the backend() | |
170 | backend_inline._figure_format = fmt |
|
185 | backend_inline._figure_format = fmt |
@@ -22,6 +22,24 b' def test_image_size():' | |||||
22 | img = display.Image(url=thisurl) |
|
22 | img = display.Image(url=thisurl) | |
23 | nt.assert_equal(u'<img src="%s"/>' % (thisurl), img._repr_html_()) |
|
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 | def test_image_filename_defaults(): |
|
43 | def test_image_filename_defaults(): | |
26 | '''test format constraint, and validity of jpeg and png''' |
|
44 | '''test format constraint, and validity of jpeg and png''' | |
27 | tpath = ipath.get_ipython_package_dir() |
|
45 | tpath = ipath.get_ipython_package_dir() |
@@ -18,7 +18,7 b' from IPython.config.configurable import SingletonConfigurable' | |||||
18 | from IPython.core.display import display |
|
18 | from IPython.core.display import display | |
19 | from IPython.core.displaypub import publish_display_data |
|
19 | from IPython.core.displaypub import publish_display_data | |
20 | from IPython.core.pylabtools import print_figure, select_figure_format |
|
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 | from IPython.utils.warn import warn |
|
22 | from IPython.utils.warn import warn | |
23 |
|
23 | |||
24 | #----------------------------------------------------------------------------- |
|
24 | #----------------------------------------------------------------------------- | |
@@ -56,7 +56,7 b' class InlineBackend(InlineBackendConfig):' | |||||
56 | inline backend.""" |
|
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 | help="The image format for figures with the inline backend.") |
|
60 | help="The image format for figures with the inline backend.") | |
61 |
|
61 | |||
62 | def _figure_format_changed(self, name, old, new): |
|
62 | def _figure_format_changed(self, name, old, new): | |
@@ -65,7 +65,7 b' class InlineBackend(InlineBackendConfig):' | |||||
65 | else: |
|
65 | else: | |
66 | select_figure_format(self.shell, new) |
|
66 | select_figure_format(self.shell, new) | |
67 |
|
67 | |||
68 |
close_figures = |
|
68 | close_figures = Bool(True, config=True, | |
69 | help="""Close all figures at the end of each cell. |
|
69 | help="""Close all figures at the end of each cell. | |
70 |
|
70 | |||
71 | When True, ensures that each cell starts with no active figures, but it |
|
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 | package_data = { |
|
147 | package_data = { | |
148 | 'IPython.config.profile' : ['README*', '*/*.py'], |
|
148 | 'IPython.config.profile' : ['README*', '*/*.py'], | |
|
149 | 'IPython.core.tests' : ['*.png', '*.jpg'], | |||
149 | 'IPython.testing' : ['*.txt'], |
|
150 | 'IPython.testing' : ['*.txt'], | |
150 | 'IPython.testing.plugin' : ['*.txt'], |
|
151 | 'IPython.testing.plugin' : ['*.txt'], | |
151 | 'IPython.html' : ['templates/*'] + static_data, |
|
152 | 'IPython.html' : ['templates/*'] + static_data, |
General Comments 0
You need to be logged in to leave comments.
Login now