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?
- OS: Debian GNU/Linux 12
Linux dev 6.8.12-2-pve #1 SMP PREEMPT_DYNAMIC PMX 6.8.12-2 (2024-09-05T10:03Z) x86_64 GNU/Linux
- Python: Python 3.11.2 (main, Aug 26 2024, 07:20:54) [GCC 12.2.0]
- Pillow: 11.1.0.dev0
--------------------------------------------------------------------
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:
- save() calls load() calls into _load_libtiff()
self.fp: _io.BufferedReader is corrupted after giving it to libtiff decode.decode()
load_end() calls exif_transpose() calls getexif() calls ImageFileDirectory_v2.load
- since
fp is corrupt, no valid tags are read. you can see this in the logs of many unknown tags
- no valid exif rotation tag means
Image.transpose() is not called in load_end()
- the image/tile size is confused (WxH swapped) somewhere. causing the tile size check to fail
Workarounds
I've come up with a handful of workarounds:
- Don't buffer
img = PIL.Image.open(img_path.open('rb', buffering=0))
The buffer can't be corrupted if there isn't one.
- 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.
- 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: fd299e3
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
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?
Linux dev 6.8.12-2-pve #1 SMP PREEMPT_DYNAMIC PMX 6.8.12-2 (2024-09-05T10:03Z) x86_64 GNU/LinuxDebugging Notes
I have debugged this, and determined it's caused by:
self.fp: _io.BufferedReaderis corrupted after giving it to libtiffdecode.decode()load_end()callsexif_transpose()callsgetexif()callsImageFileDirectory_v2.loadfpis corrupt, no valid tags are read. you can see this in the logs of many unknown tagsImage.transpose()is not called inload_end()Workarounds
I've come up with a handful of workarounds:
The buffer can't be corrupted if there isn't one.
This calls
ImageFileDirectory_v2.loadon a virgin fp, so it's not corrupt. Subsequent calls in Image.load are cached.Add after here https://github.com/python-pillow/Pillow/blob/main/src/PIL/TiffImagePlugin.py#L1385
This gets the buffer to drop its cache.
I got the idea from comments and similar code here: fd299e3
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