Skip to content

ENH: Add support to save images in WebP format #21274

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 4 commits into from
Oct 15, 2021
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
2 changes: 2 additions & 0 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
'svgz': 'Scalable Vector Graphics',
'tif': 'Tagged Image File Format',
'tiff': 'Tagged Image File Format',
'webp': 'WebP Image Format',
}
_default_backends = {
'eps': 'matplotlib.backends.backend_ps',
Expand All @@ -84,6 +85,7 @@
'svgz': 'matplotlib.backends.backend_svg',
'tif': 'matplotlib.backends.backend_agg',
'tiff': 'matplotlib.backends.backend_agg',
'webp': 'matplotlib.backends.backend_agg',
}


Expand Down
22 changes: 22 additions & 0 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,28 @@ def print_tif(self, filename_or_obj, *, pil_kwargs=None):

print_tiff = print_tif

@_check_savefig_extra_args
def print_webp(self, filename_or_obj, *, pil_kwargs=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all these pillow save formats the same? If so, I wonder if we just want a generic print_pillow(format=pillow_format) rather than adding these one-by-one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, it is the same for all the fully supported formats- BMP, DDS, DIB, EPS, GIF, ICNS, ICO, IM, JPEG, JPEG 2000, MSP, PCX, PNG, PPM, SGI, SPIDER, TGA, TIFF, WebP, XBM.
I think adding a generic print_pillow does make a lot of sense especially if we want to support all of these formats. The tests however will have to be created for each format, as they all have their specific options that need to be tested.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats the approach I would take, but maybe its more complex than that...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should leave it for a different PR then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that depends on whether we think webp is used enough to merit a standalone method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the way we would have to do this in general would be some mild-meta-programming as

print_method = getattr(canvas, 'print_%s' % format)
is how we look up the method to use and above it we get the format via
if format is None:
# get format from filename, or from backend's default filetype
if isinstance(filename, os.PathLike):
filename = os.fspath(filename)
if isinstance(filename, str):
format = os.path.splitext(filename)[1][1:]
if format is None or format == '':
format = self.get_default_filetype()
if isinstance(filename, str):
filename = filename.rstrip('.') + '.' + format
format = format.lower()
so we have to have a print_foo method for every format.

However, we can write ourselves a print function factory function and in backend_agg.py do something like:

for fmt in LIST_OF_PILLOW_FORMATS:
   setattr(BackendAgg, f'print_{fmt}', pillow_printer_factor(fmt)

"""
Write the figure to a WebP file.

Parameters
----------
filename_or_obj : str or path-like or file-like
The file to write to.

Other Parameters
----------------
pil_kwargs : dict, optional
Additional keyword arguments that are passed to
`PIL.Image.Image.save` when saving the figure.
"""
FigureCanvasAgg.draw(self)
if pil_kwargs is None:
pil_kwargs = {}
return (Image.fromarray(np.asarray(self.buffer_rgba()))
.save(filename_or_obj, format='webp', **pil_kwargs))


@_Backend.export
class _BackendAgg(_Backend):
Expand Down
19 changes: 19 additions & 0 deletions lib/matplotlib/tests/test_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,25 @@ def test_pil_kwargs_tiff():
assert tags["ImageDescription"] == "test image"


def test_pil_kwargs_webp():
plt.plot([0, 1, 2], [0, 1, 0])
buf_small = io.BytesIO()
pil_kwargs_low = {"quality": 1}
plt.savefig(buf_small, format="webp", pil_kwargs=pil_kwargs_low)
buf_large = io.BytesIO()
pil_kwargs_high = {"quality": 100}
plt.savefig(buf_large, format="webp", pil_kwargs=pil_kwargs_high)
assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes


def test_webp_alpha():
plt.plot([0, 1, 2], [0, 1, 0])
buf = io.BytesIO()
plt.savefig(buf, format="webp", transparent=True)
im = Image.open(buf)
assert im.mode == "RGBA"


def test_draw_path_collection_error_handling():
fig, ax = plt.subplots()
ax.scatter([1], [1]).set_paths(path.Path([(0, 1), (2, 3)]))
Expand Down