From c18d26b04b74e052ef6774c972da25d752818780 Mon Sep 17 00:00:00 2001 From: Jason Douglas Date: Wed, 27 Sep 2017 19:04:24 -0700 Subject: [PATCH] - Conditonally compile animation support, only if the mux.h and demux.h headers meet the ABI version requirements - Add WEBPMUX support back to WebPDecode_wrapper (to support older versions of libwebp that have mux support, but not animation) - Add HAVE_WEBPANIM flag, and use it appropriately - Update documentation / tests --- PIL/WebPImagePlugin.py | 14 +++--- PIL/features.py | 5 +- Tests/test_features.py | 5 ++ Tests/test_file_webp_animated.py | 4 +- Tests/test_file_webp_metadata.py | 14 ++++-- _webp.c | 71 +++++++++++++++++++++++++--- docs/handbook/image-file-formats.rst | 4 +- selftest.py | 3 +- 8 files changed, 96 insertions(+), 24 deletions(-) diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index 1472d67f7..9b15bf14f 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -33,7 +33,7 @@ class WebPImageFile(ImageFile.ImageFile): format_description = "WebP image" def _open(self): - if not _webp.HAVE_WEBPMUX: + if not _webp.HAVE_WEBPANIM: # Legacy mode data, width, height, self.mode = _webp.WebPDecode(self.fp.read()) self.size = width, height @@ -80,18 +80,18 @@ class WebPImageFile(ImageFile.ImageFile): @property def n_frames(self): - if not _webp.HAVE_WEBPMUX: + if not _webp.HAVE_WEBPANIM: return 1 return self._n_frames @property def is_animated(self): - if not _webp.HAVE_WEBPMUX: + if not _webp.HAVE_WEBPANIM: return False return self._n_frames > 1 def seek(self, frame): - if not _webp.HAVE_WEBPMUX: + if not _webp.HAVE_WEBPANIM: return super(WebPImageFile, self).seek(frame) # Perform some simple checks first @@ -141,7 +141,7 @@ class WebPImageFile(ImageFile.ImageFile): self._get_next() def load(self): - if _webp.HAVE_WEBPMUX: + if _webp.HAVE_WEBPANIM: if self.__loaded != self.__logical_frame: self._seek(self.__logical_frame) @@ -158,7 +158,7 @@ class WebPImageFile(ImageFile.ImageFile): return super(WebPImageFile, self).load() def tell(self): - if not _webp.HAVE_WEBPMUX: + if not _webp.HAVE_WEBPANIM: return super(WebPImageFile, self).tell() return self.__logical_frame @@ -301,7 +301,7 @@ def _save(im, fp, filename): Image.register_open(WebPImageFile.format, WebPImageFile, _accept) Image.register_save(WebPImageFile.format, _save) -if _webp.HAVE_WEBPMUX: +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 index 60f4c10ca..9cbd523c9 100644 --- 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/Tests/test_features.py b/Tests/test_features.py index cdabcad5e..54d668d2f 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -35,6 +35,11 @@ class TestFeatures(PillowTestCase): 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_animated.py b/Tests/test_file_webp_animated.py index 1aaceed04..16c6de129 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -17,8 +17,8 @@ class TestFileWebpAnimation(PillowTestCase): except ImportError: self.skipTest('WebP support not installed') - if not _webp.HAVE_WEBPMUX: - self.skipTest("WebP not compiled with mux support, " + if not _webp.HAVE_WEBPANIM: + self.skipTest("WebP library does not contain animation support, " "not testing animation") def test_n_frames(self): diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index f773b802f..cb45f45fa 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -2,17 +2,22 @@ from helper import unittest, PillowTestCase from PIL import Image +try: + from PIL import _webp + HAVE_WEBP = True + HAVE_WEBPMUX = _webp.HAVE_WEBPMUX + HAVE_WEBPANIM = _webp.HAVE_WEBPANIM +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 - if not _webp.HAVE_WEBPMUX: + if not HAVE_WEBPMUX: self.skipTest('WebPMux support not installed') def test_read_exif_metadata(self): @@ -107,6 +112,7 @@ class TestFileWebpMetadata(PillowTestCase): self.assertFalse(webp_image._getexif()) + @unittest.skipUnless(HAVE_WEBPANIM, 'WebP animation support not available') def test_write_animated_metadata(self): iccp_data = ''.encode('utf-8') exif_data = ''.encode('utf-8') diff --git a/_webp.c b/_webp.c index 10b8b3601..fe4049835 100644 --- a/_webp.c +++ b/_webp.c @@ -10,6 +10,19 @@ #include #include +/* + * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and + * WebPAnimDecoder API's are present (initial support was added in 0.5.0). The + * very early versions added had some significant differences, so we require + * later versions, before enabling animation support. + */ +#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 +#define HAVE_WEBPANIM +#endif +#endif + +#ifdef HAVE_WEBPANIM + /* -------------------------------------------------------------------- */ /* WebP Muxer Error Codes */ /* -------------------------------------------------------------------- */ @@ -624,13 +637,12 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) Py_RETURN_NONE; } - PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) { PyBytesObject* webp_string; const uint8_t* webp; Py_ssize_t size; - PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL; + PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL; WebPDecoderConfig config; VP8StatusCode vp8_status_code = VP8_STATUS_OK; char* mode = "RGB"; @@ -654,7 +666,41 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) mode = "RGBA"; } +#ifndef HAVE_WEBPMUX vp8_status_code = WebPDecode(webp, size, &config); +#else + { + int copy_data = 0; + WebPData data = { webp, size }; + WebPMuxFrameInfo image; + WebPData icc_profile_data = {0}; + WebPData exif_data = {0}; + + WebPMux* mux = WebPMuxCreate(&data, copy_data); + if (NULL == mux) + goto end; + + if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) + { + WebPMuxDelete(mux); + goto end; + } + + webp = image.bitstream.bytes; + size = image.bitstream.size; + + vp8_status_code = WebPDecode(webp, size, &config); + + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) + icc_profile = PyBytes_FromStringAndSize((const char*)icc_profile_data.bytes, icc_profile_data.size); + + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) + exif = PyBytes_FromStringAndSize((const char*)exif_data.bytes, exif_data.size); + + WebPDataClear(&image.bitstream); + WebPMuxDelete(mux); + } +#endif } if (vp8_status_code != VP8_STATUS_OK) @@ -675,14 +721,18 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) #else pymode = PyString_FromString(mode); #endif - ret = Py_BuildValue("SiiS", bytes, config.output.width, - config.output.height, pymode); + ret = Py_BuildValue("SiiSSS", bytes, config.output.width, + config.output.height, pymode, + NULL == icc_profile ? Py_None : icc_profile, + NULL == exif ? Py_None : exif); end: WebPFreeDecBuffer(&config.output); Py_XDECREF(bytes); Py_XDECREF(pymode); + Py_XDECREF(icc_profile); + Py_XDECREF(exif); if (Py_None == ret) Py_RETURN_NONE; @@ -714,7 +764,7 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ static PyMethodDef webpMethods[] = { -#ifdef HAVE_WEBPMUX +#ifdef HAVE_WEBPANIM {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, #endif @@ -733,6 +783,14 @@ void addMuxFlagToModule(PyObject* m) { #endif } +void addAnimFlagToModule(PyObject* m) { +#ifdef HAVE_WEBPANIM + PyModule_AddObject(m, "HAVE_WEBPANIM", Py_True); +#else + PyModule_AddObject(m, "HAVE_WEBPANIM", Py_False); +#endif +} + void addTransparencyFlagToModule(PyObject* m) { PyModule_AddObject(m, "HAVE_TRANSPARENCY", PyBool_FromLong(!WebPDecoderBuggyAlpha())); @@ -740,9 +798,10 @@ void addTransparencyFlagToModule(PyObject* m) { static int setup_module(PyObject* m) { addMuxFlagToModule(m); + addAnimFlagToModule(m); addTransparencyFlagToModule(m); -#ifdef HAVE_WEBPMUX +#ifdef HAVE_WEBPANIM /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8528b24e1..d0b9453db 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -683,8 +683,8 @@ Saving sequences .. note:: Support for animated WebP files will only be enabled if the system webp - library was built with webpmux support. You can check webpmux support - at runtime by inspecting the `_webp.HAVE_WEBPMUX` module flag. + library is v0.5.0 or later. You can check webp animation support at + runtime by inspecting the `_webp.HAVE_WEBPANIM` module flag. When calling :py:meth:`~PIL.Image.Image.save`, the following options are available when the save_all argument is present and true. diff --git a/selftest.py b/selftest.py index 108e57fd2..324d23b45 100755 --- a/selftest.py +++ b/selftest.py @@ -178,8 +178,9 @@ if __name__ == "__main__": ("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)"),