diff --git a/doc/users/next_whats_new/gif_savefig.rst b/doc/users/next_whats_new/gif_savefig.rst new file mode 100644 index 000000000000..e6f4732e8b95 --- /dev/null +++ b/doc/users/next_whats_new/gif_savefig.rst @@ -0,0 +1,6 @@ +Saving figures as GIF works again +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +According to the figure documentation, the ``savefig`` method supports the +GIF format with the file extension ``.gif``. However, GIF support had been +broken since Matplotlib 2.0.0. It works again. diff --git a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py index f462f6bf2428..487ae9aaf62d 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py @@ -44,7 +44,7 @@ # regions can overlap and alpha allows you to see both. Note that the # postscript format does not support alpha (this is a postscript # limitation, not a matplotlib limitation), so when using alpha save -# your figures in PNG, PDF or SVG. +# your figures in GIF, PNG, PDF or SVG. # # Our next example computes two populations of random walkers with a # different mean and standard deviation of the normal distributions from diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index cd39db6c3a51..4d087911a5b2 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -62,6 +62,7 @@ _log = logging.getLogger(__name__) _default_filetypes = { 'eps': 'Encapsulated Postscript', + 'gif': 'Graphics Interchange Format', 'jpg': 'Joint Photographic Experts Group', 'jpeg': 'Joint Photographic Experts Group', 'pdf': 'Portable Document Format', @@ -78,6 +79,7 @@ } _default_backends = { 'eps': 'matplotlib.backends.backend_ps', + 'gif': 'matplotlib.backends.backend_agg', 'jpg': 'matplotlib.backends.backend_agg', 'jpeg': 'matplotlib.backends.backend_agg', 'pdf': 'matplotlib.backends.backend_pdf', diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index c37427369267..b435ae565ce4 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -490,6 +490,9 @@ def print_to_buffer(self): # print_figure(), and the latter ensures that `self.figure.dpi` already # matches the dpi kwarg (if any). + def print_gif(self, filename_or_obj, *, metadata=None, pil_kwargs=None): + self._print_pil(filename_or_obj, "gif", pil_kwargs, metadata) + def print_jpg(self, filename_or_obj, *, metadata=None, pil_kwargs=None): # savefig() has already applied savefig.facecolor; we now set it to # white to make imsave() blend semi-transparent figures against an @@ -507,7 +510,7 @@ def print_tif(self, filename_or_obj, *, metadata=None, pil_kwargs=None): def print_webp(self, filename_or_obj, *, metadata=None, pil_kwargs=None): self._print_pil(filename_or_obj, "webp", pil_kwargs, metadata) - print_jpg.__doc__, print_tif.__doc__, print_webp.__doc__ = map( + print_gif.__doc__, print_jpg.__doc__, print_tif.__doc__, print_webp.__doc__ = map( """ Write the figure to a {} file. @@ -518,7 +521,7 @@ def print_webp(self, filename_or_obj, *, metadata=None, pil_kwargs=None): pil_kwargs : dict, optional Additional keyword arguments that are passed to `PIL.Image.Image.save` when saving the figure. - """.format, ["JPEG", "TIFF", "WebP"]) + """.format, ["GIF", "JPEG", "TIFF", "WebP"]) @_Backend.export diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 0f252bc1da8e..c598dfcddc66 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -99,6 +99,16 @@ def _read_until(self, terminator): return bytes(buf) +class _MagickConverter: + def __call__(self, orig, dest): + try: + subprocess.run( + [mpl._get_executable_info("magick").executable, orig, dest], + check=True) + except subprocess.CalledProcessError as e: + raise _ConverterError() from e + + class _GSConverter(_Converter): def __call__(self, orig, dest): if not self._proc: @@ -230,6 +240,12 @@ def __call__(self, orig, dest): def _update_converter(): + try: + mpl._get_executable_info("magick") + except mpl.ExecutableNotFoundError: + pass + else: + converter['gif'] = _MagickConverter() try: mpl._get_executable_info("gs") except mpl.ExecutableNotFoundError: diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 6f1af7debdb3..af9ef48d66cc 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -204,6 +204,7 @@ def wrapper(*args, extension, request, **kwargs): if extension not in comparable_formats(): reason = { + 'gif': 'because ImageMagick is not installed', 'pdf': 'because Ghostscript is not installed', 'eps': 'because Ghostscript is not installed', 'svg': 'because Inkscape is not installed', @@ -279,7 +280,7 @@ def image_comparison(baseline_images, extensions=None, tol=0, extensions : None or list of str The list of extensions to test, e.g. ``['png', 'pdf']``. - If *None*, defaults to all supported extensions: png, pdf, and svg. + If *None*, defaults to: png, pdf, and svg. When testing a single extension, it can be directly included in the names passed to *baseline_images*. In that case, *extensions* must not diff --git a/lib/matplotlib/tests/baseline_images/test_agg_filter/agg_filter_alpha.gif b/lib/matplotlib/tests/baseline_images/test_agg_filter/agg_filter_alpha.gif new file mode 100644 index 000000000000..01a2d0bc288e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_agg_filter/agg_filter_alpha.gif differ diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 59387793605a..56b26904d041 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -263,6 +263,24 @@ def test_pil_kwargs_webp(): assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes +def test_gif_no_alpha(): + plt.plot([0, 1, 2], [0, 1, 0]) + buf = io.BytesIO() + plt.savefig(buf, format="gif", transparent=False) + im = Image.open(buf) + assert im.mode == "P" + assert im.info["transparency"] >= len(im.palette.colors) + + +def test_gif_alpha(): + plt.plot([0, 1, 2], [0, 1, 0]) + buf = io.BytesIO() + plt.savefig(buf, format="gif", transparent=True) + im = Image.open(buf) + assert im.mode == "P" + assert im.info["transparency"] < len(im.palette.colors) + + @pytest.mark.skipif(not features.check("webp"), reason="WebP support not available") def test_webp_alpha(): plt.plot([0, 1, 2], [0, 1, 0]) diff --git a/lib/matplotlib/tests/test_agg_filter.py b/lib/matplotlib/tests/test_agg_filter.py index dc8cff6858ae..545e62d20d7c 100644 --- a/lib/matplotlib/tests/test_agg_filter.py +++ b/lib/matplotlib/tests/test_agg_filter.py @@ -5,7 +5,7 @@ @image_comparison(baseline_images=['agg_filter_alpha'], - extensions=['png', 'pdf']) + extensions=['gif', 'png', 'pdf']) def test_agg_filter_alpha(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False