mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-26 09:14:27 +03:00
Merge pull request #2761 from monolithlabs/animated_webp
Add support for animated WebP files
This commit is contained in:
commit
a3a9faf3a2
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
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 |
|
@ -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])
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
145
Tests/test_file_webp_animated.py
Normal file
145
Tests/test_file_webp_animated.py
Normal 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()
|
|
@ -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__':
|
||||
|
|
|
@ -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
633
_webp.c
|
@ -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, ×tamp, &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, ×tamp)) {
|
||||
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
|
||||
|
|
|
@ -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
|
||||
^^^
|
||||
|
|
|
@ -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)"),
|
||||
|
|
Loading…
Reference in New Issue
Block a user