What did you do?
This is derived from a real world bug I observed while trying to interact with a company internal archive that archives customer messages like emails or paper letters. This bug report was formulated by an agent guided reproduction that tried to to achieve bug reproduction parity without exposing any of the private data that originally triggered this that I cannot share. I hope this is enough to fix this issue - should it not be, then I would be happy to provide more details as needed.
I created a synthetic three-frame LZW TIFF:
- frame 0:
RGB, PhotometricInterpretation=2
- frames 1 and 2: palette (
P), PhotometricInterpretation=3
I read n_frames and then reused the same Image handle to seek and copy every frame.
The failure is deterministic: 10/10 runs with Pillow 12.3.0.
The following controls succeed:
- The same seek/copy loop without first reading
n_frames.
- The same seek/load/copy loop without first reading
n_frames.
- Homogeneous RGB-only and P-only LZW TIFFs.
- Reopening the TIFF for each requested output frame, then sequentially loading frames through that target.
This appears related to #2229, which also involved n_frames and multi-page TIFFs. However, that closed issue failed with a closed-file error, while current Pillow raises ValueError: unrecognized image mode.
What did you expect to happen?
Reading n_frames should not affect subsequent frame decoding. The loop should copy all three frames successfully, with modes RGB, P, and P.
What actually happened?
The first image.copy() after reading n_frames raises:
ValueError: unrecognized image mode
The synthetic TIFF has no orientation tag, but the traceback enters ImageOps.exif_transpose() during TIFF loading.
Traceback (most recent call last):
File ".../PIL/Image.py", line 1364, in copy
self.load()
File ".../PIL/TiffImagePlugin.py", line 1306, in load
return self._load_libtiff()
File ".../PIL/TiffImagePlugin.py", line 1417, in _load_libtiff
self.load_end()
File ".../PIL/TiffImagePlugin.py", line 1328, in load_end
ImageOps.exif_transpose(self, in_place=True)
File ".../PIL/ImageOps.py", line 702, in exif_transpose
image.load()
File ".../PIL/TiffImagePlugin.py", line 1307, in load
return super().load()
File ".../PIL/ImageFile.py", line 298, in load
pixel = Image.Image.load(self)
File ".../PIL/Image.py", line 986, in load
self.im.putpalette(self.palette.mode, mode, arr)
ValueError: unrecognized image mode
Workaround: do not reuse the handle used to read n_frames; reopen the TIFF per requested output frame and sequentially load through that frame before copying.
What are your OS, Python and Pillow versions?
- OS: macOS 26.6 arm64
- Python: 3.14.6
- Pillow: 12.3.0
--------------------------------------------------------------------
Pillow 12.3.0
Python 3.14.6 (main, Jun 10 2026, 10:03:53) [Clang 21.1.8 ]
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 12.3.0
--- FREETYPE2 support ok, loaded 2.14.3
--- LITTLECMS2 support ok, loaded 2.19
--- WEBP support ok, loaded 1.6.0
--- AVIF support ok, loaded 1.4.2
--- JPEG support ok, compiled for libjpeg-turbo 3.1.4.1
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.4
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.1.zlib-ng,
compiled for zlib-ng 2.3.3
--- LIBTIFF support ok, loaded 4.7.1
--- XCB (X protocol) support ok
--------------------------------------------------------------------
from PIL import Image
path = "mixed-rgb-p-p.tif"
palette = Image.new("P", (2, 2))
palette.putpalette([0, 0, 255, 255, 255, 255] + [0, 0, 0] * 254)
Image.new("RGB", (2, 2), "red").save(
path,
format="TIFF",
save_all=True,
append_images=[palette, palette],
compression="tiff_lzw",
)
with Image.open(path) as image:
assert image.n_frames == 3 # Required trigger.
for index in range(3):
image.seek(index)
page = image.copy() # ValueError on the first iteration.
print(index, page.mode)
What did you do?
This is derived from a real world bug I observed while trying to interact with a company internal archive that archives customer messages like emails or paper letters. This bug report was formulated by an agent guided reproduction that tried to to achieve bug reproduction parity without exposing any of the private data that originally triggered this that I cannot share. I hope this is enough to fix this issue - should it not be, then I would be happy to provide more details as needed.
I created a synthetic three-frame LZW TIFF:
RGB,PhotometricInterpretation=2P),PhotometricInterpretation=3I read
n_framesand then reused the sameImagehandle to seek and copy every frame.The failure is deterministic: 10/10 runs with Pillow 12.3.0.
The following controls succeed:
n_frames.n_frames.This appears related to #2229, which also involved
n_framesand multi-page TIFFs. However, that closed issue failed with a closed-file error, while current Pillow raisesValueError: unrecognized image mode.What did you expect to happen?
Reading
n_framesshould not affect subsequent frame decoding. The loop should copy all three frames successfully, with modesRGB,P, andP.What actually happened?
The first
image.copy()after readingn_framesraises:The synthetic TIFF has no orientation tag, but the traceback enters
ImageOps.exif_transpose()during TIFF loading.Workaround: do not reuse the handle used to read
n_frames; reopen the TIFF per requested output frame and sequentially load through that frame before copying.What are your OS, Python and Pillow versions?