python-pillow / Pillow

Python Imaging Library (Fork)
https://python-pillow.org
Other
12.3k stars 2.23k forks source link

TIFF image loading EXIF corruption #8559

Open Knio opened 3 hours ago

Knio commented 3 hours ago

What did you do?

Load a TIFF with exif rotation info and save as JPG

What did you expect to happen?

Save image

What actually happened?

save() crashes

What are your OS, Python and Pillow versions?

--------------------------------------------------------------------
Pillow 11.1.0.dev0
Python 3.11.2 (main, Aug 26 2024, 07:20:54) [GCC 12.2.0]
--------------------------------------------------------------------
Python executable is /usr/bin/python3
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from /home/tom/.local/lib/python3.11/site-packages/PIL
Binary Pillow modules loaded from /home/tom/.local/lib/python3.11/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 11.1.0.dev0
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.12.1
--- LITTLECMS2 support ok, loaded 2.14
--- WEBP support ok, loaded 1.2.4
--- JPEG support ok, compiled for libjpeg-turbo 2.1.5
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.2
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.13
--- LIBTIFF support ok, loaded 4.5.0
*** RAQM (Bidirectional Text) support not installed
*** LIBIMAGEQUANT (Quantization method) support not installed
*** XCB (X protocol) support not installed
--------------------------------------------------------------------
img_path = pathlib.Path('IMG_0282.CR2')
img = PIL.Image.open(img_path)
img.save(img_path.with_suffix('.jpg'), format='JPEG')

Traceback (most recent call last):
  File "/mnt/zp/tom/docs/Programming/bugreports/pillow/pillow_bug2.py", line 27, in save
    img.save(img_path.with_suffix('.jpg'), format='JPEG')
  File "/home/tom/.local/lib/python3.11/site-packages/PIL/Image.py", line 2605, in save
    save_handler(self, fp, filename)
  File "/home/tom/.local/lib/python3.11/site-packages/PIL/JpegImagePlugin.py", line 843, in _save
    ImageFile._save(
  File "/home/tom/.local/lib/python3.11/site-packages/PIL/ImageFile.py", line 556, in _save
    _encode_tile(im, fp, tile, bufsize, fh)
  File "/home/tom/.local/lib/python3.11/site-packages/PIL/ImageFile.py", line 576, in _encode_tile
    encoder.setimage(im.im, extents)
SystemError: tile cannot extend outside image

Debugging Notes

I have debugged this, and determined it's caused by:

Workarounds

I've come up with a handful of workarounds:

1) Don't buffer

img = PIL.Image.open(img_path.open('rb', buffering=0))

The buffer can't be corrupted if there isn't one.

2) Pre-read exif tags

img = PIL.Image.open(img_path)
img.getexif()

This calls ImageFileDirectory_v2.load on a virgin fp, so it's not corrupt. Subsequent calls in Image.load are cached.

3) Poke the buffer

Add after here https://github.com/python-pillow/Pillow/blob/main/src/PIL/TiffImagePlugin.py#L1385

            n, err = decoder.decode(b"fpfp")
            if not close_self_fp:
                self.fp.seek(0, 2)
                assert self.fp.read(1) == b''
                self.fp.seek(0)

This gets the buffer to drop its cache. I got the idea from comments and similar code here: https://github.com/python-pillow/Pillow/commit/fd299e36cec4c12a0f201dbbbc1014dfaca7005a

However, I don't think this is a safe idea. This is not a documented behavior and the buffering/caching behavior of this could change at any time. When I tried with just seek(0), it did not help, and looking at the CPython code, it looks like seek() might be optimized if it's a location that's already buffered and won't actually move the file handle.

pillow.zip

Knio commented 3 hours ago

Broken case: Exif.load_from_fp called after _load_libtiff image

Good case: Exif loaded before, Image.transpose gets called image

Also, I think this explains my confusion in the other thread about not seeing exif tags sometimes

radarhere commented 2 hours ago

Pillow: 11.1.0.dev0

Could you be more specific about which commit you're building from? Does this happen with the released Pillow 11.0.0?