Adding support for metadata in webp images.

Pillow now uses the webpmux library to envelop the webp images in RIFF. This allows for easy support of exif and icc_profile metadata.

Also included tests that verify compatibility with jpeg for exif and icc_profile metadata.

If the user does not have webp with webpmux enabled, pillow will fall back to the previous approach, meaning no exif or icc_profile metadata will be read or written to.
This commit is contained in:
Bernardo Heynemann 2013-07-04 17:57:05 -03:00
parent 786196a77c
commit b4735f7829
8 changed files with 207 additions and 12 deletions

BIN
Images/flower.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
Images/flower2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -29,11 +29,19 @@ class WebPImageFile(ImageFile.ImageFile):
format_description = "WebP image" format_description = "WebP image"
def _open(self): 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.size = width, height
self.fp = BytesIO(data) self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] 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): def _save(im, fp, filename):
image_mode = im.mode image_mode = im.mode
@ -41,14 +49,21 @@ def _save(im, fp, filename):
raise IOError("cannot write mode %s as WEBP" % image_mode) raise IOError("cannot write mode %s as WEBP" % image_mode)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "")
data = _webp.WebPEncode( data = _webp.WebPEncode(
im.tobytes(), im.tobytes(),
im.size[0], im.size[0],
im.size[1], im.size[1],
float(quality), 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) fp.write(data)

BIN
Tests/images/flower.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
Tests/images/flower2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -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)

92
_webp.c
View File

@ -5,6 +5,9 @@
#include <webp/decode.h> #include <webp/decode.h>
#include <webp/types.h> #include <webp/types.h>
#ifdef HAVE_WEBPMUX
#include <webp/mux.h>
#endif
PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
{ {
@ -12,12 +15,18 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
int height; int height;
float quality_factor; float quality_factor;
uint8_t *rgb; uint8_t *rgb;
uint8_t *icc_bytes;
uint8_t *exif_bytes;
uint8_t *output; uint8_t *output;
char *mode; char *mode;
Py_ssize_t size; Py_ssize_t size;
Py_ssize_t icc_size;
Py_ssize_t exif_size;
size_t ret_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; Py_RETURN_NONE;
} }
@ -35,9 +44,42 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
Py_RETURN_NONE; 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) { if (ret_size > 0) {
PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size); PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size);
#ifdef HAVE_WEBPMUX
WebPDataClear(&output_data);
#else
free(output); free(output);
#endif
return ret; return ret;
} }
Py_RETURN_NONE; Py_RETURN_NONE;
@ -49,7 +91,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
PyBytesObject *webp_string; PyBytesObject *webp_string;
uint8_t *webp; uint8_t *webp;
Py_ssize_t size; Py_ssize_t size;
PyObject *ret, *bytes, *pymode; PyObject *ret, *bytes, *pymode, *icc_profile = Py_None, *exif = Py_None;
WebPDecoderConfig config; WebPDecoderConfig config;
VP8StatusCode vp8_status_code = VP8_STATUS_OK; VP8StatusCode vp8_status_code = VP8_STATUS_OK;
char* mode = "RGB"; char* mode = "RGB";
@ -72,7 +114,35 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
config.output.colorspace = MODE_RGBA; config.output.colorspace = MODE_RGBA;
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); 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) { if (vp8_status_code != VP8_STATUS_OK) {
@ -95,8 +165,8 @@ 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, icc_profile, exif);
WebPFreeDecBuffer(&config.output); WebPFreeDecBuffer(&config.output);
return ret; return ret;
} }
@ -124,6 +194,14 @@ static PyMethodDef webpMethods[] =
{NULL, NULL} {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 #if PY_VERSION_HEX >= 0x03000000
PyMODINIT_FUNC PyMODINIT_FUNC
@ -139,12 +217,14 @@ PyInit__webp(void) {
}; };
m = PyModule_Create(&module_def); m = PyModule_Create(&module_def);
addMuxFlagToModule(m);
return m; return m;
} }
#else #else
PyMODINIT_FUNC PyMODINIT_FUNC
init_webp(void) init_webp(void)
{ {
Py_InitModule("_webp", webpMethods); PyObject* m = Py_InitModule("_webp", webpMethods);
addMuxFlagToModule(m);
} }
#endif #endif

View File

@ -86,7 +86,7 @@ LCMS_ROOT = None
class pil_build_ext(build_ext): class pil_build_ext(build_ext):
class feature: class feature:
zlib = jpeg = tiff = freetype = tcl = tk = lcms = webp = None zlib = jpeg = tiff = freetype = tcl = tk = lcms = webp = webpmux = None
required = [] required = []
def require(self, feat): 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" if _find_library_file(self, "webp"): # in googles precompiled zip it is call "libwebp"
feature.webp = "webp" 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: for f in feature:
if not getattr(feature, f) and feature.require(f): if not getattr(feature, f) and feature.require(f):
raise ValueError( raise ValueError(
@ -386,8 +392,16 @@ class pil_build_ext(build_ext):
"PIL._imagingcms", ["_imagingcms.c"], libraries=["lcms"] + extra)) "PIL._imagingcms", ["_imagingcms.c"], libraries=["lcms"] + extra))
if os.path.isfile("_webp.c") and feature.webp: 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( exts.append(Extension(
"PIL._webp", ["_webp.c"], libraries=["webp"])) "PIL._webp", ["_webp.c"], libraries=libs, define_macros=defs))
if sys.platform == "darwin": if sys.platform == "darwin":
# locate Tcl/Tk frameworks # locate Tcl/Tk frameworks
@ -452,7 +466,8 @@ class pil_build_ext(build_ext):
(feature.tiff, "TIFF G3/G4 (experimental)"), (feature.tiff, "TIFF G3/G4 (experimental)"),
(feature.freetype, "FREETYPE2"), (feature.freetype, "FREETYPE2"),
(feature.lcms, "LITTLECMS"), (feature.lcms, "LITTLECMS"),
(feature.webp, "WEBP"), ] (feature.webp, "WEBP"),
(feature.webpmux, "WEBPMUX"), ]
all = 1 all = 1
for option in options: for option in options: