Repository: python-pillow/Pillow

Issue:
Using seek to skip more than one frame with FliImageFile only shows the pixels changed that frame
### What did you do?
I opened a FLI file and used .seek(50) on the image before creating a PhotoImage to display it on a tix label.

### What did you expect to happen?
I expected to see the complete image.

### What actually happened?
I only saw the part of the image that had changed for that particular frame. The rest of the image is black.

### What versions of Pillow and Python are you using?
Python 3.6.2 on Windows 7 x64
Pillow: 4.2.1

I did find that if I hack in a call to self.load() in FliImageFile's _seek() method, the frame displays fully. I don't know if this is the best way to fix the issue.

```python
import PIL as pil
from PIL import Image,ImageTk,FliImagePlugin
import tkinter.tix as tix

class FliImageFile(FliImagePlugin.FliImageFile):
    def _seek(self, frame):
        FliImagePlugin.FliImageFile._seek(self, frame)
        # ensure that this frame is loaded
        self.load()

def createlabel(root, filename):
    label = tix.Label(root)
    label.original = Image.open(filename)
    label.original.seek(50) # Go to frame 50.
    label.photoimage = ImageTk.PhotoImage(label.original) # keep a reference!
    label.config(image=label.photoimage)
    return label

def main():
    root = tix.Tk()
    label1 = createlabel(root, 'a.fli')
    label1.pack()
    # Hack to replace PIL's FliImageFile with one that loads image data at
    # the end of each internal _seek() call.
    Image.OPEN[FliImagePlugin.FliImageFile.format] = (FliImageFile, Image.OPEN[FliImagePlugin.FliImageFile.format][1])
    label2 = createlabel(root, 'a.fli')
    label2.pack()
    root.mainloop()

main()
```
Using a.fli found at https://samples.libav.org/fli-flc/

Top image is what Pillow displays as-is. The bottom image uses my hack that loads the image at the end of _seek.

![fli-bug](https://user-images.githubusercontent.com/10321049/28949712-9eb3f24a-788c-11e7-8780-6931c971724d.png)


Discussion:
Minimal example to reproduce:
```python
>>> from PIL import Image
>>> im = Image.open("a.fli")
>>> im.seek(50)
>>> im.show()
```
![image](https://user-images.githubusercontent.com/1324225/28963145-b9c8d634-7910-11e7-85f7-63e68ecb410f.png)


This works:
```python
>>> im2 = Image.open("a.fli")
>>> im2.load()
<PixelAccess object at 0x10f6d5c30>
>>> im2.seek(50)
>>> im2.show()
```
![image](https://user-images.githubusercontent.com/1324225/28963116-9cc42ee4-7910-11e7-81d8-fdda6366e283.png)

---

`load` isn't usually done as part of `open`, [docs](https://pillow.readthedocs.io/en/4.2.x/reference/Image.html#PIL.Image.Image.load):

> ### `PIL.Image.open(fp, mode='r')`
> Opens and identifies the given image file.
>
> This is a lazy operation; this function identifies the file, but the file remains open and the actual image data is not read from the file until you try to process the data (or call the load() method). See new().

...

> ### `Image.load()`
> 
> Allocates storage for the image and loads the pixel data. In normal cases, you don’t need to call this method, since the Image class automatically loads an opened image when it is accessed for the first time. This method will close the file associated with the image.

And some other plugins don't `load` after seek, but [GIF does](https://github.com/python-pillow/Pillow/blob/a4dafe78df5fa050131bddac0b2378d11ea8d9b8/PIL/GifImagePlugin.py#L144).
@hugovk It doesn't though, look at the differences in the file. The shoe is not complete in the one that you posted. I think this may need to do the same thing that GIF does because of delta compression, in that a seek to n+1 is going to require all of the 0..n frames to be loaded first. 
Here's frame 50 exported with gimp:

![a fli-frame50](https://user-images.githubusercontent.com/1324225/28968156-5e557778-7927-11e7-822b-c805fee077f9.png)

@hugovk, @wiredfool : (Referring to the images in the second post) The shoe is incomplete and the item in the center of the conveyor belt shouldn't be there at all in frame 50 (or actually, I guess it should be on the far left by that frame).

Isn't GIF actually loading the previous frame's self.tile at the top of _seek instead loading the new frame's self.tile information at the end of _seek? I tried doing something similar here and was a frame behind.
@adambiser Yeah, I think the gif plugin is a frame behind, to preserve the semantics of 'lazy' loading.  At the end of the seek(N) call, you're going to have the image reflect frame n-1, and calling load on it will load frame N. IIRC there's even some seek-forward/back silliness there as well.
@wiredfool True. FliImageFile probably just needs that bit of code at the top of its _seek method, too.

``` Python
    else:
        # ensure that the previous frame was loaded
        if not self.im:
            self.load()
```
Why did this get closed from https://github.com/python-pillow/Pillow/pull/2650?
https://github.com/python-pillow/Pillow/pull/2650 says that it does not fix this.
Reopening.

GitHub automatically closed it because it misunderstood "Doesn't fix #2649" as "fix #2649"!

Resolving patch:
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -131,6 +131,9 @@ def _seek(self, frame):
             self.__frame = -1
             self.__fp.seek(self.__rewind)
             self.__offset = 128
+        else:
+            # ensure that the previous frame was loaded
+            self.load()
 
         if frame != self.__frame + 1:
             raise ValueError("cannot seek to frame %d" % frame)


Test patch:
diff --git a/Tests/images/a_fli.png b/Tests/images/a_fli.png
new file mode 100644
Binary files /dev/null and b/Tests/images/a_fli.png differ
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -85,6 +85,13 @@ def test_seek_tell(self):
         layer_number = im.tell()
         self.assertEqual(layer_number, 1)
 
+    def test_seek(self):
+        im = Image.open(animated_test_file)
+        im.seek(50)
+
+        expected = Image.open("Tests/images/a_fli.png")
+        self.assert_image_equal(im, expected)
+
 
 if __name__ == '__main__':
     unittest.main()