Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GifImagePlugin seek-related optimizations #6075

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ def test_seek():
img.seek(img.tell() + 1)
except EOFError:
assert frame_count == 5
assert img._n_frames == frame_count


def test_seek_info():
Expand Down
28 changes: 28 additions & 0 deletions docs/releasenotes/9.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,34 @@ can be used to return data in the current mode of the palette.
Other Changes
=============

GifImagePlugin seek-related optimizations
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Optimized :py:meth:`~PIL.GifImagePlugin.seek()`,
:py:property:`~PIL.GifImagePlugin.n_frames` and
:py:property:`~PIL.GifImagePlugin.is_animated` when seeking back to the *current* frame
before invoking the operation, after an ``EOFError`` is raised.

These operations no longer have to seek back to **frame 0** before seeking forward,
instead they seek back to the frame just before the *current* frame and then seek
just one frame forward.

Eliminated seek checks while computing :py:property:`~PIL.GifImagePlugin.n_frames`
by using :py:meth:`PIL.GifImagePlugin._seek()` in place of
:py:meth:`~PIL.GifImagePlugin.seek()` since the frame numbers cannot be invalid until
the final one, which raises ``EOFError``.

When an ``EOFError`` is raised within :py:meth:`~PIL.GifImagePlugin.seek()` i.e just
seeked past the last frame, ``self._n_frames`` is updated if it hadn't been updated
earlier, to prevent re-computation when :py:property:`~PIL.GifImagePlugin.n_frames` is
later invoked.

Added ``self.__prev_offset`` to always hold the offset of the frame just before the
current frame.

Replaced all internal calls to :py:meth:`~PIL.GifImagePlugin.tell()` with direct
references to ``self.__frame``.

ImageShow temporary files on Unix
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
27 changes: 20 additions & 7 deletions src/PIL/GifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,17 @@ def _open(self):
@property
def n_frames(self):
if self._n_frames is None:
current = self.tell()
prev_offset = self.__prev_offset
current = self.__frame
try:
while True:
self.seek(self.tell() + 1)
self._seek(self.__frame + 1)
except EOFError:
self._n_frames = self.tell() + 1
self.seek(current)
self._n_frames = self.__frame

self.__offset = prev_offset
self.__frame = current - 1
self._seek(current)
return self._n_frames

@property
Expand All @@ -109,15 +113,18 @@ def is_animated(self):
if self._n_frames is not None:
self._is_animated = self._n_frames != 1
else:
current = self.tell()
prev_offset = self.__prev_offset
current = self.__frame

try:
self.seek(1)
self._is_animated = True
except EOFError:
self._is_animated = False

self.seek(current)
self.__offset = prev_offset
self.__frame = current - 1
self._seek(current)
return self._is_animated

def seek(self, frame):
Expand All @@ -127,12 +134,17 @@ def seek(self, frame):
self.im = None
self._seek(0)

prev_offset = self.__prev_offset
last_frame = self.__frame
for f in range(self.__frame + 1, frame + 1):
try:
self._seek(f)
except EOFError as e:
self.seek(last_frame)
if not self._n_frames:
self._n_frames = f
self.__offset = prev_offset
self.__frame = last_frame - 1
self._seek(last_frame)
raise EOFError("no more images in GIF file") from e

def _seek(self, frame):
Expand All @@ -157,6 +169,7 @@ def _seek(self, frame):
self.tile = []

self.fp = self.__fp
self.__prev_offset = self.__offset
if self.__offset:
# backup to last frame
self.fp.seek(self.__offset)
Expand Down