Skip to content

DOC / BUG: Fix savefig to GIF format with .gif suffix #29372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/users/next_whats_new/gif_savefig.rst
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
7 changes: 5 additions & 2 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions lib/matplotlib/testing/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@
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

Check warning on line 109 in lib/matplotlib/testing/compare.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/testing/compare.py#L108-L109

Added lines #L108 - L109 were not covered by tests


class _GSConverter(_Converter):
def __call__(self, orig, dest):
if not self._proc:
Expand Down Expand Up @@ -230,6 +240,12 @@


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:
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/testing/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions lib/matplotlib/tests/test_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_agg_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading