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"
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
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)
|
90
_webp.c
90
_webp.c
|
@ -5,6 +5,9 @@
|
|||
#include <webp/decode.h>
|
||||
#include <webp/types.h>
|
||||
|
||||
#ifdef HAVE_WEBPMUX
|
||||
#include <webp/mux.h>
|
||||
#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;
|
||||
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
|
||||
|
|
21
setup.py
21
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:
|
||||
|
|
Loading…
Reference in New Issue
Block a user