Repository: python-pillow/Pillow

Issue:
KeyError: 'WEBP' when trying to write animated WebP file
### What did you do?
Attempt to save an image sequence to a *.webp file using save_all=True.

### What did you expect to happen?
Write an animated WebP file. Looking at WebPImagePlugin.py, it's clear support hasn't been added for writing WebP image sequences yet, not sure if this is on the roadmap already or is terribly difficult to add, if it's pretty straightforward I might be able to contribute a PR for it.

### What actually happened?
Receive an error: "KeyError: 'WEBP'"

### What versions of Pillow and Python are you using?
Pillow 4.2.1, Python 3.6.0

Code to reproduce:

```python
from PIL import Image
im = Image.open('image.png')
im.save('image.webp', save_all=True)
```


Discussion:
I don't think anyone's planning on doing this (pipe up if you are), so PRs are welcome!

Edit: note there are some WebP rewrite PRs sitting around (https://github.com/python-pillow/Pillow/search?q=webp&state=open&type=Issues&utf8=%E2%9C%93) but they're not really moving.

Resolving patch:
diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py
--- a/PIL/WebPImagePlugin.py
+++ b/PIL/WebPImagePlugin.py
@@ -3,6 +3,11 @@
 
 
 _VALID_WEBP_MODES = {
+    "RGBX": True,
+    "RGBA": True,
+    }
+
+_VALID_WEBP_LEGACY_MODES = {
     "RGB": True,
     "RGBA": True,
     }
@@ -28,32 +33,263 @@ class WebPImageFile(ImageFile.ImageFile):
     format_description = "WebP image"
 
     def _open(self):
-        data, width, height, self.mode, icc_profile, exif = \
-            _webp.WebPDecode(self.fp.read())
+        if not _webp.HAVE_WEBPANIM:
+            # Legacy mode
+            data, width, height, self.mode, icc_profile, exif = \
+                _webp.WebPDecode(self.fp.read())
+            if icc_profile:
+                self.info["icc_profile"] = icc_profile
+            if exif:
+                self.info["exif"] = exif
+            self.size = width, height
+            self.fp = BytesIO(data)
+            self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
+            self._n_frames = 1
+            return
+
+        # Use the newer AnimDecoder API to parse the (possibly) animated file,
+        # and access muxed chunks like ICC/EXIF/XMP.
+        self._decoder = _webp.WebPAnimDecoder(self.fp.read())
 
+        # Get info from decoder
+        width, height, loop_count, bgcolor, frame_count, mode = \
+            self._decoder.get_info()
+        self.size = width, height
+        self.info["loop"] = loop_count
+        bg_a, bg_r, bg_g, bg_b = \
+            (bgcolor >> 24) & 0xFF, \
+            (bgcolor >> 16) & 0xFF, \
+            (bgcolor >> 8) & 0xFF, \
+            bgcolor & 0xFF
+        self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
+        self._n_frames = frame_count
+        self.mode = mode
+        self.tile = []
+
+        # Attempt to read ICC / EXIF / XMP chunks from file
+        icc_profile = self._decoder.get_chunk("ICCP")
+        exif = self._decoder.get_chunk("EXIF")
+        xmp = self._decoder.get_chunk("XMP ")
         if icc_profile:
             self.info["icc_profile"] = icc_profile
         if exif:
             self.info["exif"] = exif
+        if xmp:
+            self.info["xmp"] = xmp
 
-        self.size = width, height
-        self.fp = BytesIO(data)
-        self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
+        # Initialize seek state
+        self._reset(reset=False)
+        self.seek(0)
 
     def _getexif(self):
         from .JpegImagePlugin import _getexif
         return _getexif(self)
 
+    @property
+    def n_frames(self):
+        return self._n_frames
 
-def _save(im, fp, filename):
-    image_mode = im.mode
-    if im.mode not in _VALID_WEBP_MODES:
-        raise IOError("cannot write mode %s as WEBP" % image_mode)
+    @property
+    def is_animated(self):
+        return self._n_frames > 1
+
+    def seek(self, frame):
+        if not _webp.HAVE_WEBPANIM:
+            return super(WebPImageFile, self).seek(frame)
+
+        # Perform some simple checks first
+        if frame >= self._n_frames:
+            raise EOFError("attempted to seek beyond end of sequence")
+        if frame < 0:
+            raise EOFError("negative frame index is not valid")
+
+        # Set logical frame to requested position
+        self.__logical_frame = frame
+
+    def _reset(self, reset=True):
+        if reset:
+            self._decoder.reset()
+        self.__physical_frame = 0
+        self.__loaded = -1
+        self.__timestamp = 0
+
+    def _get_next(self):
+        # Get next frame
+        ret = self._decoder.get_next()
+        self.__physical_frame += 1
+
+        # Check if an error occurred
+        if ret is None:
+            self._reset()   # Reset just to be safe
+            self.seek(0)
+            raise EOFError("failed to decode next frame in WebP file")
+
+        # Compute duration
+        data, timestamp = ret
+        duration = timestamp - self.__timestamp
+        self.__timestamp = timestamp
+
+        # libwebp gives frame end, adjust to start of frame
+        timestamp -= duration
+        return data, timestamp, duration
+
+    def _seek(self, frame):
+        if self.__physical_frame == frame:
+            return              # Nothing to do
+        if frame < self.__physical_frame:
+            self._reset()       # Rewind to beginning
+        while self.__physical_frame < frame:
+            self._get_next()    # Advance to the requested frame
+
+    def load(self):
+        if _webp.HAVE_WEBPANIM:
+            if self.__loaded != self.__logical_frame:
+                self._seek(self.__logical_frame)
+
+                # We need to load the image data for this frame
+                data, timestamp, duration = self._get_next()
+                self.info["timestamp"] = timestamp
+                self.info["duration"] = duration
+                self.__loaded = self.__logical_frame
+
+                # Set tile
+                self.fp = BytesIO(data)
+                self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
+
+        return super(WebPImageFile, self).load()
+
+    def tell(self):
+        if not _webp.HAVE_WEBPANIM:
+            return super(WebPImageFile, self).tell()
+
+        return self.__logical_frame
+
+
+def _save_all(im, fp, filename):
+    encoderinfo = im.encoderinfo.copy()
+    append_images = encoderinfo.get("append_images", [])
+
+    # If total frame count is 1, then save using the legacy API, which
+    # will preserve non-alpha modes
+    total = 0
+    for ims in [im]+append_images:
+        total += 1 if not hasattr(ims, "n_frames") else ims.n_frames
+    if total == 1:
+        _save(im, fp, filename)
+        return
 
+    background = encoderinfo.get("background", (0, 0, 0, 0))
+    duration = im.encoderinfo.get("duration", 0)
+    loop = im.encoderinfo.get("loop", 0)
+    minimize_size = im.encoderinfo.get("minimize_size", False)
+    kmin = im.encoderinfo.get("kmin", None)
+    kmax = im.encoderinfo.get("kmax", None)
+    allow_mixed = im.encoderinfo.get("allow_mixed", False)
+    verbose = False
     lossless = im.encoderinfo.get("lossless", False)
     quality = im.encoderinfo.get("quality", 80)
+    method = im.encoderinfo.get("method", 0)
     icc_profile = im.encoderinfo.get("icc_profile", "")
     exif = im.encoderinfo.get("exif", "")
+    xmp = im.encoderinfo.get("xmp", "")
+    if allow_mixed:
+        lossless = False
+
+    # Sensible keyframe defaults are from gif2webp.c script
+    if kmin is None:
+        kmin = 9 if lossless else 3
+    if kmax is None:
+        kmax = 17 if lossless else 5
+
+    # Validate background color
+    if (not isinstance(background, (list, tuple)) or len(background) != 4 or
+            not all(v >= 0 and v < 256 for v in background)):
+        raise IOError("Background color is not an RGBA tuple clamped "
+                      "to (0-255): %s" % str(background))
+
+    # Convert to packed uint
+    bg_r, bg_g, bg_b, bg_a = background
+    background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0)
+
+    # Setup the WebP animation encoder
+    enc = _webp.WebPAnimEncoder(
+        im.size[0], im.size[1],
+        background,
+        loop,
+        minimize_size,
+        kmin, kmax,
+        allow_mixed,
+        verbose
+    )
+
+    # Add each frame
+    frame_idx = 0
+    timestamp = 0
+    cur_idx = im.tell()
+    try:
+        for ims in [im]+append_images:
+            # Get # of frames in this image
+            if not hasattr(ims, "n_frames"):
+                nfr = 1
+            else:
+                nfr = ims.n_frames
+
+            for idx in range(nfr):
+                ims.seek(idx)
+                ims.load()
+
+                # Make sure image mode is supported
+                frame = ims
+                if ims.mode not in _VALID_WEBP_MODES:
+                    alpha = ims.mode == 'P' and 'A' in ims.im.getpalettemode()
+                    frame = ims.convert('RGBA' if alpha else 'RGBX')
+
+                # Append the frame to the animation encoder
+                enc.add(
+                    frame.tobytes(),
+                    timestamp,
+                    frame.size[0], frame.size[1],
+                    frame.mode,
+                    lossless,
+                    quality,
+                    method
+                )
+
+                # Update timestamp and frame index
+                if isinstance(duration, (list, tuple)):
+                    timestamp += duration[frame_idx]
+                else:
+                    timestamp += duration
+                frame_idx += 1
+
+    finally:
+        im.seek(cur_idx)
+
+    # Force encoder to flush frames
+    enc.add(
+        None,
+        timestamp,
+        0, 0, "", lossless, quality, 0
+    )
+
+    # Get the final output from the encoder
+    data = enc.assemble(icc_profile, exif, xmp)
+    if data is None:
+        raise IOError("cannot write file as WebP (encoder returned None)")
+
+    fp.write(data)
+
+
+def _save(im, fp, filename):
+    lossless = im.encoderinfo.get("lossless", False)
+    quality = im.encoderinfo.get("quality", 80)
+    icc_profile = im.encoderinfo.get("icc_profile", "")
+    exif = im.encoderinfo.get("exif", "")
+    xmp = im.encoderinfo.get("xmp", "")
+
+    if im.mode not in _VALID_WEBP_LEGACY_MODES:
+        alpha = im.mode == 'P' and 'A' in im.im.getpalettemode()
+        im = im.convert('RGBA' if alpha else 'RGB')
 
     data = _webp.WebPEncode(
         im.tobytes(),
@@ -63,16 +299,18 @@ def _save(im, fp, filename):
         float(quality),
         im.mode,
         icc_profile,
-        exif
+        exif,
+        xmp
     )
     if data is None:
-        raise IOError("cannot write file as WEBP (encoder returned None)")
+        raise IOError("cannot write file as WebP (encoder returned None)")
 
     fp.write(data)
 
 
 Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
 Image.register_save(WebPImageFile.format, _save)
-
+if _webp.HAVE_WEBPANIM:
+    Image.register_save_all(WebPImageFile.format, _save_all)
 Image.register_extension(WebPImageFile.format, ".webp")
 Image.register_mime(WebPImageFile.format, "image/webp")
diff --git a/PIL/features.py b/PIL/features.py
--- a/PIL/features.py
+++ b/PIL/features.py
@@ -43,6 +43,7 @@ def get_supported_codecs():
     return [f for f in codecs if check_codec(f)]
 
 features = {
+    "webp_anim": ("PIL._webp", 'HAVE_WEBPANIM'),
     "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'),
     "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
     "raqm": ("PIL._imagingft", "HAVE_RAQM")
@@ -53,7 +54,7 @@ def check_feature(feature):
         raise ValueError("Unknown feature %s" % feature)
 
     module, flag = features[feature]
-    
+
     try:
         imported_module = __import__(module, fromlist=['PIL'])
         return getattr(imported_module, flag)
@@ -75,4 +76,4 @@ def get_supported():
     ret.extend(get_supported_features())
     ret.extend(get_supported_codecs())
     return ret
-        
+
diff --git a/selftest.py b/selftest.py
--- a/selftest.py
+++ b/selftest.py
@@ -178,8 +178,9 @@ def testimage():
         ("freetype2", "FREETYPE2"),
         ("littlecms2", "LITTLECMS2"),
         ("webp", "WEBP"),
-        ("transp_webp", "Transparent WEBP"),
+        ("transp_webp", "WEBP Transparency"),
         ("webp_mux", "WEBPMUX"),
+        ("webp_anim", "WEBP Animation"),
         ("jpg", "JPEG"),
         ("jpg_2000", "OPENJPEG (JPEG2000)"),
         ("zlib", "ZLIB (PNG/ZIP)"),


Test patch:
diff --git a/Tests/images/anim_frame1.webp b/Tests/images/anim_frame1.webp
new file mode 100644
Binary files /dev/null and b/Tests/images/anim_frame1.webp differ
diff --git a/Tests/images/anim_frame2.webp b/Tests/images/anim_frame2.webp
new file mode 100644
Binary files /dev/null and b/Tests/images/anim_frame2.webp differ
diff --git a/Tests/images/iss634.webp b/Tests/images/iss634.webp
new file mode 100644
Binary files /dev/null and b/Tests/images/iss634.webp differ
diff --git a/Tests/images/transparent.gif b/Tests/images/transparent.gif
new file mode 100644
Binary files /dev/null and b/Tests/images/transparent.gif differ
diff --git a/Tests/test_features.py b/Tests/test_features.py
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -35,6 +35,11 @@ def check_webp_mux(self):
         self.assertEqual(features.check('webp_mux'),
                          _webp.HAVE_WEBPMUX)
 
+    @unittest.skipUnless(HAVE_WEBP, True)
+    def check_webp_anim(self):
+        self.assertEqual(features.check('webp_anim'),
+                         _webp.HAVE_WEBPANIM)
+
     def test_check_modules(self):
         for feature in features.modules:
             self.assertIn(features.check_module(feature), [True, False])
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -4,29 +4,35 @@
 
 try:
     from PIL import _webp
+    HAVE_WEBP = True
 except ImportError:
-    # Skip in setUp()
-    pass
+    HAVE_WEBP = False
 
 
 class TestFileWebp(PillowTestCase):
 
     def setUp(self):
-        try:
-            from PIL import _webp
-        except ImportError:
+        if not HAVE_WEBP:
             self.skipTest('WebP support not installed')
+            return
+
+        # WebPAnimDecoder only returns RGBA or RGBX, never RGB
+        self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB"
 
     def test_version(self):
         _webp.WebPDecoderVersion()
         _webp.WebPDecoderBuggyAlpha()
 
     def test_read_rgb(self):
+        """
+        Can we read a RGB mode WebP file without error?
+        Does it have the bits we expect?
+        """
 
         file_path = "Tests/images/hopper.webp"
         image = Image.open(file_path)
 
-        self.assertEqual(image.mode, "RGB")
+        self.assertEqual(image.mode, self.rgb_mode)
         self.assertEqual(image.size, (128, 128))
         self.assertEqual(image.format, "WEBP")
         image.load()
@@ -35,6 +41,7 @@ def test_read_rgb(self):
         # generated with:
         # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
         target = Image.open('Tests/images/hopper_webp_bits.ppm')
+        target = target.convert(self.rgb_mode)
         self.assert_image_similar(image, target, 20.0)
 
     def test_write_rgb(self):
@@ -45,12 +52,10 @@ def test_write_rgb(self):
 
         temp_file = self.tempfile("temp.webp")
 
-        hopper("RGB").save(temp_file)
-
+        hopper(self.rgb_mode).save(temp_file)
         image = Image.open(temp_file)
-        image.load()
 
-        self.assertEqual(image.mode, "RGB")
+        self.assertEqual(image.mode, self.rgb_mode)
         self.assertEqual(image.size, (128, 128))
         self.assertEqual(image.format, "WEBP")
         image.load()
@@ -69,20 +74,67 @@ def test_write_rgb(self):
         # then we're going to accept that it's a reasonable lossy version of
         # the image. The old lena images for WebP are showing ~16 on
         # Ubuntu, the jpegs are showing ~18.
-        target = hopper("RGB")
-        self.assert_image_similar(image, target, 12)
+        target = hopper(self.rgb_mode)
+        self.assert_image_similar(image, target, 12.0)
+
+    def test_write_unsupported_mode_L(self):
+        """
+        Saving a black-and-white file to WebP format should work, and be
+        similar to the original file.
+        """
+
+        temp_file = self.tempfile("temp.webp")
+        hopper("L").save(temp_file)
+        image = Image.open(temp_file)
+
+        self.assertEqual(image.mode, self.rgb_mode)
+        self.assertEqual(image.size, (128, 128))
+        self.assertEqual(image.format, "WEBP")
+
+        image.load()
+        image.getdata()
+        target = hopper("L").convert(self.rgb_mode)
+
+        self.assert_image_similar(image, target, 10.0)
+
+    def test_write_unsupported_mode_P(self):
+        """
+        Saving a palette-based file to WebP format should work, and be
+        similar to the original file.
+        """
 
-    def test_write_unsupported_mode(self):
         temp_file = self.tempfile("temp.webp")
+        hopper("P").save(temp_file)
+        image = Image.open(temp_file)
+
+        self.assertEqual(image.mode, self.rgb_mode)
+        self.assertEqual(image.size, (128, 128))
+        self.assertEqual(image.format, "WEBP")
+
+        image.load()
+        image.getdata()
+        target = hopper("P").convert(self.rgb_mode)
 
-        im = hopper("L")
-        self.assertRaises(IOError, im.save, temp_file)
+        self.assert_image_similar(image, target, 50.0)
 
     def test_WebPEncode_with_invalid_args(self):
+        """
+        Calling encoder functions with no arguments should result in an error.
+        """
+
+        if _webp.HAVE_WEBPANIM:
+            self.assertRaises(TypeError, _webp.WebPAnimEncoder)
         self.assertRaises(TypeError, _webp.WebPEncode)
 
     def test_WebPDecode_with_invalid_args(self):
+        """
+        Calling decoder functions with no arguments should result in an error.
+        """
+
+        if _webp.HAVE_WEBPANIM:
+            self.assertRaises(TypeError, _webp.WebPAnimDecoder)
         self.assertRaises(TypeError, _webp.WebPDecode)
 
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -22,6 +22,11 @@ def setUp(self):
                           "not testing transparency")
 
     def test_read_rgba(self):
+        """
+        Can we read an RGBA mode file without error?
+        Does it have the bits we expect?
+        """
+
         # Generated with `cwebp transparent.png -o transparent.webp`
         file_path = "Tests/images/transparent.webp"
         image = Image.open(file_path)
@@ -38,6 +43,11 @@ def test_read_rgba(self):
         self.assert_image_similar(image, target, 20.0)
 
     def test_write_lossless_rgb(self):
+        """
+        Can we write an RGBA mode file with lossless compression without
+        error? Does it have the bits we expect?
+        """
+
         temp_file = self.tempfile("temp.webp")
         # temp_file = "temp.webp"
 
@@ -90,6 +100,27 @@ def test_write_rgba(self):
         else:
             self.assert_image_similar(image, pil_image, 1.0)
 
+    def test_write_unsupported_mode_PA(self):
+        """
+        Saving a palette-based file with transparency to WebP format
+        should work, and be similar to the original file.
+        """
+
+        temp_file = self.tempfile("temp.webp")
+        file_path = "Tests/images/transparent.gif"
+        Image.open(file_path).save(temp_file)
+        image = Image.open(temp_file)
+
+        self.assertEqual(image.mode, "RGBA")
+        self.assertEqual(image.size, (200, 150))
+        self.assertEqual(image.format, "WEBP")
+
+        image.load()
+        image.getdata()
+        target = Image.open(file_path).convert("RGBA")
+
+        self.assert_image_similar(image, target, 10.0)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py
new file mode 100644
--- /dev/null
+++ b/Tests/test_file_webp_animated.py
@@ -0,0 +1,145 @@
+from helper import unittest, PillowTestCase
+
+from PIL import Image
+
+try:
+    from PIL import _webp
+    HAVE_WEBP = True
+except ImportError:
+    HAVE_WEBP = False
+
+
+class TestFileWebpAnimation(PillowTestCase):
+
+    def setUp(self):
+        if not HAVE_WEBP:
+            self.skipTest('WebP support not installed')
+            return
+
+        if not _webp.HAVE_WEBPANIM:
+            self.skipTest("WebP library does not contain animation support, "
+                          "not testing animation")
+
+    def test_n_frames(self):
+        """
+        Ensure that WebP format sets n_frames and is_animated
+        attributes correctly.
+        """
+
+        im = Image.open("Tests/images/hopper.webp")
+        self.assertEqual(im.n_frames, 1)
+        self.assertFalse(im.is_animated)
+
+        im = Image.open("Tests/images/iss634.webp")
+        self.assertEqual(im.n_frames, 42)
+        self.assertTrue(im.is_animated)
+
+    def test_write_animation_L(self):
+        """
+        Convert an animated GIF to animated WebP, then compare the
+        frame count, and first and last frames to ensure they're
+        visually similar.
+        """
+
+        orig = Image.open("Tests/images/iss634.gif")
+        self.assertGreater(orig.n_frames, 1)
+
+        temp_file = self.tempfile("temp.webp")
+        orig.save(temp_file, save_all=True)
+        im = Image.open(temp_file)
+        self.assertEqual(im.n_frames, orig.n_frames)
+
+        # Compare first and last frames to the original animated GIF
+        orig.load()
+        im.load()
+        self.assert_image_similar(im, orig.convert("RGBA"), 25.0)
+        orig.seek(orig.n_frames-1)
+        im.seek(im.n_frames-1)
+        orig.load()
+        im.load()
+        self.assert_image_similar(im, orig.convert("RGBA"), 25.0)
+
+    def test_write_animation_RGB(self):
+        """
+        Write an animated WebP from RGB frames, and ensure the frames
+        are visually similar to the originals.
+        """
+
+        temp_file = self.tempfile("temp.webp")
+        temp_file2 = self.tempfile("temp.png")
+        frame1 = Image.open('Tests/images/anim_frame1.webp')
+        frame2 = Image.open('Tests/images/anim_frame2.webp')
+        frame1.save(temp_file,
+            save_all=True, append_images=[frame2], lossless=True)
+
+        im = Image.open(temp_file)
+        self.assertEqual(im.n_frames, 2)
+
+        # Compare first frame to original
+        im.load()
+        im.save(temp_file2)
+        self.assert_image_equal(im, frame1.convert("RGBA"))
+
+        # Compare second frame to original
+        im.seek(1)
+        im.load()
+        self.assert_image_equal(im, frame2.convert("RGBA"))
+
+    def test_timestamp_and_duration(self):
+        """
+        Try passing a list of durations, and make sure the encoded
+        timestamps and durations are correct.
+        """
+
+        durations = [0, 10, 20, 30, 40]
+        temp_file = self.tempfile("temp.webp")
+        frame1 = Image.open('Tests/images/anim_frame1.webp')
+        frame2 = Image.open('Tests/images/anim_frame2.webp')
+        frame1.save(temp_file, save_all=True,
+                    append_images=[frame2, frame1, frame2, frame1],
+                    duration=durations)
+
+        im = Image.open(temp_file)
+        self.assertEqual(im.n_frames, 5)
+        self.assertTrue(im.is_animated)
+
+        # Check that timestamps and durations match original values specified
+        ts = 0
+        for frame in range(im.n_frames):
+            im.seek(frame)
+            im.load()
+            self.assertEqual(im.info["duration"], durations[frame])
+            self.assertEqual(im.info["timestamp"], ts)
+            ts += durations[frame]
+
+    def test_seeking(self):
+        """
+        Create an animated WebP file, and then try seeking through
+        frames in reverse-order, verifying the timestamps and durations
+        are correct.
+        """
+
+        dur = 33
+        temp_file = self.tempfile("temp.webp")
+        frame1 = Image.open('Tests/images/anim_frame1.webp')
+        frame2 = Image.open('Tests/images/anim_frame2.webp')
+        frame1.save(temp_file, save_all=True,
+                    append_images=[frame2, frame1, frame2, frame1],
+                    duration=dur)
+
+        im = Image.open(temp_file)
+        self.assertEqual(im.n_frames, 5)
+        self.assertTrue(im.is_animated)
+
+        # Traverse frames in reverse, checking timestamps and durations
+        ts = dur * (im.n_frames-1)
+        for frame in reversed(range(im.n_frames)):
+            im.seek(frame)
+            im.load()
+            self.assertEqual(im.info["duration"], dur)
+            self.assertEqual(im.info["timestamp"], ts)
+            ts -= dur
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py
--- a/Tests/test_file_webp_lossless.py
+++ b/Tests/test_file_webp_lossless.py
@@ -4,37 +4,39 @@
 
 try:
     from PIL import _webp
+    HAVE_WEBP = True
 except ImportError:
-    pass
-    # Skip in setUp()
+    HAVE_WEBP = False
 
 
 class TestFileWebpLossless(PillowTestCase):
 
     def setUp(self):
-        try:
-            from PIL import _webp
-        except:
+        if not HAVE_WEBP:
             self.skipTest('WebP support not installed')
+            return
 
         if (_webp.WebPDecoderVersion() < 0x0200):
             self.skipTest('lossless not included')
 
+        # WebPAnimDecoder only returns RGBA or RGBX, never RGB
+        self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB"
+
     def test_write_lossless_rgb(self):
         temp_file = self.tempfile("temp.webp")
 
-        hopper("RGB").save(temp_file, lossless=True)
+        hopper(self.rgb_mode).save(temp_file, lossless=True)
 
         image = Image.open(temp_file)
         image.load()
 
-        self.assertEqual(image.mode, "RGB")
+        self.assertEqual(image.mode, self.rgb_mode)
         self.assertEqual(image.size, (128, 128))
         self.assertEqual(image.format, "WEBP")
         image.load()
         image.getdata()
 
-        self.assert_image_equal(image, hopper("RGB"))
+        self.assert_image_equal(image, hopper(self.rgb_mode))
 
 
 if __name__ == '__main__':
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -2,13 +2,17 @@
 
 from PIL import Image
 
+try:
+    from PIL import _webp
+    HAVE_WEBP = True
+except ImportError:
+    HAVE_WEBP = False
+
 
 class TestFileWebpMetadata(PillowTestCase):
 
     def setUp(self):
-        try:
-            from PIL import _webp
-        except ImportError:
+        if not HAVE_WEBP:
             self.skipTest('WebP support not installed')
             return
 
@@ -107,6 +111,29 @@ def test_read_no_exif(self):
 
         self.assertFalse(webp_image._getexif())
 
+    def test_write_animated_metadata(self):
+        if not _webp.HAVE_WEBPANIM:
+            self.skipTest('WebP animation support not available')
+
+        iccp_data = '<iccp_data>'.encode('utf-8')
+        exif_data = '<exif_data>'.encode('utf-8')
+        xmp_data = '<xmp_data>'.encode('utf-8')
+
+        temp_file = self.tempfile("temp.webp")
+        frame1 = Image.open('Tests/images/anim_frame1.webp')
+        frame2 = Image.open('Tests/images/anim_frame2.webp')
+        frame1.save(temp_file, save_all=True,
+                    append_images=[frame2, frame1, frame2],
+                    icc_profile=iccp_data, exif=exif_data, xmp=xmp_data)
+
+        image = Image.open(temp_file)
+        self.assertIn('icc_profile', image.info)
+        self.assertIn('exif', image.info)
+        self.assertIn('xmp', image.info)
+        self.assertEqual(iccp_data, image.info.get('icc_profile', None))
+        self.assertEqual(exif_data, image.info.get('exif', None))
+        self.assertEqual(xmp_data, image.info.get('xmp', None))
+
 
 if __name__ == '__main__':
     unittest.main()