mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-27 09:44:31 +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 = {
|
_VALID_WEBP_MODES = {
|
||||||
|
"RGBX": True,
|
||||||
|
"RGBA": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
_VALID_WEBP_LEGACY_MODES = {
|
||||||
"RGB": True,
|
"RGB": True,
|
||||||
"RGBA": True,
|
"RGBA": True,
|
||||||
}
|
}
|
||||||
|
@ -28,32 +33,263 @@ class WebPImageFile(ImageFile.ImageFile):
|
||||||
format_description = "WebP image"
|
format_description = "WebP image"
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
data, width, height, self.mode, icc_profile, exif = \
|
if not _webp.HAVE_WEBPANIM:
|
||||||
_webp.WebPDecode(self.fp.read())
|
# 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:
|
if icc_profile:
|
||||||
self.info["icc_profile"] = icc_profile
|
self.info["icc_profile"] = icc_profile
|
||||||
if exif:
|
if exif:
|
||||||
self.info["exif"] = exif
|
self.info["exif"] = exif
|
||||||
|
if xmp:
|
||||||
|
self.info["xmp"] = xmp
|
||||||
|
|
||||||
self.size = width, height
|
# Initialize seek state
|
||||||
self.fp = BytesIO(data)
|
self._reset(reset=False)
|
||||||
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
|
self.seek(0)
|
||||||
|
|
||||||
def _getexif(self):
|
def _getexif(self):
|
||||||
from .JpegImagePlugin import _getexif
|
from .JpegImagePlugin import _getexif
|
||||||
return _getexif(self)
|
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):
|
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)
|
lossless = im.encoderinfo.get("lossless", False)
|
||||||
quality = im.encoderinfo.get("quality", 80)
|
quality = im.encoderinfo.get("quality", 80)
|
||||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||||
exif = im.encoderinfo.get("exif", "")
|
exif = im.encoderinfo.get("exif", "")
|
||||||
|
xmp = im.encoderinfo.get("xmp", "")
|
||||||
|
|
||||||
|
if im.mode not in _VALID_WEBP_LEGACY_MODES:
|
||||||
|
alpha = im.mode == 'P' and 'A' in im.im.getpalettemode()
|
||||||
|
im = im.convert('RGBA' if alpha else 'RGB')
|
||||||
|
|
||||||
data = _webp.WebPEncode(
|
data = _webp.WebPEncode(
|
||||||
im.tobytes(),
|
im.tobytes(),
|
||||||
|
@ -63,16 +299,18 @@ def _save(im, fp, filename):
|
||||||
float(quality),
|
float(quality),
|
||||||
im.mode,
|
im.mode,
|
||||||
icc_profile,
|
icc_profile,
|
||||||
exif
|
exif,
|
||||||
|
xmp
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
raise IOError("cannot write file as WEBP (encoder returned None)")
|
raise IOError("cannot write file as WebP (encoder returned None)")
|
||||||
|
|
||||||
fp.write(data)
|
fp.write(data)
|
||||||
|
|
||||||
|
|
||||||
Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
|
Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
|
||||||
Image.register_save(WebPImageFile.format, _save)
|
Image.register_save(WebPImageFile.format, _save)
|
||||||
|
if _webp.HAVE_WEBPANIM:
|
||||||
|
Image.register_save_all(WebPImageFile.format, _save_all)
|
||||||
Image.register_extension(WebPImageFile.format, ".webp")
|
Image.register_extension(WebPImageFile.format, ".webp")
|
||||||
Image.register_mime(WebPImageFile.format, "image/webp")
|
Image.register_mime(WebPImageFile.format, "image/webp")
|
||||||
|
|
|
@ -43,6 +43,7 @@ def get_supported_codecs():
|
||||||
return [f for f in codecs if check_codec(f)]
|
return [f for f in codecs if check_codec(f)]
|
||||||
|
|
||||||
features = {
|
features = {
|
||||||
|
"webp_anim": ("PIL._webp", 'HAVE_WEBPANIM'),
|
||||||
"webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'),
|
"webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'),
|
||||||
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
|
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
|
||||||
"raqm": ("PIL._imagingft", "HAVE_RAQM")
|
"raqm": ("PIL._imagingft", "HAVE_RAQM")
|
||||||
|
@ -53,7 +54,7 @@ def check_feature(feature):
|
||||||
raise ValueError("Unknown feature %s" % feature)
|
raise ValueError("Unknown feature %s" % feature)
|
||||||
|
|
||||||
module, flag = features[feature]
|
module, flag = features[feature]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
imported_module = __import__(module, fromlist=['PIL'])
|
imported_module = __import__(module, fromlist=['PIL'])
|
||||||
return getattr(imported_module, flag)
|
return getattr(imported_module, flag)
|
||||||
|
@ -75,4 +76,4 @@ def get_supported():
|
||||||
ret.extend(get_supported_features())
|
ret.extend(get_supported_features())
|
||||||
ret.extend(get_supported_codecs())
|
ret.extend(get_supported_codecs())
|
||||||
return ret
|
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'),
|
self.assertEqual(features.check('webp_mux'),
|
||||||
_webp.HAVE_WEBPMUX)
|
_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):
|
def test_check_modules(self):
|
||||||
for feature in features.modules:
|
for feature in features.modules:
|
||||||
self.assertIn(features.check_module(feature), [True, False])
|
self.assertIn(features.check_module(feature), [True, False])
|
||||||
|
|
|
@ -4,29 +4,35 @@ from PIL import Image
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import _webp
|
from PIL import _webp
|
||||||
|
HAVE_WEBP = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Skip in setUp()
|
HAVE_WEBP = False
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestFileWebp(PillowTestCase):
|
class TestFileWebp(PillowTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
try:
|
if not HAVE_WEBP:
|
||||||
from PIL import _webp
|
|
||||||
except ImportError:
|
|
||||||
self.skipTest('WebP support not installed')
|
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):
|
def test_version(self):
|
||||||
_webp.WebPDecoderVersion()
|
_webp.WebPDecoderVersion()
|
||||||
_webp.WebPDecoderBuggyAlpha()
|
_webp.WebPDecoderBuggyAlpha()
|
||||||
|
|
||||||
def test_read_rgb(self):
|
def test_read_rgb(self):
|
||||||
|
"""
|
||||||
|
Can we read a RGB mode WebP file without error?
|
||||||
|
Does it have the bits we expect?
|
||||||
|
"""
|
||||||
|
|
||||||
file_path = "Tests/images/hopper.webp"
|
file_path = "Tests/images/hopper.webp"
|
||||||
image = Image.open(file_path)
|
image = Image.open(file_path)
|
||||||
|
|
||||||
self.assertEqual(image.mode, "RGB")
|
self.assertEqual(image.mode, self.rgb_mode)
|
||||||
self.assertEqual(image.size, (128, 128))
|
self.assertEqual(image.size, (128, 128))
|
||||||
self.assertEqual(image.format, "WEBP")
|
self.assertEqual(image.format, "WEBP")
|
||||||
image.load()
|
image.load()
|
||||||
|
@ -35,6 +41,7 @@ class TestFileWebp(PillowTestCase):
|
||||||
# generated with:
|
# generated with:
|
||||||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||||
target = Image.open('Tests/images/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)
|
self.assert_image_similar(image, target, 20.0)
|
||||||
|
|
||||||
def test_write_rgb(self):
|
def test_write_rgb(self):
|
||||||
|
@ -45,12 +52,10 @@ class TestFileWebp(PillowTestCase):
|
||||||
|
|
||||||
temp_file = self.tempfile("temp.webp")
|
temp_file = self.tempfile("temp.webp")
|
||||||
|
|
||||||
hopper("RGB").save(temp_file)
|
hopper(self.rgb_mode).save(temp_file)
|
||||||
|
|
||||||
image = Image.open(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.size, (128, 128))
|
||||||
self.assertEqual(image.format, "WEBP")
|
self.assertEqual(image.format, "WEBP")
|
||||||
image.load()
|
image.load()
|
||||||
|
@ -69,20 +74,67 @@ class TestFileWebp(PillowTestCase):
|
||||||
# then we're going to accept that it's a reasonable lossy version of
|
# 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
|
# the image. The old lena images for WebP are showing ~16 on
|
||||||
# Ubuntu, the jpegs are showing ~18.
|
# Ubuntu, the jpegs are showing ~18.
|
||||||
target = hopper("RGB")
|
target = hopper(self.rgb_mode)
|
||||||
self.assert_image_similar(image, target, 12)
|
self.assert_image_similar(image, target, 12.0)
|
||||||
|
|
||||||
|
def test_write_unsupported_mode_L(self):
|
||||||
|
"""
|
||||||
|
Saving a black-and-white file to WebP format should work, and be
|
||||||
|
similar to the original file.
|
||||||
|
"""
|
||||||
|
|
||||||
def test_write_unsupported_mode(self):
|
|
||||||
temp_file = self.tempfile("temp.webp")
|
temp_file = self.tempfile("temp.webp")
|
||||||
|
hopper("L").save(temp_file)
|
||||||
|
image = Image.open(temp_file)
|
||||||
|
|
||||||
im = hopper("L")
|
self.assertEqual(image.mode, self.rgb_mode)
|
||||||
self.assertRaises(IOError, im.save, temp_file)
|
self.assertEqual(image.size, (128, 128))
|
||||||
|
self.assertEqual(image.format, "WEBP")
|
||||||
|
|
||||||
|
image.load()
|
||||||
|
image.getdata()
|
||||||
|
target = hopper("L").convert(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):
|
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)
|
self.assertRaises(TypeError, _webp.WebPEncode)
|
||||||
|
|
||||||
def test_WebPDecode_with_invalid_args(self):
|
def test_WebPDecode_with_invalid_args(self):
|
||||||
|
"""
|
||||||
|
Calling decoder functions with no arguments should result in an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if _webp.HAVE_WEBPANIM:
|
||||||
|
self.assertRaises(TypeError, _webp.WebPAnimDecoder)
|
||||||
self.assertRaises(TypeError, _webp.WebPDecode)
|
self.assertRaises(TypeError, _webp.WebPDecode)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -22,6 +22,11 @@ class TestFileWebpAlpha(PillowTestCase):
|
||||||
"not testing transparency")
|
"not testing transparency")
|
||||||
|
|
||||||
def test_read_rgba(self):
|
def test_read_rgba(self):
|
||||||
|
"""
|
||||||
|
Can we read an RGBA mode file without error?
|
||||||
|
Does it have the bits we expect?
|
||||||
|
"""
|
||||||
|
|
||||||
# Generated with `cwebp transparent.png -o transparent.webp`
|
# Generated with `cwebp transparent.png -o transparent.webp`
|
||||||
file_path = "Tests/images/transparent.webp"
|
file_path = "Tests/images/transparent.webp"
|
||||||
image = Image.open(file_path)
|
image = Image.open(file_path)
|
||||||
|
@ -38,6 +43,11 @@ class TestFileWebpAlpha(PillowTestCase):
|
||||||
self.assert_image_similar(image, target, 20.0)
|
self.assert_image_similar(image, target, 20.0)
|
||||||
|
|
||||||
def test_write_lossless_rgb(self):
|
def test_write_lossless_rgb(self):
|
||||||
|
"""
|
||||||
|
Can we write an RGBA mode file with lossless compression without
|
||||||
|
error? Does it have the bits we expect?
|
||||||
|
"""
|
||||||
|
|
||||||
temp_file = self.tempfile("temp.webp")
|
temp_file = self.tempfile("temp.webp")
|
||||||
# temp_file = "temp.webp"
|
# temp_file = "temp.webp"
|
||||||
|
|
||||||
|
@ -90,6 +100,27 @@ class TestFileWebpAlpha(PillowTestCase):
|
||||||
else:
|
else:
|
||||||
self.assert_image_similar(image, pil_image, 1.0)
|
self.assert_image_similar(image, pil_image, 1.0)
|
||||||
|
|
||||||
|
def test_write_unsupported_mode_PA(self):
|
||||||
|
"""
|
||||||
|
Saving a palette-based file with transparency to WebP format
|
||||||
|
should work, and be similar to the original file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
temp_file = self.tempfile("temp.webp")
|
||||||
|
file_path = "Tests/images/transparent.gif"
|
||||||
|
Image.open(file_path).save(temp_file)
|
||||||
|
image = Image.open(temp_file)
|
||||||
|
|
||||||
|
self.assertEqual(image.mode, "RGBA")
|
||||||
|
self.assertEqual(image.size, (200, 150))
|
||||||
|
self.assertEqual(image.format, "WEBP")
|
||||||
|
|
||||||
|
image.load()
|
||||||
|
image.getdata()
|
||||||
|
target = Image.open(file_path).convert("RGBA")
|
||||||
|
|
||||||
|
self.assert_image_similar(image, target, 10.0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
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:
|
try:
|
||||||
from PIL import _webp
|
from PIL import _webp
|
||||||
|
HAVE_WEBP = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
HAVE_WEBP = False
|
||||||
# Skip in setUp()
|
|
||||||
|
|
||||||
|
|
||||||
class TestFileWebpLossless(PillowTestCase):
|
class TestFileWebpLossless(PillowTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
try:
|
if not HAVE_WEBP:
|
||||||
from PIL import _webp
|
|
||||||
except:
|
|
||||||
self.skipTest('WebP support not installed')
|
self.skipTest('WebP support not installed')
|
||||||
|
return
|
||||||
|
|
||||||
if (_webp.WebPDecoderVersion() < 0x0200):
|
if (_webp.WebPDecoderVersion() < 0x0200):
|
||||||
self.skipTest('lossless not included')
|
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):
|
def test_write_lossless_rgb(self):
|
||||||
temp_file = self.tempfile("temp.webp")
|
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 = Image.open(temp_file)
|
||||||
image.load()
|
image.load()
|
||||||
|
|
||||||
self.assertEqual(image.mode, "RGB")
|
self.assertEqual(image.mode, self.rgb_mode)
|
||||||
self.assertEqual(image.size, (128, 128))
|
self.assertEqual(image.size, (128, 128))
|
||||||
self.assertEqual(image.format, "WEBP")
|
self.assertEqual(image.format, "WEBP")
|
||||||
image.load()
|
image.load()
|
||||||
image.getdata()
|
image.getdata()
|
||||||
|
|
||||||
self.assert_image_equal(image, hopper("RGB"))
|
self.assert_image_equal(image, hopper(self.rgb_mode))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -2,13 +2,17 @@ from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import _webp
|
||||||
|
HAVE_WEBP = True
|
||||||
|
except ImportError:
|
||||||
|
HAVE_WEBP = False
|
||||||
|
|
||||||
|
|
||||||
class TestFileWebpMetadata(PillowTestCase):
|
class TestFileWebpMetadata(PillowTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
try:
|
if not HAVE_WEBP:
|
||||||
from PIL import _webp
|
|
||||||
except ImportError:
|
|
||||||
self.skipTest('WebP support not installed')
|
self.skipTest('WebP support not installed')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -107,6 +111,29 @@ class TestFileWebpMetadata(PillowTestCase):
|
||||||
|
|
||||||
self.assertFalse(webp_image._getexif())
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
633
_webp.c
633
_webp.c
|
@ -8,27 +8,551 @@
|
||||||
|
|
||||||
#ifdef HAVE_WEBPMUX
|
#ifdef HAVE_WEBPMUX
|
||||||
#include <webp/mux.h>
|
#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
|
||||||
|
|
||||||
|
#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)
|
PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
{
|
{
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
int lossless;
|
int lossless;
|
||||||
float quality_factor;
|
float quality_factor;
|
||||||
uint8_t *rgb;
|
uint8_t* rgb;
|
||||||
uint8_t *icc_bytes;
|
uint8_t* icc_bytes;
|
||||||
uint8_t *exif_bytes;
|
uint8_t* exif_bytes;
|
||||||
uint8_t *output;
|
uint8_t* xmp_bytes;
|
||||||
char *mode;
|
uint8_t* output;
|
||||||
|
char* mode;
|
||||||
Py_ssize_t size;
|
Py_ssize_t size;
|
||||||
Py_ssize_t icc_size;
|
Py_ssize_t icc_size;
|
||||||
Py_ssize_t exif_size;
|
Py_ssize_t exif_size;
|
||||||
|
Py_ssize_t xmp_size;
|
||||||
size_t ret_size;
|
size_t ret_size;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"iiifss#s#",
|
if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"iiifss#s#s#",
|
||||||
(char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode,
|
(char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode,
|
||||||
&icc_bytes, &icc_size, &exif_bytes, &exif_size)) {
|
&icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (strcmp(mode, "RGBA")==0){
|
if (strcmp(mode, "RGBA")==0){
|
||||||
|
@ -37,11 +561,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
}
|
}
|
||||||
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
|
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
|
||||||
if (lossless) {
|
if (lossless) {
|
||||||
ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4* width, &output);
|
ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output);
|
||||||
} else
|
} else
|
||||||
#endif
|
#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){
|
} else if (strcmp(mode, "RGB")==0){
|
||||||
if (size < width * height * 3){
|
if (size < width * height * 3){
|
||||||
|
@ -49,11 +573,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
}
|
}
|
||||||
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
|
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
|
||||||
if (lossless) {
|
if (lossless) {
|
||||||
ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3* width, &output);
|
ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output);
|
||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
ret_size = WebPEncodeRGB(rgb, width, height, 3* width, quality_factor, &output);
|
ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
@ -66,17 +590,19 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
#else
|
#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. 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)
|
data in the upper byte. (Not sure why, it shouldn't have been there)
|
||||||
*/
|
*/
|
||||||
int i_icc_size = (int)icc_size;
|
int i_icc_size = (int)icc_size;
|
||||||
int i_exif_size = (int)exif_size;
|
int i_exif_size = (int)exif_size;
|
||||||
|
int i_xmp_size = (int)xmp_size;
|
||||||
WebPData output_data = {0};
|
WebPData output_data = {0};
|
||||||
WebPData image = { output, ret_size };
|
WebPData image = { output, ret_size };
|
||||||
WebPData icc_profile = { icc_bytes, i_icc_size };
|
WebPData icc_profile = { icc_bytes, i_icc_size };
|
||||||
WebPData exif = { exif_bytes, i_exif_size };
|
WebPData exif = { exif_bytes, i_exif_size };
|
||||||
|
WebPData xmp = { xmp_bytes, i_xmp_size };
|
||||||
WebPMuxError err;
|
WebPMuxError err;
|
||||||
int dbg = 0;
|
int dbg = 0;
|
||||||
|
|
||||||
|
@ -93,13 +619,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
|
|
||||||
if (i_icc_size > 0) {
|
if (i_icc_size > 0) {
|
||||||
if (dbg) {
|
if (dbg) {
|
||||||
fprintf (stderr, "Adding ICC Profile\n");
|
fprintf(stderr, "Adding ICC Profile\n");
|
||||||
}
|
}
|
||||||
err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data);
|
err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data);
|
||||||
if (dbg && err == WEBP_MUX_INVALID_ARGUMENT) {
|
if (err != WEBP_MUX_OK) {
|
||||||
fprintf(stderr, "Invalid ICC Argument\n");
|
return HandleMuxError(err, "ICCP");
|
||||||
} else if (dbg && err == WEBP_MUX_MEMORY_ERROR) {
|
|
||||||
fprintf(stderr, "ICC Memory Error\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,14 +631,25 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
fprintf(stderr, "exif size %d \n", i_exif_size);
|
fprintf(stderr, "exif size %d \n", i_exif_size);
|
||||||
}
|
}
|
||||||
if (i_exif_size > 0) {
|
if (i_exif_size > 0) {
|
||||||
if (dbg){
|
if (dbg) {
|
||||||
fprintf (stderr, "Adding Exif Data\n");
|
fprintf(stderr, "Adding Exif Data\n");
|
||||||
}
|
}
|
||||||
err = WebPMuxSetChunk(mux, "EXIF", &exif, copy_data);
|
err = WebPMuxSetChunk(mux, "EXIF", &exif, copy_data);
|
||||||
if (dbg && err == WEBP_MUX_INVALID_ARGUMENT) {
|
if (err != WEBP_MUX_OK) {
|
||||||
fprintf(stderr, "Invalid Exif Argument\n");
|
return HandleMuxError(err, "EXIF");
|
||||||
} else if (dbg && err == WEBP_MUX_MEMORY_ERROR) {
|
}
|
||||||
fprintf(stderr, "Exif Memory Error\n");
|
}
|
||||||
|
|
||||||
|
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;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
|
PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
|
||||||
{
|
{
|
||||||
PyBytesObject *webp_string;
|
PyBytesObject* webp_string;
|
||||||
const uint8_t *webp;
|
const uint8_t* webp;
|
||||||
Py_ssize_t size;
|
Py_ssize_t size;
|
||||||
PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL;
|
PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL;
|
||||||
WebPDecoderConfig config;
|
WebPDecoderConfig config;
|
||||||
|
@ -152,7 +686,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
|
||||||
Py_RETURN_NONE;
|
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);
|
vp8_status_code = WebPGetFeatures(webp, size, &config.input);
|
||||||
if (vp8_status_code == VP8_STATUS_OK) {
|
if (vp8_status_code == VP8_STATUS_OK) {
|
||||||
|
@ -204,12 +738,12 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
|
||||||
goto end;
|
goto end;
|
||||||
|
|
||||||
if (config.output.colorspace < MODE_YUV) {
|
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);
|
config.output.u.RGBA.size);
|
||||||
} else {
|
} else {
|
||||||
// Skipping YUV for now. Need Test Images.
|
// Skipping YUV for now. Need Test Images.
|
||||||
// UNDONE -- unclear if we'll ever get here if we set mode_rgb*
|
// 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);
|
config.output.u.YUVA.y_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,8 +789,16 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){
|
||||||
return Py_BuildValue("i", WebPDecoderBuggyAlpha());
|
return Py_BuildValue("i", WebPDecoderBuggyAlpha());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Module Setup */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
|
||||||
static PyMethodDef webpMethods[] =
|
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"},
|
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
|
||||||
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
|
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
|
||||||
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
|
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
|
||||||
|
@ -272,11 +814,32 @@ void addMuxFlagToModule(PyObject* m) {
|
||||||
#endif
|
#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) {
|
void addTransparencyFlagToModule(PyObject* m) {
|
||||||
PyModule_AddObject(m, "HAVE_TRANSPARENCY",
|
PyModule_AddObject(m, "HAVE_TRANSPARENCY",
|
||||||
PyBool_FromLong(!WebPDecoderBuggyAlpha()));
|
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
|
#if PY_VERSION_HEX >= 0x03000000
|
||||||
PyMODINIT_FUNC
|
PyMODINIT_FUNC
|
||||||
|
@ -292,8 +855,9 @@ PyInit__webp(void) {
|
||||||
};
|
};
|
||||||
|
|
||||||
m = PyModule_Create(&module_def);
|
m = PyModule_Create(&module_def);
|
||||||
addMuxFlagToModule(m);
|
if (setup_module(m) < 0)
|
||||||
addTransparencyFlagToModule(m);
|
return NULL;
|
||||||
|
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
@ -301,7 +865,6 @@ PyMODINIT_FUNC
|
||||||
init_webp(void)
|
init_webp(void)
|
||||||
{
|
{
|
||||||
PyObject* m = Py_InitModule("_webp", webpMethods);
|
PyObject* m = Py_InitModule("_webp", webpMethods);
|
||||||
addMuxFlagToModule(m);
|
setup_module(m);
|
||||||
addTransparencyFlagToModule(m);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -110,7 +110,7 @@ are available::
|
||||||
**append_images**
|
**append_images**
|
||||||
A list of images to append as additional frames. Each of the
|
A list of images to append as additional frames. Each of the
|
||||||
images in the list can be single or multiframe images.
|
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**
|
**duration**
|
||||||
The display duration of each frame of the multiframe gif, in
|
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:
|
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
|
|
||||||
**lossless**
|
**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**
|
**quality**
|
||||||
Integer, 1-100, Defaults to 80. Sets the quality level for
|
Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest
|
||||||
lossy compression.
|
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**
|
**icc_procfile**
|
||||||
The ICC Profile to include in the saved file. Only supported if
|
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**
|
**exif**
|
||||||
The exif data to include in the saved file. Only supported if
|
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
|
XBM
|
||||||
^^^
|
^^^
|
||||||
|
|
|
@ -178,8 +178,9 @@ if __name__ == "__main__":
|
||||||
("freetype2", "FREETYPE2"),
|
("freetype2", "FREETYPE2"),
|
||||||
("littlecms2", "LITTLECMS2"),
|
("littlecms2", "LITTLECMS2"),
|
||||||
("webp", "WEBP"),
|
("webp", "WEBP"),
|
||||||
("transp_webp", "Transparent WEBP"),
|
("transp_webp", "WEBP Transparency"),
|
||||||
("webp_mux", "WEBPMUX"),
|
("webp_mux", "WEBPMUX"),
|
||||||
|
("webp_anim", "WEBP Animation"),
|
||||||
("jpg", "JPEG"),
|
("jpg", "JPEG"),
|
||||||
("jpg_2000", "OPENJPEG (JPEG2000)"),
|
("jpg_2000", "OPENJPEG (JPEG2000)"),
|
||||||
("zlib", "ZLIB (PNG/ZIP)"),
|
("zlib", "ZLIB (PNG/ZIP)"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user