Repository: python-pillow/Pillow

Issue:
Add support for RGBA webp image encoding and decoding
Would it be possible to wrap the `WebPEncodeRGBA` and `WebPDecodeRGBA` functionality of the webp library inside Pillow?



Discussion:
Maybe, can you send a pull request?

My ability to write C isn't great, but I can certainly have a go!

If you can write it we can review it! I'm not a big C person either but I think @wiredfool is

OK. Will do. The RGB encoded and decoder certainly provide a good template to do this. My largest concern is code reuse, but I am happy to receive refactoring suggestions :)

I'm not a big C person, though I know enough to be dangerous or effective, depending on your perspective. (Tall, yes. Big, No. Also, I know C and 25 other letters :)

I'd also say that code can be refactored and replaced, but interfaces are forever. 

So we should be careful about the interface. I'm not up to date on exactly what's going on in the webp library, but as a user, I'd expect that I'd call image.open('foo.webp') and get back what I want, no matter if it's RGB or RGBA. The bits in WebPImagePlugin look a little iffy there.  Save looks a little easier to manage, since we know what we have prior to calling out to the webp library. 

Haha, I've made a start on it and think that there's probably not a lot than can be done about the interface.

I've also come to the conclusion that the python wrapper should be able to auto-detect the type of webp image being opened from the header and use the appropriate decoder.

In the other direction, it should be possible to determine the encoder from the mode of the image, although I'm not sure how to do lossless. I guess that would need to be one of the options passed to the `save()` method

@euangoddard Got a pull request for us :question:  :smiley: 

Yes and no. I have actually fixed the issue, but I realised from reading the webp spec that there are several issues with (a) the existing implementation and (b) the extension of that which I implemented.

I am having some trouble in implementing the C-code to proxy `WebPGetFeatures` which is necessary to be able to correctly identify webp images. If I can get some help in wrapping this I should be able to support this and the lossless encoder and decoder. 

OK 2.1.0 goes out July 1 so maybe you can get something in by then? Partial implementations are probably OK as long as nothing is obviously broken come release time. (The webp support is new as of 2.0.0 anyway IIRC, so it will probably take a while to settle.) Thanks for working on this

I can certainly commit the webp alpha work that I had working since I'm pretty sure that was working. I started to look into adding support for the features detection as I found that I was replicating a lot of stuff in python that's already done inside the webp library.

@euangoddard Commit what you have into a branch in your fork, and I'll see what I can do to help you with WebPGetFeatures. 

@wiredfool thanks! I'll do that. It's in a bit of a crappy state in the _webp.c file, but otherwise it should all work.


Resolving patch:
diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py
--- a/PIL/WebPImagePlugin.py
+++ b/PIL/WebPImagePlugin.py
@@ -3,29 +3,55 @@
 from io import BytesIO
 import _webp
 
+
+_VALID_WEBP_MODES = {
+    "RGB": True,
+    "RGBA": True,
+    }
+
+_VP8_MODES_BY_IDENTIFIER = {
+    b"VP8 ": "RGB",
+    b"VP8X": "RGBA",
+    } 
+
+
 def _accept(prefix):
-    return prefix[:4] == b"RIFF" and prefix[8:16] == b"WEBPVP8 "
+    is_riff_file_format = prefix[:4] == b"RIFF"
+    is_webp_file = prefix[8:12] == b"WEBP"
+    is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER
+    
+    return is_riff_file_format and is_webp_file and is_valid_vp8_mode
+
 
 class WebPImageFile(ImageFile.ImageFile):
 
     format = "WEBP"
     format_description = "WebP image"
 
-    def _open(self):
-        self.mode = "RGB"
-        data, width, height = _webp.WebPDecodeRGB(self.fp.read())
+    def _open(self):                
+        data, width, height, self.mode = _webp.WebPDecode(self.fp.read())
         self.size = width, height
         self.fp = BytesIO(data)
-        self.tile = [("raw", (0, 0) + self.size, 0, 'RGB')]
+        self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
+
 
 def _save(im, fp, filename):
-    if im.mode != "RGB":
-        raise IOError("cannot write mode %s as WEBP" % im.mode)
+    image_mode = im.mode
+    if im.mode not in _VALID_WEBP_MODES:
+        raise IOError("cannot write mode %s as WEBP" % image_mode)
+    
     quality = im.encoderinfo.get("quality", 80)
     
-    data = _webp.WebPEncodeRGB(im.tobytes(), im.size[0], im.size[1], im.size[0] * 3, float(quality))
+    data = _webp.WebPEncode(
+        im.tobytes(),
+        im.size[0],
+        im.size[1],
+        float(quality),
+		im.mode
+        )
     fp.write(data)
 
+
 Image.register_open("WEBP", WebPImageFile, _accept)
 Image.register_save("WEBP", _save)
 
diff --git a/selftest.py b/selftest.py
--- a/selftest.py
+++ b/selftest.py
@@ -190,6 +190,14 @@ def check_codec(feature, codec):
     check_module("FREETYPE2", "_imagingft")
     check_module("LITTLECMS", "_imagingcms")
     check_module("WEBP", "_webp")
