Pillow icon indicating copy to clipboard operation
Pillow copied to clipboard

AttributeError: 'Image' object has no attribute 'encoderinfo'. Did you mean: 'encoderconfig'?

Open ThomasDevoogdt opened this issue 7 months ago • 7 comments

Traceback:

>     image.save("reference.jpg")
>   File "/usr/local/lib/python3.12/dist-packages/PIL/Image.py", line 2596, in save
>     save_handler(self, fp, filename)
>   File "/usr/local/lib/python3.12/dist-packages/PIL/JpegImagePlugin.py", line 670, in _save
>     info = im.encoderinfo
>            ^^^^^^^^^^^^^^
> AttributeError: 'Image' object has no attribute 'encoderinfo'. Did you mean: 'encoderconfig'?

Version Used:

Ubuntu 24.04 LTS
Python 3.12.3
Pillow 11.1.0

I recently bumped Pillow from 9.3.0 to 11.1.0. At some point in the code, image = Image.open(io.BytesIO(bytes)) is used to create a Pillow image. I get the trackback as above when trying to save. Since this code did work for ages, I would expect that some Pillow change is to blame.

I see that JpegImagePlugin.py (line 670), tries to get encoderinfo. I don't know the Pillow code at all, but a wild guess is that this commit https://github.com/radarhere/Pillow/commit/203ca12626b22d185b59199b60ceb55081f6c3b2 is related to the issue I see now. Perhaps I can just work-around this bug by assigning a dict to encoderinfo myself for now, will update you if that work-around did the trick.

ThomasDevoogdt avatar Apr 28 '25 19:04 ThomasDevoogdt

Hi. I'm unable to replicate the problem with Pillow 11.1.0 and the following code.

import io
from PIL import Image
with open("Tests/images/hopper.jpg", "rb") as fp:
    bytes = fp.read()
image = Image.open(io.BytesIO(bytes))
image.save("reference.jpg")

Could we get a more specific self-contained example? As in, code that we can run from start to finish, along with any input images, to trigger the problem?

radarhere avatar Apr 28 '25 22:04 radarhere

Hi, I was able to reproduce. The root cause is that I was running the save command in parallel. This use case did perfectly work for 9.3.0, but not for 11.1.0.

wget https://python-pillow.github.io/assets/images/pillow-logo-248x250.png
from concurrent.futures import ThreadPoolExecutor

from PIL import Image

image = Image.open("pillow-logo-248x250.png")
image = image.convert('RGB')

max_parallel = 6

with ThreadPoolExecutor(max_workers=max_parallel) as executor:
    futures = []

    for _ in range(max_parallel):
        futures.append(executor.submit(lambda: image.save('pillow-logo-248x250.jpg')))

    for future in futures:
        future.result()

Running the above will fail more likely if you increase max_parallel, I get a 100% fail ratio if it's 10, but that is probably depended on the hardware I use.

ThomasDevoogdt avatar Apr 29 '25 08:04 ThomasDevoogdt

Thanks. How would you feel about copying the image before saving it, so that the save() operation isn't being run in parallel on the same image instance? lambda: image.copy().save('pillow-logo-248x250.jpg'))

radarhere avatar Apr 29 '25 09:04 radarhere

@radarhere I indeed fixed it by not saving that specific image in parallel. I leave it up to you to close the issue. Either Pillow does support threading, and then there is a bug, or it doesn't, and then it's fine to just close.

ThomasDevoogdt avatar Apr 29 '25 11:04 ThomasDevoogdt

I don't want to say that Pillow can't operate within threads in general. However, I will say that sharing a image instance across threads may not work.

#4848 was a very similar issue, where a user opened an image, and then performed an operation within threads. When Pillow opened the image, it opened a file handle, and then within the threads, errors appeared when Pillow thought it was done with the file handle and closed it in one thread, but then in another thread tried to read from the file handle.

radarhere avatar Apr 29 '25 11:04 radarhere

Maybe we need to start using contextvars.

aclark4life avatar Apr 29 '25 12:04 aclark4life

#8942 should resolve this specific situation.

radarhere avatar May 02 '25 15:05 radarhere