- Support non-alpha modes with WebPAnimDecoder

- Support writing metadata chunks with WebPAnimEncoder
- Add XMP metadata support to legacy WebPEncode wrapper
- Cleanup unused mux code in legacy WebPDecode wrapper
- Fix some bugs present when compiled without WebP Mux support
- Fix conversion from L/P/PA modes when saving WebP files
- Update existing tests, and add new ones for WebP animation and metadata support
This commit is contained in:
Jason Douglas 2017-09-26 20:27:40 -07:00
parent 482d803717
commit cd12a48fe0
10 changed files with 400 additions and 89 deletions

View File

@ -86,6 +86,9 @@ class WebPImageFile(ImageFile.ImageFile):
return self._n_frames > 1 return self._n_frames > 1
def seek(self, frame): def seek(self, frame):
if not _webp.HAVE_WEBPMUX:
return super(WebPImageFile, self).seek(frame)
# Perform some simple checks first # Perform some simple checks first
if frame >= self._n_frames: if frame >= self._n_frames:
raise EOFError("attempted to seek beyond end of sequence") raise EOFError("attempted to seek beyond end of sequence")
@ -133,6 +136,7 @@ class WebPImageFile(ImageFile.ImageFile):
self._get_next() self._get_next()
def load(self): def load(self):
if _webp.HAVE_WEBPMUX:
if self.__loaded != self.__logical_frame: if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame) self._seek(self.__logical_frame)
@ -149,15 +153,24 @@ class WebPImageFile(ImageFile.ImageFile):
return super(WebPImageFile, self).load() return super(WebPImageFile, self).load()
def tell(self): def tell(self):
if not _webp.HAVE_WEBPMUX:
return super(WebPImageFile, self).tell()
return self.__logical_frame return self.__logical_frame
def _save_all(im, fp, filename): def _save_all(im, fp, filename):
if not _webp.HAVE_WEBPMUX: encoderinfo = im.encoderinfo.copy()
append_images = encoderinfo.get("append_images", [])
# If total frame count is 1, then save using the legacy API, which
# will preserve non-alpha modes
total = 0
for ims in [im]+append_images:
total += 1 if not hasattr(ims, "n_frames") else ims.n_frames
if total == 1:
_save(im, fp, filename) _save(im, fp, filename)
return return
encoderinfo = im.encoderinfo.copy()
append_images = encoderinfo.get("append_images", [])
background = encoderinfo.get("background", (0, 0, 0, 0)) background = encoderinfo.get("background", (0, 0, 0, 0))
duration = im.encoderinfo.get("duration", 0) duration = im.encoderinfo.get("duration", 0)
loop = im.encoderinfo.get("loop", 0) loop = im.encoderinfo.get("loop", 0)
@ -171,6 +184,7 @@ def _save_all(im, fp, filename):
method = im.encoderinfo.get("method", 0) method = im.encoderinfo.get("method", 0)
icc_profile = im.encoderinfo.get("icc_profile", "") icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "") exif = im.encoderinfo.get("exif", "")
xmp = im.encoderinfo.get("xmp", "")
if allow_mixed: if allow_mixed:
lossless = False lossless = False
@ -217,8 +231,8 @@ def _save_all(im, fp, filename):
# Make sure image mode is supported # Make sure image mode is supported
frame = ims frame = ims
if not ims.mode in _VALID_WEBP_MODES: if not ims.mode in _VALID_WEBP_MODES:
alpha = 'A' in ims.im.getpalettemode() alpha = ims.mode == 'P' and 'A' in ims.im.getpalettemode()
frame = image.convert('RGBA' if alpha else 'RGB') frame = ims.convert('RGBA' if alpha else 'RGB')
# Append the frame to the animation encoder # Append the frame to the animation encoder
enc.add( enc.add(
@ -246,26 +260,22 @@ def _save_all(im, fp, filename):
) )
# Get the final output from the encoder # Get the final output from the encoder
data = enc.assemble(icc_profile, exif) data = enc.assemble(icc_profile, exif, xmp)
if data is None: if data is None:
raise IOError("cannot write file as WEBP (encoder returned None)") raise IOError("cannot write file as WEBP (encoder returned None)")
fp.write(data) fp.write(data)
def _save(im, fp, filename): 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:
alpha = 'A' in im.im.getpalettemode()
im = im.convert('RGBA' if alpha else 'RGB')
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile", "") icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "") exif = im.encoderinfo.get("exif", "")
xmp = im.encoderinfo.get("xmp", "")
if im.mode not in _VALID_WEBP_MODES:
alpha = im.mode == 'P' and 'A' in im.im.getpalettemode()
im = im.convert('RGBA' if alpha else 'RGB')
data = _webp.WebPEncode( data = _webp.WebPEncode(
im.tobytes(), im.tobytes(),
@ -275,7 +285,8 @@ def _save(im, fp, filename):
float(quality), float(quality),
im.mode, im.mode,
icc_profile, icc_profile,
exif exif,
xmp
) )
if data is None: if data is None:
raise IOError("cannot write file as WEBP (encoder returned None)") raise IOError("cannot write file as WEBP (encoder returned None)")
@ -285,6 +296,7 @@ def _save(im, fp, filename):
Image.register_open(WebPImageFile.format, WebPImageFile, _accept) Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
Image.register_save(WebPImageFile.format, _save) Image.register_save(WebPImageFile.format, _save)
Image.register_save_all(WebPImageFile.format, _save_all) if _webp.HAVE_WEBPMUX:
Image.register_save_all(WebPImageFile.format, _save_all)
Image.register_extension(WebPImageFile.format, ".webp") Image.register_extension(WebPImageFile.format, ".webp")
Image.register_mime(WebPImageFile.format, "image/webp") Image.register_mime(WebPImageFile.format, "image/webp")

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

