mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-10 19:56:47 +03:00
- 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:
parent
482d803717
commit
cd12a48fe0
|
@ -86,6 +86,9 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
return self._n_frames > 1
|
||||
|
||||
def seek(self, frame):
|
||||
if not _webp.HAVE_WEBPMUX:
|
||||
return super(WebPImageFile, self).seek(frame)
|
||||
|
||||
# Perform some simple checks first
|
||||
if frame >= self._n_frames:
|
||||
raise EOFError("attempted to seek beyond end of sequence")
|
||||
|
@ -133,31 +136,41 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
self._get_next()
|
||||
|
||||
def load(self):
|
||||
if self.__loaded != self.__logical_frame:
|
||||
self._seek(self.__logical_frame)
|
||||
if _webp.HAVE_WEBPMUX:
|
||||
if self.__loaded != self.__logical_frame:
|
||||
self._seek(self.__logical_frame)
|
||||
|
||||
# We need to load the image data for this frame
|
||||
data, timestamp, duration = self._get_next()
|
||||
self.info["timestamp"] = timestamp
|
||||
self.info["duration"] = duration
|
||||
self.__loaded = self.__logical_frame
|
||||
# We need to load the image data for this frame
|
||||
data, timestamp, duration = self._get_next()
|
||||
self.info["timestamp"] = timestamp
|
||||
self.info["duration"] = duration
|
||||
self.__loaded = self.__logical_frame
|
||||
|
||||
# Set tile
|
||||
self.fp = BytesIO(data)
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
|
||||
# Set tile
|
||||
self.fp = BytesIO(data)
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
|
||||
|
||||
return super(WebPImageFile, self).load()
|
||||
|
||||
def tell(self):
|
||||
if not _webp.HAVE_WEBPMUX:
|
||||
return super(WebPImageFile, self).tell()
|
||||
|
||||
return self.__logical_frame
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
encoderinfo = im.encoderinfo.copy()
|
||||
append_images = encoderinfo.get("append_images", [])
|
||||
background = encoderinfo.get("background", (0, 0, 0, 0))
|
||||
duration = im.encoderinfo.get("duration", 0)
|
||||
loop = im.encoderinfo.get("loop", 0)
|
||||
|
@ -171,6 +184,7 @@ def _save_all(im, fp, filename):
|
|||
method = im.encoderinfo.get("method", 0)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
xmp = im.encoderinfo.get("xmp", "")
|
||||
if allow_mixed:
|
||||
lossless = False
|
||||
|
||||
|
@ -217,8 +231,8 @@ def _save_all(im, fp, filename):
|
|||
# Make sure image mode is supported
|
||||
frame = ims
|
||||
if not ims.mode in _VALID_WEBP_MODES:
|
||||
alpha = 'A' in ims.im.getpalettemode()
|
||||
frame = image.convert('RGBA' if alpha else 'RGB')
|
||||
alpha = ims.mode == 'P' and 'A' in ims.im.getpalettemode()
|
||||
frame = ims.convert('RGBA' if alpha else 'RGB')
|
||||
|
||||
# Append the frame to the animation encoder
|
||||
enc.add(
|
||||
|
@ -246,26 +260,22 @@ def _save_all(im, fp, filename):
|
|||
)
|
||||
|
||||
# Get the final output from the encoder
|
||||
data = enc.assemble(icc_profile, exif)
|
||||
data = enc.assemble(icc_profile, exif, xmp)
|
||||
if data is None:
|
||||
raise IOError("cannot write file as WEBP (encoder returned None)")
|
||||
|
||||
fp.write(data)
|
||||
|
||||
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)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
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(
|
||||
im.tobytes(),
|
||||
|
@ -275,7 +285,8 @@ def _save(im, fp, filename):
|
|||
float(quality),
|
||||
im.mode,
|
||||
icc_profile,
|
||||
exif
|
||||
exif,
|
||||
xmp
|
||||
)
|
||||
if data is 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_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_mime(WebPImageFile.format, "image/webp")
|
||||
|
|
BIN
Tests/images/anim_frame1.webp
Normal file
BIN
Tests/images/anim_frame1.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 302 B |
BIN
Tests/images/anim_frame2.webp
Normal file
BIN
Tests/images/anim_frame2.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 288 B |
BIN
Tests/images/iss634.webp
Normal file
BIN
Tests/images/iss634.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 203 KiB |
BIN
Tests/images/transparent.gif
Normal file
BIN
Tests/images/transparent.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
|
@ -22,6 +22,10 @@ class TestFileWebp(PillowTestCase):
|
|||
_webp.WebPDecoderBuggyAlpha()
|
||||
|
||||
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"
|
||||
image = Image.open(file_path)
|
||||
|
@ -46,9 +50,7 @@ class TestFileWebp(PillowTestCase):
|
|||
temp_file = self.tempfile("temp.webp")
|
||||
|
||||
hopper("RGB").save(temp_file)
|
||||
|
||||
image = Image.open(temp_file)
|
||||
image.load()
|
||||
|
||||
self.assertEqual(image.mode, "RGB")
|
||||
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
|
||||
# Ubuntu, the jpegs are showing ~18.
|
||||
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")
|
||||
hopper("L").save(temp_file)
|
||||
image = Image.open(temp_file)
|
||||
|
||||
im = hopper("L")
|
||||
self.assertRaises(IOError, im.save, temp_file)
|
||||
self.assertEqual(image.mode, "RGB")
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -22,6 +22,11 @@ class TestFileWebpAlpha(PillowTestCase):
|
|||
"not testing transparency")
|
||||
|
||||
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`
|
||||
file_path = "Tests/images/transparent.webp"
|
||||
image = Image.open(file_path)
|
||||
|
@ -38,6 +43,11 @@ class TestFileWebpAlpha(PillowTestCase):
|
|||
self.assert_image_similar(image, target, 20.0)
|
||||
|
||||
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 = "temp.webp"
|
||||
|
||||
|
@ -90,6 +100,27 @@ class TestFileWebpAlpha(PillowTestCase):
|
|||
else:
|
||||
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__':
|
||||
unittest.main()
|
||||
|
|
120
Tests/test_file_webp_animated.py
Normal file
120
Tests/test_file_webp_animated.py
Normal 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()
|
|
@ -107,6 +107,26 @@ class TestFileWebpMetadata(PillowTestCase):
|
|||
|
||||
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__':
|
||||
unittest.main()
|
||||
|
|
191
_webp.c
191
_webp.c
|
@ -10,6 +10,20 @@
|
|||
#include <webp/mux.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 */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
@ -29,6 +43,7 @@ typedef struct {
|
|||
WebPAnimDecoder* dec;
|
||||
WebPAnimInfo info;
|
||||
WebPData data;
|
||||
char *mode;
|
||||
} WebPAnimDecoderObject;
|
||||
|
||||
static PyTypeObject WebPAnimDecoder_Type;
|
||||
|
@ -80,6 +95,7 @@ PyObject* _anim_encoder_new(PyObject* self, PyObject* args)
|
|||
encp->enc = enc;
|
||||
return (PyObject*) encp;
|
||||
}
|
||||
WebPPictureFree(&(encp->frame));
|
||||
}
|
||||
PyObject_Del(encp);
|
||||
}
|
||||
|
@ -162,11 +178,13 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args)
|
|||
{
|
||||
uint8_t *icc_bytes;
|
||||
uint8_t *exif_bytes;
|
||||
uint8_t *xmp_bytes;
|
||||
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#",
|
||||
&icc_bytes, &icc_size, &exif_bytes, &exif_size)) {
|
||||
if (!PyArg_ParseTuple(args, "s#s#s#",
|
||||
&icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
@ -182,9 +200,67 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args)
|
|||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -201,10 +277,20 @@ PyObject* _anim_decoder_new(PyObject* self, PyObject* args)
|
|||
PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&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)
|
||||
WebPAnimDecoderObject* decp;
|
||||
decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type);
|
||||
if (decp) {
|
||||
decp->mode = mode;
|
||||
if (WebPDataCopy(&webp_src, &(decp->data))) {
|
||||
WebPAnimDecoder* dec = WebPAnimDecoderNew(&(decp->data), NULL);
|
||||
if (dec) {
|
||||
|
@ -237,7 +323,7 @@ PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args)
|
|||
info->loop_count,
|
||||
info->bgcolor,
|
||||
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;
|
||||
}
|
||||
|
||||
bytes = PyBytes_FromStringAndSize((char *)buf,
|
||||
decp->info.canvas_width * 4 * decp->info.canvas_height);
|
||||
// HACK: If original mode was RGB, we need to strip alpha before passing back, this
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -386,7 +488,6 @@ static PyTypeObject WebPAnimDecoder_Type = {
|
|||
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_WEBPMUX
|
||||
/* -------------------------------------------------------------------- */
|
||||
/* Legacy WebP Support */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
@ -400,16 +501,18 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
|||
uint8_t *rgb;
|
||||
uint8_t *icc_bytes;
|
||||
uint8_t *exif_bytes;
|
||||
uint8_t *xmp_bytes;
|
||||
uint8_t *output;
|
||||
char *mode;
|
||||
Py_ssize_t 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;
|
||||
|
||||
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,
|
||||
&icc_bytes, &icc_size, &exif_bytes, &exif_size)) {
|
||||
&icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
|
||||
return NULL;
|
||||
}
|
||||
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_exif_size = (int)exif_size;
|
||||
int i_xmp_size = (int)xmp_size;
|
||||
WebPData output_data = {0};
|
||||
WebPData image = { output, ret_size };
|
||||
WebPData icc_profile = { icc_bytes, i_icc_size };
|
||||
WebPData exif = { exif_bytes, i_exif_size };
|
||||
WebPData xmp = { xmp_bytes, i_xmp_size };
|
||||
WebPMuxError err;
|
||||
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);
|
||||
WebPMuxDelete(mux);
|
||||
free(output);
|
||||
|
@ -520,7 +640,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
|
|||
PyBytesObject *webp_string;
|
||||
const uint8_t *webp;
|
||||
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;
|
||||
VP8StatusCode vp8_status_code = VP8_STATUS_OK;
|
||||
char* mode = "RGB";
|
||||
|
@ -544,41 +664,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
|
|||
mode = "RGBA";
|
||||
}
|
||||
|
||||
#ifndef HAVE_WEBPMUX
|
||||
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)
|
||||
|
@ -599,18 +685,14 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
|
|||
#else
|
||||
pymode = PyString_FromString(mode);
|
||||
#endif
|
||||
ret = Py_BuildValue("SiiSSS", bytes, config.output.width,
|
||||
config.output.height, pymode,
|
||||
NULL == icc_profile ? Py_None : icc_profile,
|
||||
NULL == exif ? Py_None : exif);
|
||||
ret = Py_BuildValue("SiiS", bytes, config.output.width,
|
||||
config.output.height, pymode);
|
||||
|
||||
end:
|
||||
WebPFreeDecBuffer(&config.output);
|
||||
|
||||
Py_XDECREF(bytes);
|
||||
Py_XDECREF(pymode);
|
||||
Py_XDECREF(icc_profile);
|
||||
Py_XDECREF(exif);
|
||||
|
||||
if (Py_None == ret)
|
||||
Py_RETURN_NONE;
|
||||
|
@ -618,8 +700,6 @@ end:
|
|||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Return the decoder's version number, packed in hexadecimal using 8bits for
|
||||
// each of major/minor/revision. E.g: v2.5.7 is 0x020507.
|
||||
PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){
|
||||
|
@ -647,10 +727,9 @@ static PyMethodDef webpMethods[] =
|
|||
#ifdef HAVE_WEBPMUX
|
||||
{"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"},
|
||||
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
|
||||
#else
|
||||
#endif
|
||||
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
|
||||
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
|
||||
#endif
|
||||
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
|
||||
{"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"},
|
||||
{NULL, NULL}
|
||||
|
|
Loading…
Reference in New Issue
Block a user