- 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
This commit is contained in:
Jason Douglas 2017-09-27 19:04:24 -07:00
parent e534991409
commit c18d26b04b
8 changed files with 96 additions and 24 deletions

View File

@ -33,7 +33,7 @@ class WebPImageFile(ImageFile.ImageFile):
format_description = "WebP image" format_description = "WebP image"
def _open(self): def _open(self):
if not _webp.HAVE_WEBPMUX: if not _webp.HAVE_WEBPANIM:
# Legacy mode # Legacy mode
data, width, height, self.mode = _webp.WebPDecode(self.fp.read()) data, width, height, self.mode = _webp.WebPDecode(self.fp.read())
self.size = width, height self.size = width, height
@ -80,18 +80,18 @@ class WebPImageFile(ImageFile.ImageFile):
@property @property
def n_frames(self): def n_frames(self):
if not _webp.HAVE_WEBPMUX: if not _webp.HAVE_WEBPANIM:
return 1 return 1
return self._n_frames return self._n_frames
@property @property
def is_animated(self): def is_animated(self):
if not _webp.HAVE_WEBPMUX: if not _webp.HAVE_WEBPANIM:
return False return False
return self._n_frames > 1 return self._n_frames > 1
def seek(self, frame): def seek(self, frame):
if not _webp.HAVE_WEBPMUX: if not _webp.HAVE_WEBPANIM:
return super(WebPImageFile, self).seek(frame) return super(WebPImageFile, self).seek(frame)
# Perform some simple checks first # Perform some simple checks first
@ -141,7 +141,7 @@ class WebPImageFile(ImageFile.ImageFile):
self._get_next() self._get_next()
def load(self): def load(self):
if _webp.HAVE_WEBPMUX: if _webp.HAVE_WEBPANIM:
if self.__loaded != self.__logical_frame: if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame) self._seek(self.__logical_frame)
@ -158,7 +158,7 @@ class WebPImageFile(ImageFile.ImageFile):
return super(WebPImageFile, self).load() return super(WebPImageFile, self).load()
def tell(self): def tell(self):
if not _webp.HAVE_WEBPMUX: if not _webp.HAVE_WEBPANIM:
return super(WebPImageFile, self).tell() return super(WebPImageFile, self).tell()
return self.__logical_frame return self.__logical_frame
@ -301,7 +301,7 @@ def _save(im, fp, filename):
Image.register_open(WebPImageFile.format, WebPImageFile, _accept) Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
Image.register_save(WebPImageFile.format, _save) Image.register_save(WebPImageFile.format, _save)
if _webp.HAVE_WEBPMUX: if _webp.HAVE_WEBPANIM:
Image.register_save_all(WebPImageFile.format, _save_all) Image.register_save_all(WebPImageFile.format, _save_all)
Image.register_extension(WebPImageFile.format, ".webp") Image.register_extension(WebPImageFile.format, ".webp")
Image.register_mime(WebPImageFile.format, "image/webp") Image.register_mime(WebPImageFile.format, "image/webp")

View File