BIN
Tests/images/iss634.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -22,6 +22,10 @@ class TestFileWebp(PillowTestCase):
_webp.WebPDecoderBuggyAlpha() _webp.WebPDecoderBuggyAlpha()
def test_read_rgb(self): def test_read_rgb(self):
"""
Can we read a RGB mode webp file without error.
Does it have the bits we expect?
"""
file_path = "Tests/images/hopper.webp" file_path = "Tests/images/hopper.webp"
image = Image.open(file_path) image = Image.open(file_path)
@ -46,9 +50,7 @@ class TestFileWebp(PillowTestCase):
temp_file = self.tempfile("temp.webp") temp_file = self.tempfile("temp.webp")
hopper("RGB").save(temp_file) hopper("RGB").save(temp_file)
image = Image.open(temp_file) image = Image.open(temp_file)
image.load()
self.assertEqual(image.mode, "RGB") self.assertEqual(image.mode, "RGB")
self.assertEqual(image.size, (128, 128)) self.assertEqual(image.size, (128, 128))
@ -70,19 +72,66 @@ class TestFileWebp(PillowTestCase):
# the image. The old lena images for WebP are showing ~16 on # the image. The old lena images for WebP are showing ~16 on
# Ubuntu, the jpegs are showing ~18. # Ubuntu, the jpegs are showing ~18.
target = hopper("RGB") target = hopper("RGB")
self.assert_image_similar(image, target, 12) self.assert_image_similar(image, target, 12.0)
def test_write_unsupported_mode_L(self):
"""
Saving a black-and-white file to webp format should work, and be
similar to the original file.
"""
def test_write_unsupported_mode(self):
temp_file = self.tempfile("temp.webp") temp_file = self.tempfile("temp.webp")
hopper("L").save(temp_file)
image = Image.open(temp_file)
im = hopper("L") self.assertEqual(image.mode, "RGB")
self.assertRaises(IOError, im.save, temp_file) self.assertEqual(image.size, (128, 128))
self.assertEqual(image.format, "WEBP")
image.load()
image.getdata()
target = hopper("L").convert("RGB")
self.assert_image_similar(image, target, 10.0)
def test_write_unsupported_mode_P(self):
"""
Saving a palette-based file to webp format should work, and be
similar to the original file.
"""
temp_file = self.tempfile("temp.webp")
hopper("P").save(temp_file)
image = Image.open(temp_file)
self.assertEqual(image.mode, "RGB")
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.format, "WEBP")
image.load()
image.getdata()
target = hopper("P").convert("RGB")
self.assert_image_similar(image, target, 50.0)
def test_WebPEncode_with_invalid_args(self): def test_WebPEncode_with_invalid_args(self):
"""
Calling encoder functions with no arguments should result in an error.
"""
if _webp.HAVE_WEBPMUX:
self.assertRaises(TypeError, _webp.WebPAnimEncoder)
self.assertRaises(TypeError, _webp.WebPEncode) self.assertRaises(TypeError, _webp.WebPEncode)
def test_WebPDecode_with_invalid_args(self): def test_WebPDecode_with_invalid_args(self):
"""
Calling decoder functions with no arguments should result in an error.
"""
if _webp.HAVE_WEBPMUX:
self.assertRaises(TypeError, _webp.WebPAnimDecoder)
self.assertRaises(TypeError, _webp.WebPDecode) self.assertRaises(TypeError, _webp.WebPDecode)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -22,6 +22,11 @@ class TestFileWebpAlpha(PillowTestCase):
"not testing transparency") "not testing transparency")
def test_read_rgba(self): def test_read_rgba(self):
"""
Can we read a RGBA mode file without error.
Does it have the bits we expect?
"""
# Generated with `cwebp transparent.png -o transparent.webp` # Generated with `cwebp transparent.png -o transparent.webp`
file_path = "Tests/images/transparent.webp" file_path = "Tests/images/transparent.webp"
image = Image.open(file_path) image = Image.open(file_path)
@ -38,6 +43,11 @@ class TestFileWebpAlpha(PillowTestCase):
self.assert_image_similar(image, target, 20.0) self.assert_image_similar(image, target, 20.0)
def test_write_lossless_rgb(self): def test_write_lossless_rgb(self):
"""
Can we write a RGBA mode file with lossless compression without error.
Does it have the bits we expect?
"""
temp_file = self.tempfile("temp.webp") temp_file = self.tempfile("temp.webp")
# temp_file = "temp.webp" # temp_file = "temp.webp"
@ -90,6 +100,27 @@ class TestFileWebpAlpha(PillowTestCase):
else: else:
self.assert_image_similar(image, pil_image, 1.0) self.assert_image_similar(image, pil_image, 1.0)
def test_write_unsupported_mode_PA(self):
"""
Saving a palette-based file with transparency to webp format
should work, and be similar to the original file.
"""
temp_file = self.tempfile("temp.webp")
file_path = "Tests/images/transparent.gif"
Image.open(file_path).save(temp_file)
image = Image.open(temp_file)
self.assertEqual(image.mode, "RGBA")
self.assertEqual(image.size, (200, 150))
self.assertEqual(image.format, "WEBP")
image.load()
image.getdata()
target = Image.open(file_path).convert("RGBA")
self.assert_image_similar(image, target, 10.0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -0,0 +1,120 @@
from helper import unittest, PillowTestCase, hopper
from PIL import Image
try:
from PIL import _webp
except ImportError:
pass
# Skip in setUp()
class TestFileWebpAnimation(PillowTestCase):
def setUp(self):
try:
from PIL import _webp
except ImportError:
self.skipTest('WebP support not installed')
if not _webp.HAVE_WEBPMUX:
self.skipTest("WebP not compiled with mux support, "
"not testing animation")
def test_n_frames(self):
"""
Ensure that webp format sets n_frames and is_animated
attributes correctly.
"""
im = Image.open("Tests/images/hopper.webp")
self.assertEqual(im.n_frames, 1)
self.assertFalse(im.is_animated)
im = Image.open("Tests/images/iss634.webp")
self.assertEqual(im.n_frames, 42)
self.assertTrue(im.is_animated)
def test_write_animation(self):
"""
Convert an animated GIF to animated WebP, then compare the
frame count, and first and last frames to ensure they're
visually similar.
"""
orig = Image.open("Tests/images/iss634.gif")
self.assertGreater(orig.n_frames, 1)
temp_file = self.tempfile("temp.webp")
orig.save(temp_file, save_all=True)
im = Image.open(temp_file)
self.assertEqual(im.n_frames, orig.n_frames)
# Compare first and last frames to the original animated GIF
orig.load()
im.load()
self.assert_image_similar(im, orig.convert("RGBA"), 25.0)
orig.seek(orig.n_frames-1)
im.seek(im.n_frames-1)
orig.load()
im.load()
self.assert_image_similar(im, orig.convert("RGBA"), 25.0)
def test_timestamp_and_duration(self):
"""
Try passing a list of durations, and make sure the encoded
timestamps and durations are correct.
"""
durations = [0, 10, 20, 30, 40]
temp_file = self.tempfile("temp.webp")
frame1 = Image.open('Tests/images/anim_frame1.webp')
frame2 = Image.open('Tests/images/anim_frame2.webp')
frame1.save(temp_file, save_all=True,
append_images=[frame2, frame1, frame2, frame1],
duration=durations)
im = Image.open(temp_file)
self.assertEqual(im.n_frames, 5)
self.assertTrue(im.is_animated)
# Double-check that timestamps and durations match original values specified
ts = 0
for frame in range(im.n_frames):
im.seek(frame)
im.load()
self.assertEqual(im.info["duration"], durations[frame])
self.assertEqual(im.info["timestamp"], ts)
ts += durations[frame]
def test_seeking(self):
"""
Create an animated webp file, and then try seeking through
frames in reverse-order, verifying the timestamps and durations
are correct.
"""
dur = 33
temp_file = self.tempfile("temp.webp")
frame1 = Image.open('Tests/images/anim_frame1.webp')
frame2 = Image.open('Tests/images/anim_frame2.webp')
frame1.save(temp_file, save_all=True,
append_images=[frame2, frame1, frame2, frame1],
duration=dur)
im = Image.open(temp_file)
self.assertEqual(im.n_frames, 5)
self.assertTrue(im.is_animated)
# Traverse frames in reverse order, double-check timestamps and duration
ts = dur * (im.n_frames-1)
for frame in reversed(range(im.n_frames)):
im.seek(frame)
im.load()
self.assertEqual(im.info["duration"], dur)
self.assertEqual(im.info["timestamp"], ts)
ts -= dur
if __name__ == '__main__':
unittest.main()

View File

@ -107,6 +107,26 @@ class TestFileWebpMetadata(PillowTestCase):
self.assertFalse(webp_image._getexif()) self.assertFalse(webp_image._getexif())
def test_write_animated_metadata(self):
iccp_data = "<iccp_data>"
exif_data = "<exif_data>"
xmp_data = "<xmp_data>"
temp_file = self.tempfile("temp.webp")
frame1 = Image.open('Tests/images/anim_frame1.webp')
frame2 = Image.open('Tests/images/anim_frame2.webp')
frame1.save(temp_file, save_all=True,
append_images=[frame2, frame1, frame2],
icc_profile=iccp_data, exif=exif_data, xmp=xmp_data)
image = Image.open(temp_file)
self.assertIn('icc_profile', image.info)
self.assertIn('exif', image.info)
self.assertIn('xmp', image.info)
self.assertEqual(iccp_data, image.info.get('icc_profile', None))
self.assertEqual(exif_data, image.info.get('exif', None))
self.assertEqual(xmp_data, image.info.get('xmp', None))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

187
_webp.c
View File

@ -10,6 +10,20 @@
#include <webp/mux.h> #include <webp/mux.h>
#include <webp/demux.h> #include <webp/demux.h>
/* -------------------------------------------------------------------- */
/* WebP Muxer Error Codes */
/* -------------------------------------------------------------------- */
static const char* const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = {
"WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_BAD_DATA",
"WEBP_MUX_MEMORY_ERROR", "WEBP_MUX_NOT_ENOUGH_DATA"
};
static const char* ErrorString(WebPMuxError err) {
assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA);
return kErrorMessages[-err];
}
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* WebP Animation Support */ /* WebP Animation Support */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
@ -29,6 +43,7 @@ typedef struct {
WebPAnimDecoder* dec; WebPAnimDecoder* dec;
WebPAnimInfo info; WebPAnimInfo info;
WebPData data; WebPData data;
char *mode;
} WebPAnimDecoderObject; } WebPAnimDecoderObject;
static PyTypeObject WebPAnimDecoder_Type; static PyTypeObject WebPAnimDecoder_Type;
@ -80,6 +95,7 @@ PyObject* _anim_encoder_new(PyObject* self, PyObject* args)
encp->enc = enc; encp->enc = enc;
return (PyObject*) encp; return (PyObject*) encp;
} }
WebPPictureFree(&(encp->frame));
} }
PyObject_Del(encp); PyObject_Del(encp);
} }
@ -162,11 +178,13 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args)
{ {
uint8_t *icc_bytes; uint8_t *icc_bytes;
uint8_t *exif_bytes; uint8_t *exif_bytes;
uint8_t *xmp_bytes;
Py_ssize_t icc_size; Py_ssize_t icc_size;
Py_ssize_t exif_size; Py_ssize_t exif_size;
Py_ssize_t xmp_size;
if (!PyArg_ParseTuple(args, "s#s#", if (!PyArg_ParseTuple(args, "s#s#s#",
&icc_bytes, &icc_size, &exif_bytes, &exif_size)) { &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
return NULL; return NULL;
} }
@ -182,9 +200,67 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// Convert to Python bytes and return // Re-mux to add metadata as needed
WebPMux* mux = NULL;
if (icc_size > 0 || exif_size > 0 || xmp_size > 0) {
WebPMuxError err = WEBP_MUX_OK;
int i_icc_size = (int)icc_size;
int i_exif_size = (int)exif_size;
int i_xmp_size = (int)xmp_size;
WebPData icc_profile = { icc_bytes, i_icc_size };
WebPData exif = { exif_bytes, i_exif_size };
WebPData xmp = { xmp_bytes, i_xmp_size };
mux = WebPMuxCreate(&webp_data, 1);
if (mux == NULL) {
fprintf(stderr, "ERROR: Could not re-mux to add metadata.\n");
Py_RETURN_NONE;
}
WebPDataClear(&webp_data);
// Add ICCP chunk
if (i_icc_size > 0) {
err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, 1);
if (err != WEBP_MUX_OK) {
fprintf(stderr, "ERROR (%s): Could not set ICC chunk.\n", ErrorString(err));
Py_RETURN_NONE;
}
}
// Add EXIF chunk
if (i_exif_size > 0) {
err = WebPMuxSetChunk(mux, "EXIF", &exif, 1);
if (err != WEBP_MUX_OK) {
fprintf(stderr, "ERROR (%s): Could not set EXIF chunk.\n", ErrorString(err));
Py_RETURN_NONE;
}
}
// Add XMP chunk
if (i_xmp_size > 0) {
err = WebPMuxSetChunk(mux, "XMP ", &xmp, 1);
if (err != WEBP_MUX_OK) {
fprintf(stderr, "ERROR (%s): Could not set XMP chunk.\n", ErrorString(err));
Py_RETURN_NONE;
}
}
err = WebPMuxAssemble(mux, &webp_data);
if (err != WEBP_MUX_OK) {
fprintf(stderr, "ERROR (%s): Could not assemble when re-muxing to add metadata.\n", ErrorString(err));
Py_RETURN_NONE;
}
}
// Convert to Python bytes
PyObject *ret = PyBytes_FromStringAndSize((char*)webp_data.bytes, webp_data.size); PyObject *ret = PyBytes_FromStringAndSize((char*)webp_data.bytes, webp_data.size);
WebPDataClear(&webp_data); WebPDataClear(&webp_data);
// If we had to re-mux, we should free it now that we're done with it
if (mux != NULL) {
WebPMuxDelete(mux);
}
return ret; return ret;
} }
@ -201,10 +277,20 @@ PyObject* _anim_decoder_new(PyObject* self, PyObject* args)
PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&webp, &size); PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&webp, &size);
WebPData webp_src = {webp, size}; WebPData webp_src = {webp, size};
// Sniff the mode, since the decoder API doesn't tell us
WebPDecoderConfig config;
char* mode = "RGBA";
if (WebPGetFeatures(webp, size, &config.input) == VP8_STATUS_OK) {
if (!config.input.has_alpha) {
mode = "RGB";
}
}
// Create the decoder (default mode is RGBA, if no options passed) // Create the decoder (default mode is RGBA, if no options passed)
WebPAnimDecoderObject* decp; WebPAnimDecoderObject* decp;
decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type); decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type);
if (decp) { if (decp) {
decp->mode = mode;
if (WebPDataCopy(&webp_src, &(decp->data))) { if (WebPDataCopy(&webp_src, &(decp->data))) {
WebPAnimDecoder* dec = WebPAnimDecoderNew(&(decp->data), NULL); WebPAnimDecoder* dec = WebPAnimDecoderNew(&(decp->data), NULL);
if (dec) { if (dec) {
@ -237,7 +323,7 @@ PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args)
info->loop_count, info->loop_count,
info->bgcolor, info->bgcolor,
info->frame_count, info->frame_count,
"RGBA" // WebPAnimDecoder defaults to RGBA if no mode is specified decp->mode
); );
} }
@ -275,8 +361,24 @@ PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
bytes = PyBytes_FromStringAndSize((char *)buf, // HACK: If original mode was RGB, we need to strip alpha before passing back, this
decp->info.canvas_width * 4 * decp->info.canvas_height); // is needed because internally WebPAnimDecoder doesn't suppor ta non-alpha mode
uint32_t size = decp->info.canvas_width * 4 * decp->info.canvas_height;
if (strcmp(decp->mode, "RGB")==0 && buf != NULL) {
uint32_t pixel_count = size / 4;
uint8_t* src = buf;
uint8_t* dst = buf;
for (uint32_t idx = 0; idx < pixel_count; ++idx) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst += 3;
src += 4;
}
size = pixel_count * 3;
}
bytes = PyBytes_FromStringAndSize((char *)buf, size);
return Py_BuildValue("Si", bytes, timestamp); return Py_BuildValue("Si", bytes, timestamp);
} }
@ -386,7 +488,6 @@ static PyTypeObject WebPAnimDecoder_Type = {
#endif #endif
#ifdef HAVE_WEBPMUX
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* Legacy WebP Support */ /* Legacy WebP Support */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
@ -400,16 +501,18 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
uint8_t *rgb; uint8_t *rgb;
uint8_t *icc_bytes; uint8_t *icc_bytes;
uint8_t *exif_bytes; uint8_t *exif_bytes;
uint8_t *xmp_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 icc_size;
Py_ssize_t exif_size; Py_ssize_t exif_size;
Py_ssize_t xmp_size;
size_t ret_size; size_t ret_size;
if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"iiifss#s#", if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"iiifss#s#s#",
(char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode, (char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode,
&icc_bytes, &icc_size, &exif_bytes, &exif_size)) { &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
return NULL; return NULL;
} }
if (strcmp(mode, "RGBA")==0){ if (strcmp(mode, "RGBA")==0){
@ -454,10 +557,12 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
*/ */
int i_icc_size = (int)icc_size; int i_icc_size = (int)icc_size;
int i_exif_size = (int)exif_size; int i_exif_size = (int)exif_size;
int i_xmp_size = (int)xmp_size;
WebPData output_data = {0}; WebPData output_data = {0};
WebPData image = { output, ret_size }; WebPData image = { output, ret_size };
WebPData icc_profile = { icc_bytes, i_icc_size }; WebPData icc_profile = { icc_bytes, i_icc_size };
WebPData exif = { exif_bytes, i_exif_size }; WebPData exif = { exif_bytes, i_exif_size };
WebPData xmp = { xmp_bytes, i_xmp_size };
WebPMuxError err; WebPMuxError err;
int dbg = 0; int dbg = 0;
@ -499,6 +604,21 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
} }
} }
if (dbg) {
fprintf(stderr, "xmp size %d \n", i_xmp_size);
}
if (i_xmp_size > 0) {
if (dbg){
fprintf (stderr, "Adding XMP Data\n");
}
err = WebPMuxSetChunk(mux, "XMP ", &xmp, copy_data);
if (dbg && err == WEBP_MUX_INVALID_ARGUMENT) {
fprintf(stderr, "Invalid XMP Argument\n");
} else if (dbg && err == WEBP_MUX_MEMORY_ERROR) {
fprintf(stderr, "XMP Memory Error\n");
}
}
WebPMuxAssemble(mux, &output_data); WebPMuxAssemble(mux, &output_data);
WebPMuxDelete(mux); WebPMuxDelete(mux);
free(output); free(output);
@ -520,7 +640,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
PyBytesObject *webp_string; PyBytesObject *webp_string;
const uint8_t *webp; const uint8_t *webp;
Py_ssize_t size; Py_ssize_t size;
PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL; PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL;
WebPDecoderConfig config; WebPDecoderConfig config;
VP8StatusCode vp8_status_code = VP8_STATUS_OK; VP8StatusCode vp8_status_code = VP8_STATUS_OK;
char* mode = "RGB"; char* mode = "RGB";
@ -544,41 +664,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
mode = "RGBA"; mode = "RGBA";
} }
#ifndef HAVE_WEBPMUX
vp8_status_code = WebPDecode(webp, size, &config); vp8_status_code = WebPDecode(webp, size, &config);
#else
{
int copy_data = 0;
WebPData data = { webp, size };
WebPMuxFrameInfo image;
WebPData icc_profile_data = {0};
WebPData exif_data = {0};
WebPMux* mux = WebPMuxCreate(&data, copy_data);
if (NULL == mux)
goto end;
if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image))
{
WebPMuxDelete(mux);
goto end;
}
webp = image.bitstream.bytes;
size = image.bitstream.size;
vp8_status_code = WebPDecode(webp, size, &config);
if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data))
icc_profile = PyBytes_FromStringAndSize((const char*)icc_profile_data.bytes, icc_profile_data.size);
if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data))
exif = PyBytes_FromStringAndSize((const char*)exif_data.bytes, exif_data.size);
WebPDataClear(&image.bitstream);
WebPMuxDelete(mux);
}
#endif
} }
if (vp8_status_code != VP8_STATUS_OK) if (vp8_status_code != VP8_STATUS_OK)
@ -599,18 +685,14 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
#else #else
pymode = PyString_FromString(mode); pymode = PyString_FromString(mode);
#endif #endif
ret = Py_BuildValue("SiiSSS", bytes, config.output.width, ret = Py_BuildValue("SiiS", bytes, config.output.width,
config.output.height, pymode, config.output.height, pymode);
NULL == icc_profile ? Py_None : icc_profile,
NULL == exif ? Py_None : exif);
end: end:
WebPFreeDecBuffer(&config.output); WebPFreeDecBuffer(&config.output);
Py_XDECREF(bytes); Py_XDECREF(bytes);
Py_XDECREF(pymode); Py_XDECREF(pymode);
Py_XDECREF(icc_profile);
Py_XDECREF(exif);
if (Py_None == ret) if (Py_None == ret)
Py_RETURN_NONE; Py_RETURN_NONE;
@ -618,8 +700,6 @@ end:
return ret; return ret;
} }
#endif
// Return the decoder's version number, packed in hexadecimal using 8bits for // Return the decoder's version number, packed in hexadecimal using 8bits for
// each of major/minor/revision. E.g: v2.5.7 is 0x020507. // each of major/minor/revision. E.g: v2.5.7 is 0x020507.
PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){ PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){
@ -647,10 +727,9 @@ static PyMethodDef webpMethods[] =
#ifdef HAVE_WEBPMUX #ifdef HAVE_WEBPMUX
{"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"},
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
#else #endif
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
#endif
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"}, {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
{"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"}, {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"},
{NULL, NULL} {NULL, NULL}