Merge pull request #2761 from monolithlabs/animated_webp

Add support for animated WebP files
This commit is contained in:
wiredfool 2017-11-05 15:34:50 +00:00 committed by GitHub
commit a3a9faf3a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1197 additions and 82 deletions

View File

@ -3,6 +3,11 @@ from io import BytesIO
_VALID_WEBP_MODES = {
"RGBX": True,
"RGBA": True,
}
_VALID_WEBP_LEGACY_MODES = {
"RGB": True,
"RGBA": True,
}
@ -28,32 +33,263 @@ class WebPImageFile(ImageFile.ImageFile):
format_description = "WebP image"
def _open(self):
data, width, height, self.mode, icc_profile, exif = \
_webp.WebPDecode(self.fp.read())
if not _webp.HAVE_WEBPANIM:
# Legacy mode
data, width, height, self.mode, icc_profile, exif = \
_webp.WebPDecode(self.fp.read())
if icc_profile:
self.info["icc_profile"] = icc_profile
if exif:
self.info["exif"] = exif
self.size = width, height
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
self._n_frames = 1
return
# Use the newer AnimDecoder API to parse the (possibly) animated file,
# and access muxed chunks like ICC/EXIF/XMP.
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
# Get info from decoder
width, height, loop_count, bgcolor, frame_count, mode = \
self._decoder.get_info()
self.size = width, height
self.info["loop"] = loop_count
bg_a, bg_r, bg_g, bg_b = \
(bgcolor >> 24) & 0xFF, \
(bgcolor >> 16) & 0xFF, \
(bgcolor >> 8) & 0xFF, \
bgcolor & 0xFF
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
self._n_frames = frame_count
self.mode = mode
self.tile = []
# Attempt to read ICC / EXIF / XMP chunks from file
icc_profile = self._decoder.get_chunk("ICCP")
exif = self._decoder.get_chunk("EXIF")
xmp = self._decoder.get_chunk("XMP ")
if icc_profile:
self.info["icc_profile"] = icc_profile
if exif:
self.info["exif"] = exif
if xmp:
self.info["xmp"] = xmp
self.size = width, height
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
# Initialize seek state
self._reset(reset=False)
self.seek(0)
def _getexif(self):
from .JpegImagePlugin import _getexif
return _getexif(self)
@property
def n_frames(self):
return self._n_frames
@property
def is_animated(self):
return self._n_frames > 1
def seek(self, frame):
if not _webp.HAVE_WEBPANIM:
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")
if frame < 0:
raise EOFError("negative frame index is not valid")
# Set logical frame to requested position
self.__logical_frame = frame
def _reset(self, reset=True):
if reset:
self._decoder.reset()
self.__physical_frame = 0
self.__loaded = -1
self.__timestamp = 0
def _get_next(self):
# Get next frame
ret = self._decoder.get_next()
self.__physical_frame += 1
# Check if an error occurred
if ret is None:
self._reset() # Reset just to be safe
self.seek(0)
raise EOFError("failed to decode next frame in WebP file")
# Compute duration
data, timestamp = ret
duration = timestamp - self.__timestamp
self.__timestamp = timestamp
# libwebp gives frame end, adjust to start of frame
timestamp -= duration
return data, timestamp, duration
def _seek(self, frame):
if self.__physical_frame == frame:
return # Nothing to do
if frame < self.__physical_frame:
self._reset() # Rewind to beginning
while self.__physical_frame < frame:
self._get_next() # Advance to the requested frame
def load(self):
if _webp.HAVE_WEBPANIM:
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
# 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_WEBPANIM:
return super(WebPImageFile, self).tell()
return self.__logical_frame
def _save_all(im, fp, filename):
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
background = encoderinfo.get("background", (0, 0, 0, 0))
duration = im.encoderinfo.get("duration", 0)
loop = im.encoderinfo.get("loop", 0)
minimize_size = im.encoderinfo.get("minimize_size", False)
kmin = im.encoderinfo.get("kmin", None)
kmax = im.encoderinfo.get("kmax", None)
allow_mixed = im.encoderinfo.get("allow_mixed", False)
verbose = False
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
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
# Sensible keyframe defaults are from gif2webp.c script
if kmin is None:
kmin = 9 if lossless else 3
if kmax is None:
kmax = 17 if lossless else 5
# Validate background color
if (not isinstance(background, (list, tuple)) or len(background) != 4 or
not all(v >= 0 and v < 256 for v in background)):
raise IOError("Background color is not an RGBA tuple clamped "
"to (0-255): %s" % str(background))
# Convert to packed uint
bg_r, bg_g, bg_b, bg_a = background
background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0)
# Setup the WebP animation encoder
enc = _webp.WebPAnimEncoder(
im.size[0], im.size[1],
background,
loop,
minimize_size,
kmin, kmax,
allow_mixed,
verbose
)
# Add each frame
frame_idx = 0
timestamp = 0
cur_idx = im.tell()
try:
for ims in [im]+append_images:
# Get # of frames in this image
if not hasattr(ims, "n_frames"):
nfr = 1
else:
nfr = ims.n_frames
for idx in range(nfr):
ims.seek(idx)
ims.load()
# Make sure image mode is supported
frame = ims
if ims.mode not in _VALID_WEBP_MODES:
alpha = ims.mode == 'P' and 'A' in ims.im.getpalettemode()
frame = ims.convert('RGBA' if alpha else 'RGBX')
# Append the frame to the animation encoder
enc.add(
frame.tobytes(),
timestamp,
frame.size[0], frame.size[1],
frame.mode,
lossless,
quality,
method
)
# Update timestamp and frame index
if isinstance(duration, (list, tuple)):
timestamp += duration[frame_idx]
else:
timestamp += duration
frame_idx += 1
finally:
im.seek(cur_idx)
# Force encoder to flush frames
enc.add(
None,
timestamp,
0, 0, "", lossless, quality, 0
)
# Get the final output from the encoder
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):
image_mode = im.mode
if im.mode not in _VALID_WEBP_MODES:
raise IOError("cannot write mode %s as WEBP" % image_mode)
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_LEGACY_MODES:
alpha = im.mode == 'P' and 'A' in im.im.getpalettemode()
im = im.convert('RGBA' if alpha else 'RGB')
data = _webp.WebPEncode(
im.tobytes(),
@ -63,16 +299,18 @@ 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)")
raise IOError("cannot write file as WebP (encoder returned None)")
fp.write(data)
Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
Image.register_save(WebPImageFile.format, _save)
if _webp.HAVE_WEBPANIM:
Image.register_save_all(WebPImageFile.format, _save_all)
Image.register_extension(WebPImageFile.format, ".webp")
Image.register_mime(WebPImageFile.format, "image/webp")

View File

@ -43,6 +43,7 @@ def get_supported_codecs():
return [f for f in codecs if check_codec(f)]
features = {
"webp_anim": ("PIL._webp", 'HAVE_WEBPANIM'),
"webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
"raqm": ("PIL._imagingft", "HAVE_RAQM")
@ -53,7 +54,7 @@ def check_feature(feature):
raise ValueError("Unknown feature %s" % feature)
module, flag = features[feature]
try:
imported_module = __import__(module, fromlist=['PIL'])
return getattr(imported_module, flag)
@ -75,4 +76,4 @@ def get_supported():
ret.extend(get_supported_features())
ret.extend(get_supported_codecs())
return ret

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

@ -35,6 +35,11 @@ class TestFeatures(PillowTestCase):
self.assertEqual(features.check('webp_mux'),
_webp.HAVE_WEBPMUX)
@unittest.skipUnless(HAVE_WEBP, True)
def check_webp_anim(self):
self.assertEqual(features.check('webp_anim'),
_webp.HAVE_WEBPANIM)
def test_check_modules(self):
for feature in features.modules:
self.assertIn(features.check_module(feature), [True, False])

View File

@ -4,29 +4,35 @@ from PIL import Image
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
# Skip in setUp()
pass
HAVE_WEBP = False
class TestFileWebp(PillowTestCase):
def setUp(self):
try:
from PIL import _webp
except ImportError:
if not HAVE_WEBP:
self.skipTest('WebP support not installed')
return
# WebPAnimDecoder only returns RGBA or RGBX, never RGB
self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB"
def test_version(self):
_webp.WebPDecoderVersion()
_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)
self.assertEqual(image.mode, "RGB")
self.assertEqual(image.mode, self.rgb_mode)
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.format, "WEBP")
image.load()
@ -35,6 +41,7 @@ class TestFileWebp(PillowTestCase):
# generated with:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
target = Image.open('Tests/images/hopper_webp_bits.ppm')
target = target.convert(self.rgb_mode)
self.assert_image_similar(image, target, 20.0)
def test_write_rgb(self):
@ -45,12 +52,10 @@ class TestFileWebp(PillowTestCase):
temp_file = self.tempfile("temp.webp")
hopper("RGB").save(temp_file)
hopper(self.rgb_mode).save(temp_file)
image = Image.open(temp_file)
image.load()
self.assertEqual(image.mode, "RGB")
self.assertEqual(image.mode, self.rgb_mode)
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.format, "WEBP")
image.load()
@ -69,20 +74,67 @@ class TestFileWebp(PillowTestCase):
# then we're going to accept that it's a reasonable lossy version of
# 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)
target = hopper(self.rgb_mode)
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, self.rgb_mode)
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.format, "WEBP")
image.load()
image.getdata()
target = hopper("L").convert(self.rgb_mode)
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, self.rgb_mode)
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.format, "WEBP")
image.load()
image.getdata()
target = hopper("P").convert(self.rgb_mode)
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_WEBPANIM:
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_WEBPANIM:
self.assertRaises(TypeError, _webp.WebPAnimDecoder)
self.assertRaises(TypeError, _webp.WebPDecode)
if __name__ == '__main__':
unittest.main()

