From 24aa3931604b1421becdf615d614d5e3ae2aeee6 Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Thu, 18 Mar 2021 13:06:02 -0700 Subject: [PATCH 1/2] Add formal support for alt text to IPython.display.Image. For accessibility reasons users should be encouraged to add useful alt text to images. Currently there is no recommended way to do this using the standard Image object. Adding this field is a step towards getting a common way for notebook renderers to support alt text and towards encouraging libraries creating images to add meaningful alt text. --- IPython/core/display.py | 20 ++++++++++++++++---- IPython/core/tests/test_display.py | 26 ++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index bd098e78f99..851016fc42a 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 @@ -371,7 +372,7 @@ def reload(self): with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp: encoding = None data = fp.read() - + # decode data, if an encoding was specified # We only touch self.data once since # subclasses such as SVG have @data.setter methods @@ -802,7 +803,7 @@ class Image(DisplayObject): def __init__(self, data=None, url=None, filename=None, format=None, embed=None, width=None, height=None, retina=False, - unconfined=False, metadata=None): + 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 +848,8 @@ def __init__(self, data=None, url=None, filename=None, format=None, 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 +927,7 @@ def __init__(self, data=None, url=None, filename=None, format=None, 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 +937,9 @@ def __init__(self, data=None, url=None, filename=None, format=None, 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 +969,21 @@ def reload(self): 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 u''.format( url=self.url, width=width, height=height, klass=klass, + alt=alt, ) def _repr_mimebundle_(self, include=None, exclude=None): @@ -1006,6 +1016,8 @@ def _data_and_metadata(self, always_both=False): 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 be0f85c1b84..b7b440098f7 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,7 +94,7 @@ def read(self): if args[0] == url: return MockResponse(svg_data) - elif args[0] == url + 'z': + elif args[0] == url + 'z': ret= MockResponse(gzip_svg) ret.headers['content-encoding']= 'gzip' return ret @@ -105,7 +105,7 @@ def read(self): 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'an image' % (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') From 93a63567e176ad989841c80800d75a899e9afc47 Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Sun, 28 Mar 2021 14:17:10 -0700 Subject: [PATCH 2/2] Fix lint errors --- IPython/core/display.py | 27 ++++++++++++++++++-------- IPython/core/tests/test_display.py | 31 +++++++++++++++++------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index 851016fc42a..00fe85eb3a4 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -801,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, alt=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 @@ -937,8 +948,8 @@ def __init__(self, data=None, url=None, filename=None, format=None, 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 self.alt is None and self.metadata.get("alt", {}): + self.alt = metadata["alt"] if retina: self._retina_shape() @@ -969,7 +980,7 @@ def reload(self): def _repr_html_(self): if not self.embed: - width = height = klass = alt = '' + width = height = klass = alt = "" if self.width: width = ' width="%d"' % self.width if self.height: @@ -978,7 +989,7 @@ def _repr_html_(self): klass = ' class="unconfined"' if self.alt: alt = ' alt="%s"' % html.escape(self.alt) - return u''.format( + return ''.format( url=self.url, width=width, height=height, @@ -1017,7 +1028,7 @@ def _data_and_metadata(self, always_both=False): if self.unconfined: md['unconfined'] = self.unconfined if self.alt: - md['alt'] = 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 b7b440098f7..8deb11a1647 100644 --- a/IPython/core/tests/test_display.py +++ b/IPython/core/tests/test_display.py @@ -94,9 +94,9 @@ def read(self): 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) @@ -459,19 +459,24 @@ def test_display_handle(): def test_image_alt_tag(): """Simple test for display.Image(args, alt=x,)""" - thisurl = 'http://example.com/image.png' - img = display.Image(url=thisurl, alt='an image') + thisurl = "http://example.com/image.png" + img = display.Image(url=thisurl, alt="an image") nt.assert_equal(u'an image' % (thisurl), img._repr_html_()) - img = display.Image(url=thisurl, unconfined=True, alt='an image') - nt.assert_equal(u'an image' % (thisurl), img._repr_html_()) + img = display.Image(url=thisurl, unconfined=True, alt="an image") + nt.assert_equal( + u'an image' % (thisurl), + img._repr_html_(), + ) img = display.Image(url=thisurl, alt='>"& <') - nt.assert_equal(u'>"& <' % (thisurl), img._repr_html_()) + nt.assert_equal( + u'>"& <' % (thisurl), img._repr_html_() + ) - img = display.Image(url=thisurl, metadata={'alt':'an image'}) - nt.assert_equal(img.alt, 'an image') + 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') + 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.assert_equal(md["alt"], "an image")