mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 01:16:16 +03:00
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:
parent
786196a77c
commit
b4735f7829
BIN
Images/flower.webp
Normal file
BIN
Images/flower.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
Images/flower2.webp
Normal file
BIN
Images/flower2.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -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
BIN
Tests/images/flower.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
Tests/images/flower2.jpg
Normal file
BIN
Tests/images/flower2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
85
Tests/test_file_webp_metadata.py
Normal file
85
Tests/test_file_webp_metadata.py
Normal 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
92
_webp.c
|
@ -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
|
||||||
|
|
21
setup.py
21
setup.py
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user