Search code examples
python-imaging-library

Pillow closes file pointer while iterating an image sequence


FLIR has a custom image format for some of their cameras, for which I have made an image plugin. This works fine for single images, but for files with multiple frames, it fails to load the subsequent frames because the file pointer is closed.

This works:

import flir
from PIL import Image, ImageSequence
infile = 'example.seq'

for i in range(3):
    im = Image.open(infile)
    im.seek(i)
    im.show()
    print(im.tell())

Output:

Loading frame 0 at offset 0
0
Loading frame 0 at offset 0
Loading frame 1 at offset 166620
1
Loading frame 0 at offset 0
Loading frame 2 at offset 333112
2

But this doesn't work:

import flir
from PIL import Image, ImageSequence
infile = 'example.seq'

im = Image.open(infile)
for i in range(3):
    im.seek(i)
    im.show()
    print(im.tell())

Output:

Loading frame 0 at offset 0
0
Traceback (most recent call last):
 in <module>
    im.seek(i)
  File "flir.py", line 86, in seek
    self.open_rel(offset)
  File "flir.py", line 36, in open_rel
    self.fp.seek(offset)
    ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'seek'

Here's an example of seeking with the webp decoder. This doesn't complain of the iteration, but shows the last frame every time even though I only attempt to seek the first three.

from PIL import Image, ImageSequence

im = Image.open('animated_hopper.webp')
for i in range(3):
    im.seek(i)
    im.show()
    print(im.tell()) #Prints the last frame every time?

Output:

3
3
3

I have tried writing my own load_seek() and load_read() functions, but this does not help when the file pointer has been closed. I have tried using the ImageSequence.Iterator, but this method just calls seek() anyway and has the same result. https://pillow.readthedocs.io/en/stable/_modules/PIL/ImageSequence.html#Iterator

Here's the source code for my plugin: https://gist.github.com/kamocat/aa93f3a062723a99666af2235c95b5ab I will see about uploading a sample image.

EDIT:

  1. Image.Open accepts a file pointer as an alternative to a file path. I thought if I opened the file myself, Pillow wouldn't close it. Unfortunately this did not change the matter.
import flir
from PIL import Image, ImageSequence
infile = 'example.seq'

with open(infile, 'rb') as f:
    im = Image.open(f)
    for i in range(3):
        im.seek(i)
        im.show()
        print(im.tell())
  1. I also considered that maybe I had the call signature wrong for ImageFile.seek() and that it should return itself. Accordingly I tried:
def seek(self, pos: int) -> ImageFile.ImageFile:
    #FIXME: This depends on known image size
    # This is currently necessary because the file pointer is closed between frames
    if pos == self.tell():
        return self
    if pos > 0:
        offset = pos * self.stride + 0x80
    else:
        offset = 0
    self.open_rel(offset)
    return self

but this also did not work.

  1. Perhaps this issue is specific to Windows 10, which has not been officially tested since Pillow 7.1.0? So I tried running it in Arch Linux with Python 3.12 and Pillow 11.0.0. Unfortunately this did not change the result.

Solution

  • It turns out there are two reasons for this, both stemming from the same piece of code in ImageFile.py

    if self._exclusive_fp and self._close_exclusive_fp_after_loading:
        self.fp.close()
    self.fp = None
    

    If a file path is passed into Image.open() then it will be closed, but even if a file is opened explicitly it will be closed by the garbage collector after self.fp is assigned to None.

    The solution takes three changes:

    • In _open(), set __close_exclusive_fp_after_loading=self.is_animated
    • At the end of open_rel, preserve the file pointer with self._fp = self.fp
    • And in seek(), restore the file pointer with self.fp = self._fp