Skip to content

File pointer closing when iterating an image sequence #8716

Description

@kamocat

What did you do?

Iterate through a multi-frame image with a custom file plugin

What did you expect to happen?

I expected to see consecutive frames of the image

What actually happened?

The file pointer closed after the first call of Image.Seek()

What are your OS, Python and Pillow versions?

  • OS: Windows 10
  • Python: 3.12
  • Pillow: 10.3.0
--------------------------------------------------------------------
Pillow 10.3.0
Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)]
--------------------------------------------------------------------
Python executable is C:\Users\hornma\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\python.exe
System Python files loaded from C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2288.0_x64__qbz5n2kfra8p0
--------------------------------------------------------------------
Python Pillow modules loaded from C:\Users\hornma\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\PIL
Binary Pillow modules loaded from C:\Users\hornma\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 10.3.0
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.3.2
--- WEBP Transparency support ok
--- WEBPMUX support ok
--- WEBP Animation support ok
--- JPEG support ok, compiled for libjpeg-turbo 3.0.2
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.2
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.1
--- LIBTIFF support ok, loaded 4.6.0
*** RAQM (Bidirectional Text) support not installed
*** LIBIMAGEQUANT (Quantization method) support not installed
*** XCB (X protocol) support not installed
--------------------------------------------------------------------

Test script:

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())

Image plugin:

from PIL import Image, ImageFile
import struct
from math import log
import numpy as np
import datetime as dt
from os import stat

def _accept(prefix):
    return prefix[:3] == b"FFF"

def u16(arr, index):
    return int.from_bytes(arr[index:index+2], 'little')
def astr(arr, index):
    return arr[index:].partition(b'\0')[0].decode('ascii')
def f32(arr, index):
    return struct.unpack('f',arr[index:index+4])[0]
def i32(arr, index):
    return struct.unpack('i',arr[index:index+4])[0]
def u32(arr, index):
    return struct.unpack('I',arr[index:index+4])[0]



class FFFImageFile(ImageFile.ImageFile):
    format = "FFF"
    format_description = "Raw image exported by FLIR ResearchIR"
    
    def _open(self) -> None:
        self._mode = 'I;16L'
        self.open_rel(0)
        self.n_frames = (stat(self.fp.fileno()).st_size)//self.stride
        self.is_animated = self.n_frames>1
    
    def open_rel(self, offset) -> None:
        # Creator software info
        self.fp.seek(offset)
        h = self.fp.read(0x80)
        meta_index = u16(h, 0x4C)
        img_index = u16(h, 0x6C)
        info = {
            "creator software": astr(h,4),
        }
        
        # Camera configuration info
        self.fp.seek(offset + meta_index)
        h = self.fp.read(0x400)
        self._size = (
            u16(h,2),
            u16(h,4),
        )
        self.pos = u16(h, 0x18) #Frame number
        print(f'Loading frame {self.pos} at offset {offset}')
        kelvin = 273.15
        info['Emissivity']                   = f32(h, 0x20)
        info['IRWindowTemperature']          = f32(h, 0x30) - kelvin
        info['CameraModel']                  = astr(h, 0xd4)
        info['CameraSerial']                 = astr(h, 0x104)
        info['planck'] = {
            'R1': f32(h, 0x58),
            'B' : f32(h, 0x5c),
            'F' : f32(h, 0x60),
            'O' : i32(h, 0x308),
            'R2': f32(h, 0x30c),
        }
        ss = u32(h, 0x384)
        ms = u32(h, 0x388)
        tz = u16(h, 0x38c)
        # Our pictures are saved using naive timestamps so we ignore the tz
        info['timestamp'] = dt.datetime.fromtimestamp(ss + ms/1000)
        self.info = info
        
        # Prepare the tile
        self.stride = self.size[0] * self.size[1] * 2 + 2652 #16-bit words
        self.img_index = offset + img_index + 32
        self.tile = [ImageFile._Tile('raw', (0,0) + self.size, self.img_index, (self.mode, 0, 1))]
        
    def seek(self, pos: int) -> None:
        #FIXME: This depends on known image size
        # This is currently necessary because the file pointer is closed between frames
        if pos == self.tell():
            return
        if pos > 0:
            offset = pos * self.stride + 0x80
        else:
            offset = 0
        self.open_rel(offset)
        
    def tell(self) -> int:
        return self.pos

Image.register_open(FFFImageFile.format, FFFImageFile, _accept)

Image.register_extensions(
    FFFImageFile.format,
    [
        ".img", #Single image
        ".seq", #Series of images
    ],
)

EDIT:
Seeing as I can't upload files from my work computer, I'll have to implement a save() method if you need a sample image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions