Use disposal settings from previous frame

This commit is contained in:
Andrew Murray 2020-12-24 09:55:22 +11:00
parent ce3d80e713
commit 5e4e0fa6ee
3 changed files with 83 additions and 59 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

View File

@ -105,6 +105,31 @@ def test_apng_dispose_region():
assert im.getpixel((64, 32)) == (0, 255, 0, 255) 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(): def test_apng_dispose_op_background_p_mode():
with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im:
im.seek(1) im.seek(1)

View File

@ -803,60 +803,76 @@ class PngImageFile(ImageFile.ImageFile):
self.blend_op = self.info.get("blend") self.blend_op = self.info.get("blend")
self.dispose_extent = self.info.get("bbox") self.dispose_extent = self.info.get("bbox")
self.__frame = 0 self.__frame = 0
return
else: else:
if frame != self.__frame + 1: if frame != self.__frame + 1:
raise ValueError(f"cannot seek to frame {frame}") raise ValueError(f"cannot seek to frame {frame}")
# ensure previous frame was loaded # ensure previous frame was loaded
self.load() 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 self.fp = self.__fp
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
try: # advance to the next frame
cid, pos, length = self.png.read() if self.__prepare_idat:
except (struct.error, SyntaxError): ImageFile._safe_read(self.fp, self.__prepare_idat)
break self.__prepare_idat = 0
frame_start = False
while True:
self.fp.read(4) # CRC
if cid == b"IEND": try:
raise EOFError("No more images in APNG file") cid, pos, length = self.png.read()
if cid == b"fcTL": except (struct.error, SyntaxError):
if frame_start: break
# there must be at least one fdAT chunk between fcTL chunks
raise SyntaxError("APNG missing frame data")
frame_start = True
try: if cid == b"IEND":
self.png.call(cid, pos, length) raise EOFError("No more images in APNG file")
except UnicodeDecodeError: if cid == b"fcTL":
break
except EOFError:
if cid == b"fdAT":
length -= 4
if frame_start: if frame_start:
self.__prepare_idat = length # there must be at least one fdAT chunk between fcTL chunks
break raise SyntaxError("APNG missing frame data")
ImageFile._safe_read(self.fp, length) frame_start = True
except AttributeError:
logger.debug("%r %s %s (unknown)", cid, pos, length)
ImageFile._safe_read(self.fp, length)
self.__frame = frame try:
self.tile = self.png.im_tile self.png.call(cid, pos, length)
self.dispose_op = self.info.get("disposal") except UnicodeDecodeError:
self.blend_op = self.info.get("blend") break
self.dispose_extent = self.info.get("bbox") 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)
if not self.tile: self.__frame = frame
raise EOFError 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 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): def tell(self):
return self.__frame return self.__frame
@ -939,19 +955,6 @@ class PngImageFile(ImageFile.ImageFile):
self.png.close() self.png.close()
self.png = None self.png = None
else: 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: if self._prev_im and self.blend_op == APNG_BLEND_OP_OVER:
updated = self._crop(self.im, self.dispose_extent) updated = self._crop(self.im, self.dispose_extent)
self._prev_im.paste( self._prev_im.paste(
@ -960,10 +963,6 @@ class PngImageFile(ImageFile.ImageFile):
self.im = self._prev_im self.im = self._prev_im
if self.pyaccess: if self.pyaccess:
self.pyaccess = None self.pyaccess = None
self._prev_im = self.im.copy()
if dispose:
self._prev_im.paste(dispose, self.dispose_extent)
def _getexif(self): def _getexif(self):
if "exif" not in self.info: if "exif" not in self.info: