diff --git a/Images/flower.webp b/Images/flower.webp new file mode 100644 index 000000000..65d9d52e9 Binary files /dev/null and b/Images/flower.webp differ diff --git a/Images/flower2.webp b/Images/flower2.webp new file mode 100644 index 000000000..0302ea476 Binary files /dev/null and b/Images/flower2.webp differ diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index 7720526f9..d5d75391c 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -29,11 +29,19 @@ class WebPImageFile(ImageFile.ImageFile): format_description = "WebP image" def _open(self): - data, width, height, self.mode = _webp.WebPDecode(self.fp.read()) + data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode(self.fp.read()) + + self.info["icc_profile"] = icc_profile + self.info["exif"] = exif + self.size = width, height self.fp = BytesIO(data) self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] + def _getexif(self): + from PIL.JpegImagePlugin import JpegImageFile + return JpegImageFile._getexif.im_func(self) + def _save(im, fp, filename): image_mode = im.mode @@ -41,14 +49,21 @@ def _save(im, fp, filename): raise IOError("cannot write mode %s as WEBP" % image_mode) quality = im.encoderinfo.get("quality", 80) + icc_profile = im.encoderinfo.get("icc_profile", "") + exif = im.encoderinfo.get("exif", "") data = _webp.WebPEncode( im.tobytes(), im.size[0], im.size[1], float(quality), - im.mode - ) + im.mode, + icc_profile, + exif + ) + if data is None: + raise IOError("cannot write file as WEBP (encoder returned None)") + fp.write(data) diff --git a/Tests/images/flower.jpg b/Tests/images/flower.jpg new file mode 100644 index 000000000..933719d1c Binary files /dev/null and b/Tests/images/flower.jpg differ diff --git a/Tests/images/flower2.jpg b/Tests/images/flower2.jpg new file mode 100644 index 000000000..e94b2f065 Binary files /dev/null and b/Tests/images/flower2.jpg differ diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py new file mode 100644 index 000000000..a9c566cf4 --- /dev/null +++ b/Tests/test_file_webp_metadata.py @@ -0,0 +1,85 @@ +from tester import * + +from PIL import Image + +try: + from PIL import _webp + if not _webp.HAVE_WEBPMUX: + skip('webpmux support not installed') +except: + skip('webp support not installed') + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + + +def test_read_exif_metadata(): + + file_path = "Images/flower.webp" + image = Image.open(file_path) + + assert_equal(image.format, "WEBP") + exif_data = image.info.get("exif", None) + assert_true(exif_data) + + exif = image._getexif() + + #camera make + assert_equal(exif[271], "Canon") + + jpeg_image = Image.open('Tests/images/flower.jpg') + expected_exif = jpeg_image.info['exif'] + + assert_equal(exif_data, expected_exif) + + +def test_write_exif_metadata(): + file_path = "Tests/images/flower.jpg" + image = Image.open(file_path) + expected_exif = image.info['exif'] + + buffer = StringIO() + + image.save(buffer, "webp", exif=expected_exif) + + buffer.seek(0) + webp_image = Image.open(buffer) + + webp_exif = webp_image.info.get('exif', None) + assert_true(webp_exif) + assert_equal(webp_exif, expected_exif) + + +def test_read_icc_profile(): + + file_path = "Images/flower2.webp" + image = Image.open(file_path) + + assert_equal(image.format, "WEBP") + assert_true(image.info.get("icc_profile", None)) + + icc = image.info['icc_profile'] + + jpeg_image = Image.open('Tests/images/flower2.jpg') + expected_icc = jpeg_image.info['icc_profile'] + + assert_equal(icc, expected_icc) + + +def test_write_icc_metadata(): + file_path = "Tests/images/flower2.jpg" + image = Image.open(file_path) + expected_icc_profile = image.info['icc_profile'] + + buffer = StringIO() + + image.save(buffer, "webp", icc_profile=expected_icc_profile) + + buffer.seek(0) + webp_image = Image.open(buffer) + + webp_icc_profile = webp_image.info.get('icc_profile', None) + assert_true(webp_icc_profile) + assert_equal(webp_icc_profile, expected_icc_profile) diff --git a/_webp.c b/_webp.c index a77f203c4..877865406 100644 --- a/_webp.c +++ b/_webp.c @@ -5,6 +5,9 @@ #include #include +#ifdef HAVE_WEBPMUX +#include +#endif PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) { @@ -12,12 +15,18 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) int height; float quality_factor; uint8_t *rgb; + uint8_t *icc_bytes; + uint8_t *exif_bytes; uint8_t *output; - char *mode; + char *mode; Py_ssize_t size; + Py_ssize_t icc_size; + Py_ssize_t exif_size; size_t ret_size; - if (!PyArg_ParseTuple(args, "s#nifs",(char**)&rgb, &size, &width, &height, &quality_factor, &mode)) { + if (!PyArg_ParseTuple(args, "s#nifss#s#", + (char**)&rgb, &size, &width, &height, &quality_factor, &mode, + &icc_bytes, &icc_size, &exif_bytes, &exif_size)) { Py_RETURN_NONE; } @@ -35,9 +44,42 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) Py_RETURN_NONE; } +#ifdef HAVE_WEBPMUX + WebPData output_data = {0}; + WebPData image = { output, ret_size }; + + int copy_data = 0; // value 1 indicates given data WILL be copied to the mux + // and value 0 indicates data will NOT be copied. + + WebPMux* mux = WebPMuxNew(); + WebPMuxSetImage(mux, &image, copy_data); + + if (icc_size > 0) { + WebPData icc_profile = { icc_bytes, icc_size }; + WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data); + } + + if (exif_size > 0) { + WebPData exif = { exif_bytes, exif_size }; + WebPMuxSetChunk(mux, "EXIF", &exif, copy_data); + } + + WebPMuxAssemble(mux, &output_data); + WebPMuxDelete(mux); + + output = (uint8_t*)output_data.bytes; + ret_size = output_data.size; +#endif + if (ret_size > 0) { PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size); + +#ifdef HAVE_WEBPMUX + WebPDataClear(&output_data); +#else free(output); +#endif + return ret; } Py_RETURN_NONE; @@ -49,7 +91,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) PyBytesObject *webp_string; uint8_t *webp; Py_ssize_t size; - PyObject *ret, *bytes, *pymode; + PyObject *ret, *bytes, *pymode, *icc_profile = Py_None, *exif = Py_None; WebPDecoderConfig config; VP8StatusCode vp8_status_code = VP8_STATUS_OK; char* mode = "RGB"; @@ -72,7 +114,35 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) config.output.colorspace = MODE_RGBA; mode = "RGBA"; } + +#ifdef HAVE_WEBPMUX + int copy_data = 0; + WebPData data = { webp, size }; + WebPMuxFrameInfo image; + + WebPMux* mux = WebPMuxCreate(&data, copy_data); + WebPMuxGetFrame(mux, 1, &image); + webp = (uint8_t*)image.bitstream.bytes; + size = image.bitstream.size; +#endif + vp8_status_code = WebPDecode(webp, size, &config); + +#ifdef HAVE_WEBPMUX + WebPData icc_profile_data = {0}; + WebPMuxGetChunk(mux, "ICCP", &icc_profile_data); + if (icc_profile_data.size > 0) { + icc_profile = PyBytes_FromStringAndSize((const char*)icc_profile_data.bytes, icc_profile_data.size); + } + + WebPData exif_data = {0}; + WebPMuxGetChunk(mux, "EXIF", &exif_data); + if (exif_data.size > 0) { + exif = PyBytes_FromStringAndSize((const char*)exif_data.bytes, exif_data.size); + } + + WebPMuxDelete(mux); +#endif } if (vp8_status_code != VP8_STATUS_OK) { @@ -95,8 +165,8 @@ 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, icc_profile, exif); WebPFreeDecBuffer(&config.output); return ret; } @@ -124,6 +194,14 @@ static PyMethodDef webpMethods[] = {NULL, NULL} }; +void addMuxFlagToModule(PyObject* m) { +#ifdef HAVE_WEBPMUX + PyModule_AddObject(m, "HAVE_WEBPMUX", Py_True); +#else + PyModule_AddObject(m, "HAVE_WEBPMUX", Py_False); +#endif +} + #if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC @@ -139,12 +217,14 @@ PyInit__webp(void) { }; m = PyModule_Create(&module_def); + addMuxFlagToModule(m); return m; } #else PyMODINIT_FUNC init_webp(void) { - Py_InitModule("_webp", webpMethods); + PyObject* m = Py_InitModule("_webp", webpMethods); + addMuxFlagToModule(m); } #endif diff --git a/setup.py b/setup.py index 67b859b13..4acef5767 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ LCMS_ROOT = None class pil_build_ext(build_ext): class feature: - zlib = jpeg = tiff = freetype = tcl = tk = lcms = webp = None + zlib = jpeg = tiff = freetype = tcl = tk = lcms = webp = webpmux = None required = [] def require(self, feat): @@ -329,6 +329,12 @@ class pil_build_ext(build_ext): if _find_library_file(self, "webp"): # in googles precompiled zip it is call "libwebp" feature.webp = "webp" + if feature.want('webpmux'): + if (_find_include_file(self, "webp/mux.h") and + _find_include_file(self, "webp/demux.h")): + if _find_library_file(self, "webpmux") and _find_library_file(self, "webpdemux"): + feature.webpmux = "webpmux" + for f in feature: if not getattr(feature, f) and feature.require(f): raise ValueError( @@ -386,8 +392,16 @@ class pil_build_ext(build_ext): "PIL._imagingcms", ["_imagingcms.c"], libraries=["lcms"] + extra)) if os.path.isfile("_webp.c") and feature.webp: + libs = ["webp"] + defs = [] + + if feature.webpmux: + defs.append(("HAVE_WEBPMUX", None)) + libs.append("webpmux") + libs.append("webpdemux") + exts.append(Extension( - "PIL._webp", ["_webp.c"], libraries=["webp"])) + "PIL._webp", ["_webp.c"], libraries=libs, define_macros=defs)) if sys.platform == "darwin": # locate Tcl/Tk frameworks @@ -452,7 +466,8 @@ class pil_build_ext(build_ext): (feature.tiff, "TIFF G3/G4 (experimental)"), (feature.freetype, "FREETYPE2"), (feature.lcms, "LITTLECMS"), - (feature.webp, "WEBP"), ] + (feature.webp, "WEBP"), + (feature.webpmux, "WEBPMUX"), ] all = 1 for option in options: