Repository: sphinx-gallery/sphinx-gallery

Issue:
Add WEBP to _KNOWN_IMG_EXTS
I was wondering if we could add "webp" to the list of known image extensions that scrapers may produce. The change would be a one-liner in:

https://github.com/sphinx-gallery/sphinx-gallery/blob/e2ecb35d9049d0df0a1db4f6d15be817745cd565/sphinx_gallery/scrapers.py#L339

which would have to be changed to:

```python
_KNOWN_IMG_EXTS = ('png', 'svg', 'jpg', 'gif', 'webp')
```

--- 

The background for this is that I have recently built a custom scraper for the gallery of a rendering engine. This works great, but now I would like to add animations to some of the examples. Technically, GIF could work but the limitation of 256 colors really shows when you have complex shadows or gradients and that may send the wrong message with respect to the project's rendering capabilities. (It's wgpu/vulkan based, so it has all the bells and whistles and 265 colors don't do this justice.)

[WEBP ](https://developers.google.com/speed/webp) would be a nice alternative here, because it, too, has wide browser support, better compression than GIF and - most importantly - uses YUV420 instead of 8-bit palette RGB as a colorspace. The only thing "preventing" me from using it is sphinx-gallery, which currently hardcodes supported file extensions...

As a current workaround I am monkey-patching `_KNOWN_IMG_EXTS` in the `_get_sg_image_scraper` like so:

```python
def _get_sg_image_scraper():
    # hack webp as supported extension
    import sphinx_gallery.scrapers
    sphinx_gallery.scrapers._KNOWN_IMG_EXTS += ("webp",)

    ...
```

This works, but it would be much nicer if I wouldn't have to rely on a hack that modifies private variables from an upstream project.

Discussion:
larsoner: > Do we need tests for it? [test_custom_scraper_thumbnail_alpha](https://github.com/sphinx-gallery/sphinx-gallery/blob/b17bf5500a6b3340725cbf01207a2c3d1da14c3b/sphinx_gallery/tests/test_gen_rst.py#L445) could be parameterized with _KNOWN_IMG_EXTS but I'm not sure if this really gives more/better test coverage.

I'm less concerned about line coverage as I am about making sure the feature actually works as desired. For example, you could modify some `tinybuild` example to make matplotlib savefig in webp format (or whatever you're going to do in the matplotlib doc build to get it to actually use webp) then `assert` that `sphx_glr_<something>.webp` shows up on disk and in the HTML in `test_full.py` somewhere.
StefRe: @larsoner I'm a bit at a loss: tests run locally (Win10 / Python 3.10) without errors (218 passed out of 218 collected, nothing skipped). Do you happen to have any idea how to tackle this? Installed Pillow version is 9.4.0 on CI - the same as on my local machine.
larsoner: You incremented the file counter but I don't see a new example, did you forget a `git add sphinx_gallery/tests/tinybuild/examples/plot_webp_thumb.py` or so?
StefRe: OMG, yes of course - there should be a `sphinx_gallery/tests/tinybuild/examples/plot_webp.py` that's not included here. Thank you so much! I stared for maybe an hour at the test logs but didn't see the most obvious thing.

Patch:
diff --git a/doc/advanced.rst b/doc/advanced.rst
index 608f380..ad3ef45 100644
--- a/doc/advanced.rst
+++ b/doc/advanced.rst
@@ -154,8 +154,8 @@ Image scrapers are functions (or callable class instances) that do the following
 things:
 
 1. Collect a list of images created in the latest execution of code.
-2. Write these images to disk in PNG, JPEG, SVG, or GIF format (with .png,
-   .jpg, .svg, or .gif extensions, respectively)
+2. Write these images to disk in PNG, JPEG, SVG, GIF, or WebP format (with .png,
+   .jpg, .svg, .gif, or .webp extensions, respectively)
 3. Return reST that embeds these figures in the built documentation.
 
 The function should take the following inputs (in this order):
@@ -197,8 +197,8 @@ reST (see below).
 
 This function will be called once for each code block of your examples.
 Sphinx-Gallery will take care of scaling images for the gallery
-index page thumbnails. PNG images are scaled using Pillow, and
-SVG images are copied.
+index page thumbnails. PNG, JPEG and WebP images are scaled using Pillow, and
+SVG and GIF images are copied.
 
 .. warning:: SVG images do not work with ``latex`` build modes, thus will not
              work while building a PDF version of your documentation.
@@ -305,8 +305,8 @@ Example 3: matplotlib with SVG format
 -------------------------------------
 The :func:`sphinx_gallery.scrapers.matplotlib_scraper` supports ``**kwargs``
 to pass to :meth:`matplotlib.figure.Figure.savefig`, one of which is the
-``format`` argument. Currently Sphinx-Gallery supports PNG (default) and SVG
-output formats. To use SVG, you can do::
+``format`` argument. See :ref:`custom_scraper` for supported formats.
+To use SVG, you can do::
 
     from sphinx_gallery.scrapers import matplotlib_scraper
 
diff --git a/sphinx_gallery/scrapers.py b/sphinx_gallery/scrapers.py
index b332f2c..0de09aa 100644
--- a/sphinx_gallery/scrapers.py
+++ b/sphinx_gallery/scrapers.py
@@ -100,8 +100,8 @@ def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
         Additional keyword arguments to pass to
         :meth:`~matplotlib.figure.Figure.savefig`, e.g. ``format='svg'``.
         The ``format`` kwarg in particular is used to set the file extension
-        of the output file (currently only 'png', 'jpg', and 'svg' are
-        supported).
+        of the output file (currently only 'png', 'jpg', 'svg', 'gif', and
+        'webp' are supported).
 
     Returns
     -------
@@ -300,7 +300,8 @@ class ImagePathIterator:
 
 
 # For now, these are what we support
-_KNOWN_IMG_EXTS = ('png', 'svg', 'jpg', 'gif')
+# Update advanced.rst if this list is changed
+_KNOWN_IMG_EXTS = ('png', 'svg', 'jpg', 'gif', 'webp')
 
 
 def _find_image_ext(path):


Test patch:
diff --git a/sphinx_gallery/tests/test_full.py b/sphinx_gallery/tests/test_full.py
index 4698169..4cc65cf 100644
--- a/sphinx_gallery/tests/test_full.py
+++ b/sphinx_gallery/tests/test_full.py
@@ -30,7 +30,7 @@ import pytest
 
 # total number of plot_*.py files in tinybuild/examples + examples_rst_index
 # + examples_with_rst
-N_EXAMPLES = 14 + 3 + 2
+N_EXAMPLES = 15 + 3 + 2
 N_FAILING = 2
 N_GOOD = N_EXAMPLES - N_FAILING  # galleries that run w/o error
 # passthroughs examples_rst_index, examples_with_rst
@@ -289,6 +289,7 @@ def test_image_formats(sphinx_app):
     thumb_fnames = ['../_images/sphx_glr_plot_svg_thumb.svg',
                     '../_images/sphx_glr_plot_numpy_matplotlib_thumb.png',
                     '../_images/sphx_glr_plot_animation_thumb.gif',
+                    '../_images/sphx_glr_plot_webp_thumb.webp',
                     ]
     for thumb_fname in thumb_fnames:
         file_fname = op.join(generated_examples_dir, thumb_fname)
@@ -300,7 +301,8 @@ def test_image_formats(sphinx_app):
     for ex, ext, nums, extra in (
             ('plot_svg', 'svg', [1], None),
             ('plot_numpy_matplotlib', 'png', [1], None),
-            ('plot_animation', 'png', [1, 3], 'function Animation')):
+            ('plot_animation', 'png', [1, 3], 'function Animation'),
+            ('plot_webp', 'webp', [1], None)):
         html_fname = op.join(generated_examples_dir, '%s.html' % ex)
         with codecs.open(html_fname, 'r', 'utf-8') as fid:
             html = fid.read()
@@ -314,7 +316,8 @@ def test_image_formats(sphinx_app):
                           (ex, num, ext))
             file_fname2 = op.join(generated_examples_dir, img_fname2)
             want_html = f'srcset="{img_fname0}, {img_fname2} 2.00x"'
-            if ext in ('png', 'jpg', 'svg'):  # check 2.00x (tests directive)
+            if ext in ('png', 'jpg', 'svg', 'webp'):
+                # check 2.00x (tests directive)
                 assert op.isfile(file_fname2), file_fname2
                 assert want_html in html
 
diff --git a/sphinx_gallery/tests/tinybuild/doc/conf.py b/sphinx_gallery/tests/tinybuild/doc/conf.py
index 6a7e4e2..8ef2a50 100644
--- a/sphinx_gallery/tests/tinybuild/doc/conf.py
+++ b/sphinx_gallery/tests/tinybuild/doc/conf.py
@@ -13,6 +13,9 @@ class matplotlib_format_scraper:
         if op.basename(block_vars['target_file']) == 'plot_svg.py' and \
                 gallery_conf['builder_name'] != 'latex':
             kwargs['format'] = 'svg'
+        elif op.basename(block_vars['target_file']) == 'plot_webp.py' and \
+                gallery_conf['builder_name'] != 'latex':
+            kwargs['format'] = 'webp'
         return matplotlib_scraper(block, block_vars, gallery_conf, **kwargs)
 
 
diff --git a/sphinx_gallery/tests/tinybuild/examples/plot_webp.py b/sphinx_gallery/tests/tinybuild/examples/plot_webp.py
new file mode 100644
index 0000000..a7c3da3
--- /dev/null
+++ b/sphinx_gallery/tests/tinybuild/examples/plot_webp.py
@@ -0,0 +1,9 @@
+"""
+============
+Save as WebP
+============
+"""
+
+import matplotlib.pyplot as plt
+
+plt.plot([1, 2])