+    try:
+        import _webp
+        if _webp.WebPDecoderBuggyAlpha():
+            print("***", "Transparent WEBP", "support not installed")
+        else:
+            print("---", "Transparent WEBP", "support ok")
+    except Exception:
+        pass
     print("-"*68)
 
     # use doctest to make sure the test program behaves as documented!


Test patch:
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
@@ -6,54 +6,108 @@
     import _webp
 except:
     skip('webp support not installed')
-		
-def test_read():
-    """ Can we write a webp without error. Does it have the bits we expect?"""
-    
-    file = "Images/lena.webp"
-    im = Image.open(file)
 
-    assert_equal(im.mode, "RGB")
-    assert_equal(im.size, (128, 128))
-    assert_equal(im.format, "WEBP")
-    assert_no_exception(lambda: im.load())
-    assert_no_exception(lambda: im.getdata())
 
-    orig_bytes  = im.tobytes()
+def test_version():
+    assert_no_exception(lambda: _webp.WebPDecoderVersion())
+
+def test_good_alpha():
+    assert_equal(_webp.WebPDecoderBuggyAlpha(), 0)
+    
 
+def test_read_rgb():
+     
+    file_path = "Images/lena.webp"
+    image = Image.open(file_path)
+ 
+    assert_equal(image.mode, "RGB")
+    assert_equal(image.size, (128, 128))
+    assert_equal(image.format, "WEBP")
+    assert_no_exception(lambda: image.load())
+    assert_no_exception(lambda: image.getdata())
+ 
     # generated with: dwebp -ppm ../../Images/lena.webp -o lena_webp_bits.ppm
     target = Image.open('Tests/images/lena_webp_bits.ppm')
-    assert_image_equal(im, target)
+    assert_image_equal(image, target)
     
 
-def test_write():
-    """ Can we write a webp without error. Does it have the bits we expect?"""
-
-    file = tempfile("temp.webp")
-
-    lena("RGB").save(file)
-
-    im= Image.open(file)
-    im.load()
-
-    assert_equal(im.mode, "RGB")
-    assert_equal(im.size, (128, 128))
-    assert_equal(im.format, "WEBP")
-    assert_no_exception(lambda: im.load())
-    assert_no_exception(lambda: im.getdata())
-
+def test_write_rgb():
+    """
+    Can we write a RGB mode file to webp without error. Does it have the bits we
+    expect?
+      
+    """
+  
+    temp_file = tempfile("temp.webp")
+  
+    lena("RGB").save(temp_file)
+  
+    image = Image.open(temp_file)
+    image.load()
+  
+    assert_equal(image.mode, "RGB")
+    assert_equal(image.size, (128, 128))
+    assert_equal(image.format, "WEBP")
+    assert_no_exception(lambda: image.load())
+    assert_no_exception(lambda: image.getdata())
+  
     # If we're using the exact same version of webp, this test should pass.
     # but it doesn't if the webp is generated on Ubuntu and tested on Fedora.
-    
+      
     # generated with: dwebp -ppm temp.webp -o lena_webp_write.ppm
     #target = Image.open('Tests/images/lena_webp_write.ppm')
-    #assert_image_equal(im, target)
-
+    #assert_image_equal(image, target)
+  
     # This test asserts that the images are similar. If the average pixel difference
     # between the two images is less than the epsilon value, then we're going to
     # accept that it's a reasonable lossy version of the image. The included lena images
     # for webp are showing ~16 on Ubuntu, the jpegs are showing ~18. 
-    target = lena('RGB')
-    assert_image_similar(im, target, 20.0)
+    target = lena("RGB")
+    assert_image_similar(image, target, 20.0)
+ 
+ 
+def test_write_rgba():
+    """
+    Can we write a RGBA mode file to webp without error. Does it have the bits we
+    expect?
+     
+    """
+ 
+    temp_file = tempfile("temp.webp")
+ 
+    pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
+    pil_image.save(temp_file)
+
+    if _webp.WebPDecoderBuggyAlpha():
+        return
+
+    image = Image.open(temp_file)
+    image.load()
+ 
+    assert_equal(image.mode, "RGBA")
+    assert_equal(image.size, (10, 10))
+    assert_equal(image.format, "WEBP")
+    assert_no_exception(image.load)
+    assert_no_exception(image.getdata)
+ 
+    assert_image_similar(image, pil_image, 1.0)
 
+if _webp.WebPDecoderBuggyAlpha():
+    skip("Buggy early version of webp installed, not testing transparency")
 
+def test_read_rgba():
+    # Generated with `cwebp transparent.png -o transparent.webp`
+    file_path = "Images/transparent.webp"
+    image = Image.open(file_path)
+    
+    assert_equal(image.mode, "RGBA")
+    assert_equal(image.size, (200, 150))
+    assert_equal(image.format, "WEBP")
+    assert_no_exception(lambda: image.load())
+    assert_no_exception(lambda: image.getdata())
+    
+    orig_bytes  = image.tobytes()
+    
+    target = Image.open('Images/transparent.png')
+    assert_image_similar(image, target, 20.0)
+