mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-11-04 01:47:47 +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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											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