diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 16cecd0c4eb..984e1bad417 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -110,3 +110,24 @@ def test_all_frames() -> None: for i, im_frame in enumerate(ims): im.seek(i) assert_image_equal(im.rotate(90), im_frame) + + +def test_all_frames_restores_position_after_seek_error() -> None: + class CustomImage(Image.Image): + def seek(self, frame: int) -> None: + if frame == 3: + msg = "seek failed" + raise ValueError(msg) + self.__frame = frame + self.im = Image.core.new("1", (1, 1)) + + def tell(self) -> int: + return self.__frame + + with CustomImage() as im: + im.seek(1) + + with pytest.raises(ValueError, match="seek failed"): + ImageSequence.all_frames(im) + + assert im.tell() == 1 diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 46dbed3b58b..99913213411 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -135,6 +135,12 @@ immediately at the release of 3.15.0 final (2026-10-01, :pep:`790`). Pillow 13.0.0 now officially supports Python 3.15. +Restore image position if ImageSequence.all_frames() fails +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If processing a frame in :py:func:`~PIL.ImageSequence.all_frames` raises an exception, +the given image will now seek back to its original frame. + Fixed ImageChops.offset() for 16-bit images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 0a6ad66091d..21e9971b2e4 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -82,8 +82,8 @@ def all_frames( ims = [] for imSequence in im: current = imSequence.tell() - - ims += [im_frame.copy() for im_frame in Iterator(imSequence)] - - imSequence.seek(current) + try: + ims += [im_frame.copy() for im_frame in Iterator(imSequence)] + finally: + imSequence.seek(current) return [func(im) for im in ims] if func else ims