@ -43,6 +43,7 @@ def get_supported_codecs():
return [f for f in codecs if check_codec(f)] return [f for f in codecs if check_codec(f)]
features = { features = {
"webp_anim": ("PIL._webp", 'HAVE_WEBPANIM'),
"webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
"raqm": ("PIL._imagingft", "HAVE_RAQM") "raqm": ("PIL._imagingft", "HAVE_RAQM")

View File

@ -35,6 +35,11 @@ class TestFeatures(PillowTestCase):
self.assertEqual(features.check('webp_mux'), self.assertEqual(features.check('webp_mux'),
_webp.HAVE_WEBPMUX) _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): def test_check_modules(self):
for feature in features.modules: for feature in features.modules:
self.assertIn(features.check_module(feature), [True, False]) self.assertIn(features.check_module(feature), [True, False])

View File

@ -17,8 +17,8 @@ class TestFileWebpAnimation(PillowTestCase):
except ImportError: except ImportError:
self.skipTest('WebP support not installed') self.skipTest('WebP support not installed')
if not _webp.HAVE_WEBPMUX: if not _webp.HAVE_WEBPANIM:
self.skipTest("WebP not compiled with mux support, " self.skipTest("WebP library does not contain animation support, "
"not testing animation") "not testing animation")
def test_n_frames(self): def test_n_frames(self):

View File

@ -2,17 +2,22 @@ from helper import unittest, PillowTestCase
from PIL import Image 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): class TestFileWebpMetadata(PillowTestCase):
def setUp(self): def setUp(self):
try: if not HAVE_WEBP:
from PIL import _webp
except ImportError:
self.skipTest('WebP support not installed') self.skipTest('WebP support not installed')
return return
if not _webp.HAVE_WEBPMUX: if not HAVE_WEBPMUX:
self.skipTest('WebPMux support not installed') self.skipTest('WebPMux support not installed')
def test_read_exif_metadata(self): def test_read_exif_metadata(self):
@ -107,6 +112,7 @@ class TestFileWebpMetadata(PillowTestCase):
self.assertFalse(webp_image._getexif()) self.assertFalse(webp_image._getexif())
@unittest.skipUnless(HAVE_WEBPANIM, 'WebP animation support not available')
def test_write_animated_metadata(self): def test_write_animated_metadata(self):
iccp_data = '<iccp_data>'.encode('utf-8') iccp_data = '<iccp_data>'.encode('utf-8')
exif_data = '<exif_data>'.encode('utf-8') exif_data = '<exif_data>'.encode('utf-8')

71
_webp.c
View File

@ -10,6 +10,19 @@
#include <webp/mux.h> #include <webp/mux.h>
#include <webp/demux.h> #include <webp/demux.h>
/*
* 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 */ /* WebP Muxer Error Codes */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
@ -624,13 +637,12 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
{ {
PyBytesObject* webp_string; PyBytesObject* webp_string;
const uint8_t* webp; const uint8_t* webp;
Py_ssize_t size; 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; WebPDecoderConfig config;
VP8StatusCode vp8_status_code = VP8_STATUS_OK; VP8StatusCode vp8_status_code = VP8_STATUS_OK;
char* mode = "RGB"; char* mode = "RGB";
@ -654,7 +666,41 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
mode = "RGBA"; mode = "RGBA";
} }
#ifndef HAVE_WEBPMUX
vp8_status_code = WebPDecode(webp, size, &config); 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) if (vp8_status_code != VP8_STATUS_OK)
@ -675,14 +721,18 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
#else #else
pymode = PyString_FromString(mode); pymode = PyString_FromString(mode);
#endif #endif
ret = Py_BuildValue("SiiS", bytes, config.output.width, ret = Py_BuildValue("SiiSSS", bytes, config.output.width,
config.output.height, pymode); config.output.height, pymode,
NULL == icc_profile ? Py_None : icc_profile,
NULL == exif ? Py_None : exif);
end: end:
WebPFreeDecBuffer(&config.output); WebPFreeDecBuffer(&config.output);
Py_XDECREF(bytes); Py_XDECREF(bytes);
Py_XDECREF(pymode); Py_XDECREF(pymode);
Py_XDECREF(icc_profile);
Py_XDECREF(exif);
if (Py_None == ret) if (Py_None == ret)
Py_RETURN_NONE; Py_RETURN_NONE;
@ -714,7 +764,7 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){
static PyMethodDef webpMethods[] = static PyMethodDef webpMethods[] =
{ {
#ifdef HAVE_WEBPMUX #ifdef HAVE_WEBPANIM
{"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"},
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
#endif #endif
@ -733,6 +783,14 @@ void addMuxFlagToModule(PyObject* m) {
#endif #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) { void addTransparencyFlagToModule(PyObject* m) {
PyModule_AddObject(m, "HAVE_TRANSPARENCY", PyModule_AddObject(m, "HAVE_TRANSPARENCY",
PyBool_FromLong(!WebPDecoderBuggyAlpha())); PyBool_FromLong(!WebPDecoderBuggyAlpha()));
@ -740,9 +798,10 @@ void addTransparencyFlagToModule(PyObject* m) {
static int setup_module(PyObject* m) { static int setup_module(PyObject* m) {
addMuxFlagToModule(m); addMuxFlagToModule(m);
addAnimFlagToModule(m);
addTransparencyFlagToModule(m); addTransparencyFlagToModule(m);
#ifdef HAVE_WEBPMUX #ifdef HAVE_WEBPANIM
/* Ready object types */ /* Ready object types */
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||
PyType_Ready(&WebPAnimEncoder_Type) < 0) PyType_Ready(&WebPAnimEncoder_Type) < 0)

View File

@ -683,8 +683,8 @@ Saving sequences
.. note:: .. note::
Support for animated WebP files will only be enabled if the system webp Support for animated WebP files will only be enabled if the system webp
library was built with webpmux support. You can check webpmux support library is v0.5.0 or later. You can check webp animation support at
at runtime by inspecting the `_webp.HAVE_WEBPMUX` module flag. runtime by inspecting the `_webp.HAVE_WEBPANIM` module flag.
When calling :py:meth:`~PIL.Image.Image.save`, the following options When calling :py:meth:`~PIL.Image.Image.save`, the following options
are available when the save_all argument is present and true. are available when the save_all argument is present and true.

View File

@ -178,8 +178,9 @@ if __name__ == "__main__":
("freetype2", "FREETYPE2"), ("freetype2", "FREETYPE2"),
("littlecms2", "LITTLECMS2"), ("littlecms2", "LITTLECMS2"),
("webp", "WEBP"), ("webp", "WEBP"),
("transp_webp", "Transparent WEBP"), ("transp_webp", "WEBP Transparency"),
("webp_mux", "WEBPMUX"), ("webp_mux", "WEBPMUX"),
("webp_anim", "WEBP Animation"),
("jpg", "JPEG"), ("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"), ("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"), ("zlib", "ZLIB (PNG/ZIP)"),