diff --git a/Images/transparent.png b/Images/transparent.png new file mode 100644 index 000000000..902ea8272 Binary files /dev/null and b/Images/transparent.png differ diff --git a/Images/transparent.webp b/Images/transparent.webp new file mode 100644 index 000000000..c1e38022e Binary files /dev/null and b/Images/transparent.webp differ diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index 9321c05f3..260b43e3d 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -3,29 +3,55 @@ from PIL import ImageFile 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/README.rst b/README.rst index 91d78ed86..12e5716b2 100644 --- a/README.rst +++ b/README.rst @@ -134,7 +134,9 @@ Some (most?) of Pillow's features require external libraries. * **littlecms** provides color management -* **libwebp** provides the Webp format. +* **libwebp** provides the Webp format. + + * Pillow has been tested with version **0.1.3**, which does not read transparent webp files. Version **0.3.0** supports transparency. If the prerequisites are installed in the standard library locations for your machine (e.g. /usr or /usr/local), no additional configuration should be required. If they are installed in a non-standard location, you may need to configure setuptools to use those locations (i.e. by editing setup.py and/or setup.cfg) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 5d48f0aff..30fd94d58 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -6,54 +6,108 @@ try: 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?""" + + +def test_version(): + assert_no_exception(lambda: _webp.WebPDecoderVersion()) + +def test_good_alpha(): + assert_equal(_webp.WebPDecoderBuggyAlpha(), 0) - 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_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) + diff --git a/_webp.c b/_webp.c index 0fd5be2f4..cb872bc28 100644 --- a/_webp.c +++ b/_webp.c @@ -2,73 +2,128 @@ #include "py3.h" #include #include +#include -PyObject* WebPEncodeRGB_wrapper(PyObject* self, PyObject* args) + +PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) { - PyBytesObject *rgb_string; int width; int height; - int stride; float quality_factor; uint8_t *rgb; uint8_t *output; + char *mode; Py_ssize_t size; size_t ret_size; - if (!PyArg_ParseTuple(args, "Siiif", &rgb_string, &width, &height, &stride, &quality_factor)) { - Py_INCREF(Py_None); - return Py_None; + if (!PyArg_ParseTuple(args, "s#iifs",(char**)&rgb, &size, &width, &height, &quality_factor, &mode)) { + Py_RETURN_NONE; } - PyBytes_AsStringAndSize((PyObject *) rgb_string, (char**)&rgb, &size); + if (strcmp(mode, "RGBA")==0){ + if (size < width * height * 4){ + Py_RETURN_NONE; + } + ret_size = WebPEncodeRGBA(rgb, width, height, 4* width, quality_factor, &output); + } else if (strcmp(mode, "RGB")==0){ + if (size < width * height * 3){ + Py_RETURN_NONE; + } + ret_size = WebPEncodeRGB(rgb, width, height, 3* width, quality_factor, &output); + } else { + Py_RETURN_NONE; + } - if (stride * height > size) { - Py_INCREF(Py_None); - return Py_None; - } - - ret_size = WebPEncodeRGB(rgb, width, height, stride, quality_factor, &output); if (ret_size > 0) { PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size); free(output); return ret; } - Py_INCREF(Py_None); - return Py_None; - + Py_RETURN_NONE; } -PyObject* WebPDecodeRGB_wrapper(PyObject* self, PyObject* args) + +PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) { PyBytesObject *webp_string; - int width; - int height; uint8_t *webp; - uint8_t *output; Py_ssize_t size; - PyObject *ret; + PyObject *ret, *bytes, *pymode; + WebPDecoderConfig config; + VP8StatusCode vp8_status_code = VP8_STATUS_OK; + char* mode = "RGB"; if (!PyArg_ParseTuple(args, "S", &webp_string)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } + if (!WebPInitDecoderConfig(&config)) { + Py_RETURN_NONE; + } + PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&webp, &size); - output = WebPDecodeRGB(webp, size, &width, &height); + vp8_status_code = WebPGetFeatures(webp, size, &config.input); + if (vp8_status_code == VP8_STATUS_OK) { + // If we don't set it, we don't get alpha. + // Initialized to MODE_RGB + if (config.input.has_alpha) { + config.output.colorspace = MODE_RGBA; + mode = "RGBA"; + } + vp8_status_code = WebPDecode(webp, size, &config); + } + + if (vp8_status_code != VP8_STATUS_OK) { + WebPFreeDecBuffer(&config.output); + Py_RETURN_NONE; + } + + if (config.output.colorspace < MODE_YUV) { + bytes = PyBytes_FromStringAndSize((char *)config.output.u.RGBA.rgba, + config.output.u.RGBA.size); + } else { + // Skipping YUV for now. Need Test Images. + // UNDONE -- unclear if we'll ever get here if we set mode_rgb* + bytes = PyBytes_FromStringAndSize((char *)config.output.u.YUVA.y, + config.output.u.YUVA.y_size); + } - ret = PyBytes_FromStringAndSize((char*)output, width * height * 3); - free(output); - return Py_BuildValue("Sii", ret, width, height); +#if PY_VERSION_HEX >= 0x03000000 + pymode = PyUnicode_FromString(mode); +#else + pymode = PyString_FromString(mode); +#endif + ret = Py_BuildValue("SiiS", bytes, config.output.width, + config.output.height, pymode); + WebPFreeDecBuffer(&config.output); + return ret; +} + +// Return the decoder's version number, packed in hexadecimal using 8bits for +// each of major/minor/revision. E.g: v2.5.7 is 0x020507. +PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){ + return Py_BuildValue("i", WebPGetDecoderVersion()); +} + +/* + * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. + * Files that are valid with 0.3 are reported as being invalid. + */ +PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ + return Py_BuildValue("i", WebPGetDecoderVersion()==0x0103); } static PyMethodDef webpMethods[] = { - {"WebPEncodeRGB", WebPEncodeRGB_wrapper, METH_VARARGS, "WebPEncodeRGB"}, - {"WebPDecodeRGB", WebPDecodeRGB_wrapper, METH_VARARGS, "WebPEncodeRGB"}, + {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, + {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, + {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"}, + {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"}, {NULL, NULL} }; + #if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__webp(void) { diff --git a/selftest.py b/selftest.py index 91a14bb54..252231fbf 100644 --- a/selftest.py +++ b/selftest.py @@ -190,6 +190,14 @@ if __name__ == "__main__": 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!