View File

@ -22,6 +22,11 @@ class TestFileWebpAlpha(PillowTestCase):
"not testing transparency")
def test_read_rgba(self):
"""
Can we read an 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 an 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()

View File

@ -0,0 +1,145 @@
from helper import unittest, PillowTestCase
from PIL import Image
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
HAVE_WEBP = False
class TestFileWebpAnimation(PillowTestCase):
def setUp(self):
if not HAVE_WEBP:
self.skipTest('WebP support not installed')
return
if not _webp.HAVE_WEBPANIM:
self.skipTest("WebP library does not contain animation 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_L(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_write_animation_RGB(self):
"""
Write an animated WebP from RGB frames, and ensure the frames
are visually similar to the originals.
"""
temp_file = self.tempfile("temp.webp")
temp_file2 = self.tempfile("temp.png")
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], lossless=True)
im = Image.open(temp_file)
self.assertEqual(im.n_frames, 2)
# Compare first frame to original
im.load()
im.save(temp_file2)
self.assert_image_equal(im, frame1.convert("RGBA"))
# Compare second frame to original
im.seek(1)
im.load()
self.assert_image_equal(im, frame2.convert("RGBA"))
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)
# 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, checking timestamps and durations
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

@ -4,37 +4,39 @@ from PIL import Image
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
pass
# Skip in setUp()
HAVE_WEBP = False
class TestFileWebpLossless(PillowTestCase):
def setUp(self):
try:
from PIL import _webp
except:
if not HAVE_WEBP:
self.skipTest('WebP support not installed')
return
if (_webp.WebPDecoderVersion() < 0x0200):
self.skipTest('lossless not included')
# WebPAnimDecoder only returns RGBA or RGBX, never RGB
self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB"
def test_write_lossless_rgb(self):
temp_file = self.tempfile("temp.webp")
hopper("RGB").save(temp_file, lossless=True)
hopper(self.rgb_mode).save(temp_file, lossless=True)
image = Image.open(temp_file)
image.load()
self.assertEqual(image.mode, "RGB")
self.assertEqual(image.mode, self.rgb_mode)
self.assertEqual(image.size, (128, 128))
self.assertEqual(image.format, "WEBP")
image.load()
image.getdata()
self.assert_image_equal(image, hopper("RGB"))
self.assert_image_equal(image, hopper(self.rgb_mode))
if __name__ == '__main__':

View File

@ -2,13 +2,17 @@ from helper import unittest, PillowTestCase
from PIL import Image
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
HAVE_WEBP = False
class TestFileWebpMetadata(PillowTestCase):
def setUp(self):
try:
from PIL import _webp
except ImportError:
if not HAVE_WEBP:
self.skipTest('WebP support not installed')
return
@ -107,6 +111,29 @@ class TestFileWebpMetadata(PillowTestCase):
self.assertFalse(webp_image._getexif())
def test_write_animated_metadata(self):
if not _webp.HAVE_WEBPANIM:
self.skipTest('WebP animation support not available')
iccp_data = '<iccp_data>'.encode('utf-8')
exif_data = '<exif_data>'.encode('utf-8')
xmp_data = '<xmp_data>'.encode('utf-8')
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()

633
_webp.c
View File

@ -8,27 +8,551 @@
#ifdef HAVE_WEBPMUX
#include <webp/mux.h>
#include <webp/demux.h>
/*
* Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and
* WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The
* very early versions added had some significant differences, so we require
* later versions, before enabling animation support.
*/
#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105
#define HAVE_WEBPANIM
#endif
#endif
/* -------------------------------------------------------------------- */
/* WebP Muxer Error Handling */
/* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPMUX
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"
};
PyObject* HandleMuxError(WebPMuxError err, char* chunk) {
char message[100];
int message_len;
assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA);
// Check for a memory error first
if (err == WEBP_MUX_MEMORY_ERROR) {
return PyErr_NoMemory();
}
// Create the error message
if (chunk == NULL) {
message_len = sprintf(message, "could not assemble chunks: %s", kErrorMessages[-err]);
} else {
message_len = sprintf(message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err]);
}
if (message_len < 0) {
PyErr_SetString(PyExc_RuntimeError, "failed to construct error message");
return NULL;
}
// Set the proper error type
switch (err) {
case WEBP_MUX_NOT_FOUND:
case WEBP_MUX_INVALID_ARGUMENT:
PyErr_SetString(PyExc_ValueError, message);
break;
case WEBP_MUX_BAD_DATA:
case WEBP_MUX_NOT_ENOUGH_DATA:
PyErr_SetString(PyExc_IOError, message);
break;
default:
PyErr_SetString(PyExc_RuntimeError, message);
break;
}
return NULL;
}
#endif
/* -------------------------------------------------------------------- */
/* WebP Animation Support */
/* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPANIM
// Encoder type
typedef struct {
PyObject_HEAD
WebPAnimEncoder* enc;
WebPPicture frame;
} WebPAnimEncoderObject;
static PyTypeObject WebPAnimEncoder_Type;
// Decoder type
typedef struct {
PyObject_HEAD
WebPAnimDecoder* dec;
WebPAnimInfo info;
WebPData data;
char* mode;
} WebPAnimDecoderObject;
static PyTypeObject WebPAnimDecoder_Type;
// Encoder functions
PyObject* _anim_encoder_new(PyObject* self, PyObject* args)
{
int width, height;
uint32_t bgcolor;
int loop_count;
int minimize_size;
int kmin, kmax;
int allow_mixed;
int verbose;
WebPAnimEncoderOptions enc_options;
WebPAnimEncoderObject* encp = NULL;
WebPAnimEncoder* enc = NULL;
if (!PyArg_ParseTuple(args, "iiIiiiiii",
&width, &height, &bgcolor, &loop_count, &minimize_size,
&kmin, &kmax, &allow_mixed, &verbose)) {
return NULL;
}
// Setup and configure the encoder's options (these are animation-specific)
if (!WebPAnimEncoderOptionsInit(&enc_options)) {
PyErr_SetString(PyExc_RuntimeError, "failed to initialize encoder options");
return NULL;
}
enc_options.anim_params.bgcolor = bgcolor;
enc_options.anim_params.loop_count = loop_count;
enc_options.minimize_size = minimize_size;
enc_options.kmin = kmin;
enc_options.kmax = kmax;
enc_options.allow_mixed = allow_mixed;
enc_options.verbose = verbose;
// Validate canvas dimensions
if (width <= 0 || height <= 0) {
PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions");
return NULL;
}
// Create a new animation encoder and picture frame
encp = PyObject_New(WebPAnimEncoderObject, &WebPAnimEncoder_Type);
if (encp) {
if (WebPPictureInit(&(encp->frame))) {
enc = WebPAnimEncoderNew(width, height, &enc_options);
if (enc) {
encp->enc = enc;
return (PyObject*) encp;
}
WebPPictureFree(&(encp->frame));
}
PyObject_Del(encp);
}
PyErr_SetString(PyExc_RuntimeError, "could not create encoder object");
return NULL;
}
PyObject* _anim_encoder_dealloc(PyObject* self)
{
WebPAnimEncoderObject* encp = (WebPAnimEncoderObject*)self;
WebPPictureFree(&(encp->frame));
WebPAnimEncoderDelete(encp->enc);
Py_RETURN_NONE;
}
PyObject* _anim_encoder_add(PyObject* self, PyObject* args)
{
uint8_t* rgb;
Py_ssize_t size;
int timestamp;
int width;
int height;
char* mode;
int lossless;
float quality_factor;
int method;
WebPConfig config;
WebPAnimEncoderObject* encp = (WebPAnimEncoderObject*)self;
WebPAnimEncoder* enc = encp->enc;
WebPPicture* frame = &(encp->frame);
if (!PyArg_ParseTuple(args, "z#iiisifi",
(char**)&rgb, &size, &timestamp, &width, &height, &mode,
&lossless, &quality_factor, &method)) {
return NULL;
}
// Check for NULL frame, which sets duration of final frame
if (!rgb) {
WebPAnimEncoderAdd(enc, NULL, timestamp, NULL);
Py_RETURN_NONE;
}
// Setup config for this frame
if (!WebPConfigInit(&config)) {
PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!");
return NULL;
}
config.lossless = lossless;
config.quality = quality_factor;
config.method = method;
// Validate the config
if (!WebPValidateConfig(&config)) {
PyErr_SetString(PyExc_ValueError, "invalid configuration");
return NULL;
}
// Populate the frame with raw bytes passed to us
frame->width = width;
frame->height = height;
frame->use_argb = 1; // Don't convert RGB pixels to YUV
if (strcmp(mode, "RGBA")==0) {
WebPPictureImportRGBA(frame, rgb, 4 * width);
} else if (strcmp(mode, "RGBX")==0) {
WebPPictureImportRGBX(frame, rgb, 4 * width);
}
// Add the frame to the encoder
if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) {
PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc));
return NULL;
}
Py_RETURN_NONE;
}
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 xmp_size;
WebPData webp_data;
WebPAnimEncoderObject* encp = (WebPAnimEncoderObject*)self;
WebPAnimEncoder* enc = encp->enc;
WebPMux* mux = NULL;
PyObject* ret = NULL;
if (!PyArg_ParseTuple(args, "s#s#s#",
&icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
return NULL;
}
// Init the output buffer
WebPDataInit(&webp_data);
// Assemble everything into the output buffer
if (!WebPAnimEncoderAssemble(enc, &webp_data)) {
PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc));
return NULL;
}
// Re-mux to add metadata as needed
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) {
PyErr_SetString(PyExc_RuntimeError, "could not re-mux to add metadata");
return NULL;
}
WebPDataClear(&webp_data);
// Add ICCP chunk
if (i_icc_size > 0) {
err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, 1);
if (err != WEBP_MUX_OK) {
return HandleMuxError(err, "ICCP");
}
}
// Add EXIF chunk
if (i_exif_size > 0) {
err = WebPMuxSetChunk(mux, "EXIF", &exif, 1);
if (err != WEBP_MUX_OK) {
return HandleMuxError(err, "EXIF");
}
}
// Add XMP chunk
if (i_xmp_size > 0) {
err = WebPMuxSetChunk(mux, "XMP ", &xmp, 1);
if (err != WEBP_MUX_OK) {
return HandleMuxError(err, "XMP");
}
}
err = WebPMuxAssemble(mux, &webp_data);
if (err != WEBP_MUX_OK) {
return HandleMuxError(err, NULL);
}
}
// Convert to Python bytes
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;
}
// Decoder functions
PyObject* _anim_decoder_new(PyObject* self, PyObject* args)
{
PyBytesObject *webp_string;
const uint8_t *webp;
Py_ssize_t size;
WebPData webp_src;
char* mode;
WebPDecoderConfig config;
WebPAnimDecoderObject* decp = NULL;
WebPAnimDecoder* dec = NULL;
if (!PyArg_ParseTuple(args, "S", &webp_string)) {
return NULL;
}
PyBytes_AsStringAndSize((PyObject *)webp_string, (char**)&webp, &size);
webp_src.bytes = webp;
webp_src.size = size;
// Sniff the mode, since the decoder API doesn't tell us
mode = "RGBA";
if (WebPGetFeatures(webp, size, &config.input) == VP8_STATUS_OK) {
if (!config.input.has_alpha) {
mode = "RGBX";
}
}
// Create the decoder (default mode is RGBA, if no options passed)
decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type);
if (decp) {
decp->mode = mode;
if (WebPDataCopy(&webp_src, &(decp->data))) {
dec = WebPAnimDecoderNew(&(decp->data), NULL);
if (dec) {
if (WebPAnimDecoderGetInfo(dec, &(decp->info))) {
decp->dec = dec;
return (PyObject*)decp;
}
}
}
PyObject_Del(decp);
}
PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
return NULL;
}
PyObject* _anim_decoder_dealloc(PyObject* self)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
WebPDataClear(&(decp->data));
WebPAnimDecoderDelete(decp->dec);
Py_RETURN_NONE;
}
PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
WebPAnimInfo* info = &(decp->info);
return Py_BuildValue("IIIIIs",
info->canvas_width, info->canvas_height,
info->loop_count,
info->bgcolor,
info->frame_count,
decp->mode
);
}
PyObject* _anim_decoder_get_chunk(PyObject* self, PyObject* args)
{
char* mode;
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
const WebPDemuxer* demux;
WebPChunkIterator iter;
PyObject *ret;
if (!PyArg_ParseTuple(args, "s", &mode)) {
return NULL;
}
demux = WebPAnimDecoderGetDemuxer(decp->dec);
if (!WebPDemuxGetChunk(demux, mode, 1, &iter)) {
Py_RETURN_NONE;
}
ret = PyBytes_FromStringAndSize((const char*)iter.chunk.bytes, iter.chunk.size);
WebPDemuxReleaseChunkIterator(&iter);
return ret;
}
PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args)
{
uint8_t* buf;
int timestamp;
PyObject* bytes;
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject*)self;
if (!WebPAnimDecoderGetNext(decp->dec, &buf, &timestamp)) {
PyErr_SetString(PyExc_IOError, "failed to read next frame");
return NULL;
}
bytes = PyBytes_FromStringAndSize((char *)buf,
decp->info.canvas_width * 4 * decp->info.canvas_height);
return Py_BuildValue("Si", bytes, timestamp);
}
PyObject* _anim_decoder_has_more_frames(PyObject* self, PyObject* args)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject*)self;
return Py_BuildValue("i", WebPAnimDecoderHasMoreFrames(decp->dec));
}
PyObject* _anim_decoder_reset(PyObject* self, PyObject* args)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
WebPAnimDecoderReset(decp->dec);
Py_RETURN_NONE;
}
/* -------------------------------------------------------------------- */
/* Type Definitions */
/* -------------------------------------------------------------------- */
// WebPAnimEncoder methods
static struct PyMethodDef _anim_encoder_methods[] = {
{"add", (PyCFunction)_anim_encoder_add, METH_VARARGS, "add"},
{"assemble", (PyCFunction)_anim_encoder_assemble, METH_VARARGS, "assemble"},
{NULL, NULL} /* sentinel */
};
// WebPAnimDecoder type definition
static PyTypeObject WebPAnimEncoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"WebPAnimEncoder", /*tp_name */
sizeof(WebPAnimEncoderObject), /*tp_size */
0, /*tp_itemsize */
/* methods */
(destructor)_anim_encoder_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number */
0, /*tp_as_sequence */
0, /*tp_as_mapping */
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
0, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
_anim_encoder_methods, /*tp_methods*/
0, /*tp_members*/
0, /*tp_getset*/
};
// WebPAnimDecoder methods
static struct PyMethodDef _anim_decoder_methods[] = {
{"get_info", (PyCFunction)_anim_decoder_get_info, METH_VARARGS, "get_info"},
{"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"},
{"get_next", (PyCFunction)_anim_decoder_get_next, METH_VARARGS, "get_next"},
{"has_more_frames", (PyCFunction)_anim_decoder_has_more_frames, METH_VARARGS, "has_more_frames"},
{"reset", (PyCFunction)_anim_decoder_reset, METH_VARARGS, "reset"},
{NULL, NULL} /* sentinel */
};
// WebPAnimDecoder type definition
static PyTypeObject WebPAnimDecoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"WebPAnimDecoder", /*tp_name */
sizeof(WebPAnimDecoderObject), /*tp_size */
0, /*tp_itemsize */
/* methods */
(destructor)_anim_decoder_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number */
0, /*tp_as_sequence */
0, /*tp_as_mapping */
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
0, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
_anim_decoder_methods, /*tp_methods*/
0, /*tp_members*/
0, /*tp_getset*/
};
#endif
/* -------------------------------------------------------------------- */
/* Legacy WebP Support */
/* -------------------------------------------------------------------- */
PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
{
int width;
int height;
int lossless;
float quality_factor;
uint8_t *rgb;
uint8_t *icc_bytes;
uint8_t *exif_bytes;
uint8_t *output;
char *mode;
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){
@ -37,11 +561,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
}
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
if (lossless) {
ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4* width, &output);
ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output);
} else
#endif
{
ret_size = WebPEncodeRGBA(rgb, width, height, 4* width, quality_factor, &output);
ret_size = WebPEncodeRGBA(rgb, width, height, 4 * width, quality_factor, &output);
}
} else if (strcmp(mode, "RGB")==0){
if (size < width * height * 3){
@ -49,11 +573,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
}
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
if (lossless) {
ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3* width, &output);
ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output);
} else
#endif
{
ret_size = WebPEncodeRGB(rgb, width, height, 3* width, quality_factor, &output);
ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output);
}
} else {
Py_RETURN_NONE;
@ -66,17 +590,19 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
return ret;
}
#else
{
/* I want to truncate the *_size items that get passed into webp
{
/* I want to truncate the *_size items that get passed into WebP
data. Pypy2.1.0 had some issues where the Py_ssize_t items had
data in the upper byte. (Not sure why, it shouldn't have been there)
*/
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;
@ -93,13 +619,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
if (i_icc_size > 0) {
if (dbg) {
fprintf (stderr, "Adding ICC Profile\n");
fprintf(stderr, "Adding ICC Profile\n");
}
err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data);
if (dbg && err == WEBP_MUX_INVALID_ARGUMENT) {
fprintf(stderr, "Invalid ICC Argument\n");
} else if (dbg && err == WEBP_MUX_MEMORY_ERROR) {
fprintf(stderr, "ICC Memory Error\n");
if (err != WEBP_MUX_OK) {
return HandleMuxError(err, "ICCP");
}
}
@ -107,14 +631,25 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
fprintf(stderr, "exif size %d \n", i_exif_size);
}
if (i_exif_size > 0) {
if (dbg){
fprintf (stderr, "Adding Exif Data\n");
if (dbg) {
fprintf(stderr, "Adding Exif Data\n");
}
err = WebPMuxSetChunk(mux, "EXIF", &exif, copy_data);
if (dbg && err == WEBP_MUX_INVALID_ARGUMENT) {
fprintf(stderr, "Invalid Exif Argument\n");
} else if (dbg && err == WEBP_MUX_MEMORY_ERROR) {
fprintf(stderr, "Exif Memory Error\n");
if (err != WEBP_MUX_OK) {
return HandleMuxError(err, "EXIF");
}
}
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 (err != WEBP_MUX_OK) {
return HandleMuxError(err, "XMP ");
}
}
@ -133,11 +668,10 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
Py_RETURN_NONE;
}
PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
{
PyBytesObject *webp_string;
const uint8_t *webp;
PyBytesObject* webp_string;
const uint8_t* webp;
Py_ssize_t size;
PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL;
WebPDecoderConfig config;
@ -152,7 +686,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
Py_RETURN_NONE;
}
PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&webp, &size);
PyBytes_AsStringAndSize((PyObject*) webp_string, (char**)&webp, &size);
vp8_status_code = WebPGetFeatures(webp, size, &config.input);
if (vp8_status_code == VP8_STATUS_OK) {
@ -204,12 +738,12 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
goto end;
if (config.output.colorspace < MODE_YUV) {
bytes = PyBytes_FromStringAndSize((char *)config.output.u.RGBA.rgba,
bytes = PyBytes_FromStringAndSize((char*)config.output.u.RGBA.rgba,
config.output.u.RGBA.size);
} else {
// Skipping YUV for now. Need Test Images.
// UNDONE -- unclear if we'll ever get here if we set mode_rgb*
bytes = PyBytes_FromStringAndSize((char *)config.output.u.YUVA.y,
bytes = PyBytes_FromStringAndSize((char*)config.output.u.YUVA.y,
config.output.u.YUVA.y_size);
}
@ -255,8 +789,16 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){
return Py_BuildValue("i", WebPDecoderBuggyAlpha());
}
/* -------------------------------------------------------------------- */
/* Module Setup */
/* -------------------------------------------------------------------- */
static PyMethodDef webpMethods[] =
{
#ifdef HAVE_WEBPANIM
{"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"},
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
#endif
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
@ -272,11 +814,32 @@ void addMuxFlagToModule(PyObject* m) {
#endif
}
void addAnimFlagToModule(PyObject* m) {
#ifdef HAVE_WEBPANIM
PyModule_AddObject(m, "HAVE_WEBPANIM", Py_True);
#else
PyModule_AddObject(m, "HAVE_WEBPANIM", Py_False);
#endif
}
void addTransparencyFlagToModule(PyObject* m) {
PyModule_AddObject(m, "HAVE_TRANSPARENCY",
PyBool_FromLong(!WebPDecoderBuggyAlpha()));
}
static int setup_module(PyObject* m) {
addMuxFlagToModule(m);
addAnimFlagToModule(m);
addTransparencyFlagToModule(m);
#ifdef HAVE_WEBPANIM
/* Ready object types */
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||
PyType_Ready(&WebPAnimEncoder_Type) < 0)
return -1;
#endif
return 0;
}
#if PY_VERSION_HEX >= 0x03000000
PyMODINIT_FUNC
@ -292,8 +855,9 @@ PyInit__webp(void) {
};
m = PyModule_Create(&module_def);
addMuxFlagToModule(m);
addTransparencyFlagToModule(m);
if (setup_module(m) < 0)
return NULL;
return m;
}
#else
@ -301,7 +865,6 @@ PyMODINIT_FUNC
init_webp(void)
{
PyObject* m = Py_InitModule("_webp", webpMethods);
addMuxFlagToModule(m);
addTransparencyFlagToModule(m);
setup_module(m);
}
#endif

View File

@ -110,7 +110,7 @@ are available::
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images.
This is currently only supported for GIF, PDF and TIFF.
This is currently only supported for GIF, PDF, TIFF, and WebP.
**duration**
The display duration of each frame of the multiframe gif, in
@ -658,19 +658,69 @@ format are currently undocumented.
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**lossless**
If present and true, instructs the WEBP writer to use lossless compression.
If present and true, instructs the WebP writer to use lossless compression.
**quality**
Integer, 1-100, Defaults to 80. Sets the quality level for
lossy compression.
Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest
size and 100 the largest. For lossless, this parameter is the amount
of effort put into the compression: 0 is the fastest, but gives larger
files compared to the slowest, but best, 100.
**method**
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0.
**icc_procfile**
The ICC Profile to include in the saved file. Only supported if
the system webp library was built with webpmux support.
the system WebP library was built with webpmux support.
**exif**
The exif data to include in the saved file. Only supported if
the system webp library was built with webpmux support.
the system WebP library was built with webpmux support.
Saving sequences
~~~~~~~~~~~~~~~~~
.. note::
Support for animated WebP files will only be enabled if the system WebP
library is v0.5.0 or later. You can check webp animation support at
runtime by calling `features.check("webp_anim")`.
When calling :py:meth:`~PIL.Image.Image.save`, the following options
are available when the `save_all` argument is present and true.
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images.
**duration**
The display duration of each frame, in milliseconds. Pass a single
integer for a constant duration, or a list or tuple to set the
duration for each frame separately.
**loop**
Number of times to repeat the animation. Defaults to [0 = infinite].
**background**
Background color of the canvas, as an RGBA tuple with values in
the range of (0-255).
**minimize_size**
If true, minimize the output size (slow). Implicitly disables
key-frame insertion.
**kmin, kmax**
Minimum and maximum distance between consecutive key frames in
the output. The library may insert some key frames as needed
to satisfy this criteria. Note that these conditions should
hold: kmax > kmin and kmin >= kmax / 2 + 1. Also, if kmax <= 0,
then key-frame insertion is disabled; and if kmax == 1, then all
frames will be key-frames (kmin value does not matter for these
special cases).
**allow_mixed**
If true, use mixed compression mode; the encoder heuristically
chooses between lossy and lossless for each frame.
XBM
^^^

View File

@ -178,8 +178,9 @@ if __name__ == "__main__":
("freetype2", "FREETYPE2"),
("littlecms2", "LITTLECMS2"),
("webp", "WEBP"),
("transp_webp", "Transparent WEBP"),
("transp_webp", "WEBP Transparency"),
("webp_mux", "WEBPMUX"),
("webp_anim", "WEBP Animation"),
("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"),