diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index 4057448e0..97a910930 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -28,24 +28,134 @@ 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_WEBPMUX: + # Legacy mode + 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, self.mode)] + 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): + if not _webp.HAVE_WEBPMUX: + return 1 + return self._n_frames + + @property + def is_animated(self): + if not _webp.HAVE_WEBPMUX: + return False + return self._n_frames > 1 + + def seek(self, 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 + timestamp -= duration # libwebp gives frame end, adjust to start of frame + return data, timestamp, duration + + def _seek(self, frame): + if self.__physical_frame == frame: + return # Nothing to do + + if frame < self.__physical_frame: + # Rewind to beginning + self._reset() + + # Advance to the requested frame + while self.__physical_frame < frame: + self._get_next() + + def load(self): + 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): + return self.__logical_frame def _save_all(im, fp, filename): + if not _webp.HAVE_WEBPMUX: + _save(im, fp, filename) + return + encoderinfo = im.encoderinfo.copy() append_images = encoderinfo.get("append_images", []) background = encoderinfo.get("background", (0, 0, 0, 0)) @@ -105,7 +215,10 @@ def _save_all(im, fp, filename): ims.load() # Make sure image mode is supported - frame = ims if ims.mode in _VALID_WEBP_MODES else ims.convert("RGBA") + frame = ims + if not ims.mode in _VALID_WEBP_MODES: + alpha = 'A' in ims.im.getpalettemode() + frame = image.convert('RGBA' if alpha else 'RGB') # Append the frame to the animation encoder enc.add( @@ -139,11 +252,15 @@ def _save_all(im, fp, filename): fp.write(data) - def _save(im, fp, filename): + if _webp.HAVE_WEBPMUX: + _save_all(im, fp, filename) + return + image_mode = im.mode if im.mode not in _VALID_WEBP_MODES: - im = im.convert("RGBA") + alpha = 'A' in im.im.getpalettemode() + im = im.convert('RGBA' if alpha else 'RGB') lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) diff --git a/_webp.c b/_webp.c index 4df63c883..8a54c6935 100644 --- a/_webp.c +++ b/_webp.c @@ -8,13 +8,13 @@ #ifdef HAVE_WEBPMUX #include -#endif +#include /* -------------------------------------------------------------------- */ /* WebP Animation Support */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPMUX +// Encoder type typedef struct { PyObject_HEAD WebPAnimEncoder* enc; @@ -23,6 +23,17 @@ typedef struct { static PyTypeObject WebPAnimEncoder_Type; +// Decoder type +typedef struct { + PyObject_HEAD + WebPAnimDecoder* dec; + WebPAnimInfo info; + WebPData data; +} WebPAnimDecoderObject; + +static PyTypeObject WebPAnimDecoder_Type; + +// Encoder functions PyObject* _anim_encoder_new(PyObject* self, PyObject* args) { int width, height; @@ -177,10 +188,207 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args) return ret; } -#endif +// Decoder functions +PyObject* _anim_decoder_new(PyObject* self, PyObject* args) +{ + PyBytesObject *webp_string; + const uint8_t *webp; + Py_ssize_t size; + + if (!PyArg_ParseTuple(args, "S", &webp_string)) { + return NULL; + } + PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&webp, &size); + WebPData webp_src = {webp, size}; + + // Create the decoder (default mode is RGBA, if no options passed) + WebPAnimDecoderObject* decp; + decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type); + if (decp) { + if (WebPDataCopy(&webp_src, &(decp->data))) { + WebPAnimDecoder* dec = WebPAnimDecoderNew(&(decp->data), NULL); + if (dec) { + if (WebPAnimDecoderGetInfo(dec, &(decp->info))) { + decp->dec = dec; + return (PyObject*) decp; + } + } + } + PyObject_Del(decp); + } + fprintf(stderr, "Error! Could not create decoder object.\n"); + Py_RETURN_NONE; +} + +PyObject* _anim_decoder_dealloc(PyObject* self) +{ + WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; + WebPDataClear(&(decp->data)); + WebPAnimDecoderDelete(decp->dec); + Py_RETURN_NONE; +} + +PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args) +{ + WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; + WebPAnimInfo* info = &(decp->info); + return Py_BuildValue("IIIIIs", + info->canvas_width, info->canvas_height, + info->loop_count, + info->bgcolor, + info->frame_count, + "RGBA" // WebPAnimDecoder defaults to RGBA if no mode is specified + ); +} + +PyObject* _anim_decoder_get_chunk(PyObject* self, PyObject* args) +{ + char *mode; + PyObject *ret; + WebPChunkIterator iter; + + if (!PyArg_ParseTuple(args, "s", &mode)) { + return NULL; + } + + WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; + const WebPDemuxer* demux = WebPAnimDecoderGetDemuxer(decp->dec); + if (!WebPDemuxGetChunk(demux, mode, 1, &iter)) { + Py_RETURN_NONE; + } + + ret = PyBytes_FromStringAndSize((const char*)iter.chunk.bytes, iter.chunk.size); + WebPDemuxReleaseChunkIterator(&iter); + + return ret; +} + +PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args) +{ + uint8_t* buf; + int timestamp; + PyObject *bytes; + + WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; + if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { + fprintf(stderr, "Error! Failed to read next frame.\n"); + Py_RETURN_NONE; + } + + bytes = PyBytes_FromStringAndSize((char *)buf, + decp->info.canvas_width * 4 * decp->info.canvas_height); + return Py_BuildValue("Si", bytes, timestamp); +} + +PyObject* _anim_decoder_has_more_frames(PyObject* self, PyObject* args) +{ + WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; + return Py_BuildValue("i", WebPAnimDecoderHasMoreFrames(decp->dec)); +} + +PyObject* _anim_decoder_reset(PyObject* self, PyObject* args) +{ + WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; + WebPAnimDecoderReset(decp->dec); + Py_RETURN_NONE; +} /* -------------------------------------------------------------------- */ -/* WebP Single-Frame Support */ +/* Type Definitions */ +/* -------------------------------------------------------------------- */ + +// WebPAnimEncoder methods +static struct PyMethodDef _anim_encoder_methods[] = { + {"add", (PyCFunction)_anim_encoder_add, METH_VARARGS, "add"}, + {"assemble", (PyCFunction)_anim_encoder_assemble, METH_VARARGS, "assemble"}, + {NULL, NULL} /* sentinel */ +}; + +// WebPAnimDecoder type definition +static PyTypeObject WebPAnimEncoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "WebPAnimEncoder", /*tp_name */ + sizeof(WebPAnimEncoderObject), /*tp_size */ + 0, /*tp_itemsize */ + /* methods */ + (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _anim_encoder_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ +}; + +// WebPAnimDecoder methods +static struct PyMethodDef _anim_decoder_methods[] = { + {"get_info", (PyCFunction)_anim_decoder_get_info, METH_VARARGS, "get_info"}, + {"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"}, + {"get_next", (PyCFunction)_anim_decoder_get_next, METH_VARARGS, "get_next"}, + {"has_more_frames", (PyCFunction)_anim_decoder_has_more_frames, METH_VARARGS, "has_more_frames"}, + {"reset", (PyCFunction)_anim_decoder_reset, METH_VARARGS, "reset"}, + {NULL, NULL} /* sentinel */ +}; + +// WebPAnimDecoder type definition +static PyTypeObject WebPAnimDecoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "WebPAnimDecoder", /*tp_name */ + sizeof(WebPAnimDecoderObject), /*tp_size */ + 0, /*tp_itemsize */ + /* methods */ + (destructor)_anim_decoder_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _anim_decoder_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ +}; + +#endif + +#ifdef HAVE_WEBPMUX +/* -------------------------------------------------------------------- */ +/* Legacy WebP Support */ /* -------------------------------------------------------------------- */ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) @@ -410,6 +618,8 @@ end: return ret; } +#endif + // 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){ @@ -428,54 +638,6 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ return Py_BuildValue("i", WebPDecoderBuggyAlpha()); } -/* -------------------------------------------------------------------- */ -/* WebPAnimEncoder Type */ -/* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPMUX - -static struct PyMethodDef _anim_encoder_methods[] = { - {"add", (PyCFunction)_anim_encoder_add, 1}, - {"assemble", (PyCFunction)_anim_encoder_assemble, 1}, - {NULL, NULL} /* sentinel */ -}; - -/* type description */ -static PyTypeObject WebPAnimEncoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "WebPAnimEncoder", /*tp_name */ - sizeof(WebPAnimEncoderObject), /*tp_size */ - 0, /*tp_itemsize */ - /* methods */ - (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _anim_encoder_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ -}; - -#endif - /* -------------------------------------------------------------------- */ /* Module Setup */ /* -------------------------------------------------------------------- */ @@ -483,10 +645,12 @@ static PyTypeObject WebPAnimEncoder_Type = { static PyMethodDef webpMethods[] = { #ifdef HAVE_WEBPMUX + {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, -#endif +#else {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, +#endif {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"}, {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"}, {NULL, NULL} @@ -511,7 +675,8 @@ static int setup_module(PyObject* m) { #ifdef HAVE_WEBPMUX /* Ready object types */ - if (PyType_Ready(&WebPAnimEncoder_Type) < 0) + if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || + PyType_Ready(&WebPAnimEncoder_Type) < 0) return -1; #endif return 0;