Repository: python-pillow/Pillow

Issue:
Reading multi-image TIFF gets wrong frame after seeking backwards
Opening a multi-image tiff file and seeking backwards and then forwards can seek to the wrong frame.  Below is a test case that demonstrates the bug, and a patch that fixes it.

The attached zip file contains tiff_seek_test.py that writes a 3 frame TIFF file and demonstrates that seeking to frames 0, 1, 0, 2 in that order ends with incorrectly being on frame 1 instead of frame 2.  The test sets the pixel values of each frame to match the frame number and reports the pixel values of each frame.

[tiff_seek.zip](https://github.com/python-pillow/Pillow/files/2958894/tiff_seek.zip)

Test was run with Python 3.7 and PIL 5.4.1 on macOS 10.14.2.  PIL 6.0 code has the same faulty TiffImagePlugin.py _seek() code.  The above zip file includes a patch that fixes this bug.

The TiffImagePlugin.py _seek() code incorrectly assumes that the current frame is the last frame in its list of cached frame byte offsets (TiffImageFile._frame_pos).  That assumption is correct if only forward seeking is done, after a backward seek the current frame is not the last one in the cached frame byte offset list, and this results in caching the wrong byte offset after a seek to a frame not in the cached byte offsets.



Discussion:
From the zip, `test_seek_test.py`:

```python
# Create a 3 frame tiff image where each image has color
# equal to frame number and save to test.tif.
import numpy as np
from PIL import Image
imlist = []
for i in range(3):
    a = np.empty((100,100), dtype=np.uint8)
    a[:] = i
    imlist.append(Image.fromarray(a))

imlist[0].save('test.tif', save_all=True, append_images=imlist[1:])

# Open test.tif and read each frame,
# showing that pixel value equals frame number as expected.
im = Image.open('test.tif')
for i in range(3):
    im.seek(i)
    print (i, im.getpixel((0,0)))

# Open test.tif and read frames non-sequentially.
# Frame 2 is reported to have pixel value 1 because
# the final seek to frame 2 incorrectly seeks to frame 1.
im = Image.open('test.tif')
for i in (0,1,0,2):
    im.seek(i)
    print (i, im.getpixel((0,0)))
```

`pillow_tiff_seek_patch`:
```patch
*** PIL/TiffImagePlugin.py.orig	2019-03-11 02:36:51.000000000 -0700
--- PIL/TiffImagePlugin.py	2019-03-12 14:05:56.000000000 -0700
***************
*** 1042,1054 ****
              # reset python3 buffered io handle in case fp
              # was passed to libtiff, invalidating the buffer
              self.fp.tell()
!             self.fp.seek(self.__next)
              self._frame_pos.append(self.__next)
-             if DEBUG:
-                 print("Loading tags, location: %s" % self.fp.tell())
-             self.tag_v2.load(self.fp)
-             self.__next = self.tag_v2.next
-             self.__frame += 1
          self.fp.seek(self._frame_pos[frame])
          self.tag_v2.load(self.fp)
          self.__next = self.tag_v2.next
--- 1042,1059 ----
              # reset python3 buffered io handle in case fp
              # was passed to libtiff, invalidating the buffer
              self.fp.tell()
!             last_frame = len(self._frame_pos) - 1
!             if self.__frame != last_frame:
!                 # Current frame is not the last frame in self._frame_pos list.
!                 # Read last frame in self._frame_pos list to get next frame position
!                 # to add to list.
!                 self.fp.seek(self._frame_pos[last_frame])
!                 if DEBUG:
!                     print("Loading tags, location: %s" % self.fp.tell())
!                 self.tag_v2.load(self.fp)
!                 self.__frame = last_frame
!                 self.__next = self.tag_v2.next
              self._frame_pos.append(self.__next)
          self.fp.seek(self._frame_pos[frame])
          self.tag_v2.load(self.fp)
          self.__next = self.tag_v2.next
```

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1056,7 +1056,6 @@ def _seek(self, frame):
             self.__frame += 1
         self.fp.seek(self._frame_pos[frame])
         self.tag_v2.load(self.fp)
-        self.__next = self.tag_v2.next
         # fill the legacy tag/ifd entries
         self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
         self.__frame = frame
@@ -1087,7 +1086,7 @@ def load(self):
     def load_end(self):
         # allow closing if we're on the first frame, there's no next
         # This is the ImageFile.load path only, libtiff specific below.
-        if self.__frame == 0 and not self.__next:
+        if len(self._frame_pos) == 1 and not self.__next:
             self._close_exclusive_fp_after_loading = True
 
     def _load_libtiff(self):
@@ -1168,7 +1167,7 @@ def _load_libtiff(self):
         self.readonly = 0
         # libtiff closed the fp in a, we need to close self.fp, if possible
         if self._exclusive_fp:
-            if self.__frame == 0 and not self.__next:
+            if len(self._frame_pos) == 1 and not self.__next:
                 self.fp.close()
                 self.fp = None  # might be shared
 


Test patch:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -263,6 +263,11 @@ def test_multipage(self):
         self.assertEqual(im.size, (10, 10))
         self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0))
 
+        im.seek(0)
+        im.load()
+        self.assertEqual(im.size, (10, 10))
+        self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0))
+
         im.seek(2)
         im.load()
         self.assertEqual(im.size, (20, 20))