diff --git a/IPython/core/display.py b/IPython/core/display.py
index 4355891..310e2fa 100644
--- a/IPython/core/display.py
+++ b/IPython/core/display.py
@@ -6,6 +6,7 @@
from binascii import b2a_base64, hexlify
+import html
import json
import mimetypes
import os
@@ -800,9 +801,20 @@ class Image(DisplayObject):
_FMT_GIF: 'image/gif',
}
- def __init__(self, data=None, url=None, filename=None, format=None,
- embed=None, width=None, height=None, retina=False,
- unconfined=False, metadata=None):
+ def __init__(
+ self,
+ data=None,
+ url=None,
+ filename=None,
+ format=None,
+ embed=None,
+ width=None,
+ height=None,
+ retina=False,
+ unconfined=False,
+ metadata=None,
+ alt=None,
+ ):
"""Create a PNG/JPEG/GIF image object given raw data.
When this object is returned by an input cell or passed to the
@@ -847,6 +859,8 @@ class Image(DisplayObject):
Set unconfined=True to disable max-width confinement of the image.
metadata : dict
Specify extra metadata to attach to the image.
+ alt : unicode
+ Alternative text for the image, for use by screen readers.
Examples
--------
@@ -924,6 +938,7 @@ class Image(DisplayObject):
self.height = height
self.retina = retina
self.unconfined = unconfined
+ self.alt = alt
super(Image, self).__init__(data=data, url=url, filename=filename,
metadata=metadata)
@@ -933,6 +948,9 @@ class Image(DisplayObject):
if self.height is None and self.metadata.get('height', {}):
self.height = metadata['height']
+ if self.alt is None and self.metadata.get("alt", {}):
+ self.alt = metadata["alt"]
+
if retina:
self._retina_shape()
@@ -962,18 +980,21 @@ class Image(DisplayObject):
def _repr_html_(self):
if not self.embed:
- width = height = klass = ''
+ width = height = klass = alt = ""
if self.width:
width = ' width="%d"' % self.width
if self.height:
height = ' height="%d"' % self.height
if self.unconfined:
klass = ' class="unconfined"'
- return u''.format(
+ if self.alt:
+ alt = ' alt="%s"' % html.escape(self.alt)
+ return ''.format(
url=self.url,
width=width,
height=height,
klass=klass,
+ alt=alt,
)
def _repr_mimebundle_(self, include=None, exclude=None):
@@ -1006,6 +1027,8 @@ class Image(DisplayObject):
md['height'] = self.height
if self.unconfined:
md['unconfined'] = self.unconfined
+ if self.alt:
+ md["alt"] = self.alt
if md or always_both:
return b64_data, md
else:
diff --git a/IPython/core/tests/test_display.py b/IPython/core/tests/test_display.py
index f7e9a52..4b783bc 100644
--- a/IPython/core/tests/test_display.py
+++ b/IPython/core/tests/test_display.py
@@ -77,12 +77,12 @@ def test_embed_svg_url():
from io import BytesIO
svg_data = b''
url = 'http://test.com/circle.svg'
-
+
gzip_svg = BytesIO()
with gzip.open(gzip_svg, 'wb') as fp:
fp.write(svg_data)
gzip_svg = gzip_svg.getvalue()
-
+
def mocked_urlopen(*args, **kwargs):
class MockResponse:
def __init__(self, svg):
@@ -94,9 +94,9 @@ def test_embed_svg_url():
if args[0] == url:
return MockResponse(svg_data)
- elif args[0] == url + 'z':
- ret= MockResponse(gzip_svg)
- ret.headers['content-encoding']= 'gzip'
+ elif args[0] == url + "z":
+ ret = MockResponse(gzip_svg)
+ ret.headers["content-encoding"] = "gzip"
return ret
return MockResponse(None)
@@ -105,7 +105,7 @@ def test_embed_svg_url():
nt.assert_true(svg._repr_svg_().startswith('' % (thisurl), img._repr_html_())
+ img = display.Image(url=thisurl, unconfined=True, alt="an image")
+ nt.assert_equal(
+ u'' % (thisurl),
+ img._repr_html_(),
+ )
+ img = display.Image(url=thisurl, alt='>"& <')
+ nt.assert_equal(
+ u'' % (thisurl), img._repr_html_()
+ )
+
+ img = display.Image(url=thisurl, metadata={"alt": "an image"})
+ nt.assert_equal(img.alt, "an image")
+
+ here = os.path.dirname(__file__)
+ img = display.Image(os.path.join(here, "2x2.png"), alt="an image")
+ nt.assert_equal(img.alt, "an image")
+ _, md = img._repr_png_()
+ nt.assert_equal(md["alt"], "an image")
+
+
@nt.raises(FileNotFoundError)
def test_image_bad_filename_raises_proper_exception():
display.Image("/this/file/does/not/exist/")._repr_png_()