Repository: python-pillow/Pillow

Issue:
APNG conversion issue
Hey,

I tried to convert an APNG with Pillow and got a bad looking output. It seems that WEBP saving works a bit better but there are also problems. This issue is about converting an APNG into an APNG while having the same watermarking/thumbnailing as in #4644. I've attached a self-contained test code [apng-test.zip](https://github.com/python-pillow/Pillow/files/4705464/apng-test.zip) to reproduce the problem.

The used APNG is downloaded from https://en.wikipedia.org/wiki/APNG, the wikipedia article about APNG formats.

Hope to get this fixed soon, because I have to convert all APNG-s into WEBPs to be able to look half decent.


Discussion:
While I recognise the output is still not good, on a practical level, I found that if you remove `disposals` from your save arguments, the disappearing effect is fixed.

![out](https://user-images.githubusercontent.com/3112309/83326226-7b526880-a2b5-11ea-8b73-cd34b71a9bbc.png)
Interesting. Can you post the adjusted code please? I thought the `disposal` argument in the `save_kwargs` is that one.
Oh, I misread, my bad. It's actually *removing* that argument that helps? Odd, one would think you'd have to add that back to the save parameters.
Even if you remove the watermarking, you'll get a bad looking result after conversion:
![out](https://user-images.githubusercontent.com/987055/83326462-8afd8d00-a274-11ea-9e76-90da804fab80.png)


Resolving patch:
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -803,60 +803,76 @@ def _seek(self, frame, rewind=False):
             self.blend_op = self.info.get("blend")
             self.dispose_extent = self.info.get("bbox")
             self.__frame = 0
-            return
         else:
             if frame != self.__frame + 1:
                 raise ValueError(f"cannot seek to frame {frame}")
 
-        # ensure previous frame was loaded
-        self.load()
+            # ensure previous frame was loaded
+            self.load()
 
-        self.fp = self.__fp
+            if self.dispose:
+                self.im.paste(self.dispose, self.dispose_extent)
+            self._prev_im = self.im.copy()
 
-        # advance to the next frame
-        if self.__prepare_idat:
-            ImageFile._safe_read(self.fp, self.__prepare_idat)
-            self.__prepare_idat = 0
-        frame_start = False
-        while True:
-            self.fp.read(4)  # CRC
+            self.fp = self.__fp
 
-            try:
-                cid, pos, length = self.png.read()
-            except (struct.error, SyntaxError):
-                break
+            # advance to the next frame
+            if self.__prepare_idat:
+                ImageFile._safe_read(self.fp, self.__prepare_idat)
+                self.__prepare_idat = 0
+            frame_start = False
+            while True:
+                self.fp.read(4)  # CRC
 
-            if cid == b"IEND":
-                raise EOFError("No more images in APNG file")
-            if cid == b"fcTL":
-                if frame_start:
-                    # there must be at least one fdAT chunk between fcTL chunks
-                    raise SyntaxError("APNG missing frame data")
-                frame_start = True
+                try:
+                    cid, pos, length = self.png.read()
+                except (struct.error, SyntaxError):
+                    break
 
-            try:
-                self.png.call(cid, pos, length)
-            except UnicodeDecodeError:
-                break
-            except EOFError:
-                if cid == b"fdAT":
-                    length -= 4
+                if cid == b"IEND":
+                    raise EOFError("No more images in APNG file")
+                if cid == b"fcTL":
                     if frame_start:
-                        self.__prepare_idat = length
-                        break
-                ImageFile._safe_read(self.fp, length)
-            except AttributeError:
-                logger.debug("%r %s %s (unknown)", cid, pos, length)
-                ImageFile._safe_read(self.fp, length)
+                        # there must be at least one fdAT chunk between fcTL chunks
+                        raise SyntaxError("APNG missing frame data")
+                    frame_start = True
 
-        self.__frame = frame
-        self.tile = self.png.im_tile
-        self.dispose_op = self.info.get("disposal")
-        self.blend_op = self.info.get("blend")
-        self.dispose_extent = self.info.get("bbox")
+                try:
+                    self.png.call(cid, pos, length)
+                except UnicodeDecodeError:
+                    break
+                except EOFError:
+                    if cid == b"fdAT":
+                        length -= 4
+                        if frame_start:
+                            self.__prepare_idat = length
+                            break
+                    ImageFile._safe_read(self.fp, length)
+                except AttributeError:
+                    logger.debug("%r %s %s (unknown)", cid, pos, length)
+                    ImageFile._safe_read(self.fp, length)
+
+            self.__frame = frame
+            self.tile = self.png.im_tile
+            self.dispose_op = self.info.get("disposal")
+            self.blend_op = self.info.get("blend")
+            self.dispose_extent = self.info.get("bbox")
+
+            if not self.tile:
+                raise EOFError
+
+        # setup frame disposal (actual disposal done when needed in the next _seek())
+        if self._prev_im is None and self.dispose_op == APNG_DISPOSE_OP_PREVIOUS:
+            self.dispose_op = APNG_DISPOSE_OP_BACKGROUND
 
-        if not self.tile:
-            raise EOFError
+        if self.dispose_op == APNG_DISPOSE_OP_PREVIOUS:
+            self.dispose = self._prev_im.copy()
+            self.dispose = self._crop(self.dispose, self.dispose_extent)
+        elif self.dispose_op == APNG_DISPOSE_OP_BACKGROUND:
+            self.dispose = Image.core.fill(self.mode, self.size)
+            self.dispose = self._crop(self.dispose, self.dispose_extent)
+        else:
+            self.dispose = None
 
     def tell(self):
         return self.__frame
@@ -939,19 +955,6 @@ def load_end(self):
             self.png.close()
             self.png = None
         else:
-            # setup frame disposal (actual disposal done when needed in _seek())
-            if self._prev_im is None and self.dispose_op == APNG_DISPOSE_OP_PREVIOUS:
-                self.dispose_op = APNG_DISPOSE_OP_BACKGROUND
-
-            if self.dispose_op == APNG_DISPOSE_OP_PREVIOUS:
-                dispose = self._prev_im.copy()
-                dispose = self._crop(dispose, self.dispose_extent)
-            elif self.dispose_op == APNG_DISPOSE_OP_BACKGROUND:
-                dispose = Image.core.fill(self.im.mode, self.size)
-                dispose = self._crop(dispose, self.dispose_extent)
-            else:
-                dispose = None
-
             if self._prev_im and self.blend_op == APNG_BLEND_OP_OVER:
                 updated = self._crop(self.im, self.dispose_extent)
                 self._prev_im.paste(
@@ -960,10 +963,6 @@ def load_end(self):
                 self.im = self._prev_im
                 if self.pyaccess:
                     self.pyaccess = None
-            self._prev_im = self.im.copy()
-
-            if dispose:
-                self._prev_im.paste(dispose, self.dispose_extent)
 
     def _getexif(self):
         if "exif" not in self.info:


Test patch:
diff --git a/Tests/images/apng/dispose_op_previous_frame.png b/Tests/images/apng/dispose_op_previous_frame.png
new file mode 100644
Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_frame.png differ
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -105,6 +105,31 @@ def test_apng_dispose_region():
         assert im.getpixel((64, 32)) == (0, 255, 0, 255)
 
 
+def test_apng_dispose_op_previous_frame():
+    # Test that the dispose settings being used are from the previous frame
+    #
+    # Image created with:
+    # red = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
+    # green = red.copy()
+    # green.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255)))
+    # blue = red.copy()
+    # blue.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255)), (64, 32))
+    #
+    # red.save(
+    #     "Tests/images/apng/dispose_op_previous_frame.png",
+    #     save_all=True,
+    #     append_images=[green, blue],
+    #     disposal=[
+    #         PngImagePlugin.APNG_DISPOSE_OP_NONE,
+    #         PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
+    #         PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS
+    #     ],
+    # )
+    with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
+        im.seek(im.n_frames - 1)
+        assert im.getpixel((0, 0)) == (255, 0, 0, 255)
+
+
 def test_apng_dispose_op_background_p_mode():
     with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im:
         im.seek(1)