-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
KeyError: 'JPEG' when saving RGB images to PDF without explicit format argument #9545
Description
What did you do?
Save multiple RGB images to a PDF file using a file path (without explicitly passing format="pdf"):
from pathlib import Path
from PIL import Image
image_dir = Path("./images")
output = image_dir / "output.pdf"
files = sorted(image_dir.glob("*png"))
images = [Image.open(file).convert("RGB") for file in files]
images[0].save(output, append_images=images[1:])What did you expect to happen?
A valid PDF file is created, the same as when format="pdf" is explicitly passed:
images[0].save(output, append_images=images[1:], format="pdf")What actually happened?
Traceback (most recent call last):
File "main.py", line 12, in <module>
images[0].save(output, append_images=images[1:])
File ".../PIL/Image.py", line 2713, in save
save_handler(self, fp, filename)
File ".../PIL/PdfImagePlugin.py", line 44, in _save_all
_save(im, fp, filename, save_all=True)
File ".../PIL/PdfImagePlugin.py", line 262, in _save
image_ref, procset = _write_image(im, filename, existing_pdf, image_refs)
File ".../PIL/PdfImagePlugin.py", line 151, in _write_image
Image.SAVE["JPEG"](im, op, filename)
KeyError: 'JPEG'
What are your OS, Python and Pillow versions?
- OS: Linux (Ubuntu)
- Python: 3.13.11
- Pillow: 12.2.0
Root cause analysis
This is a regression introduced by #9398 (lazy plugin loading, included in Pillow 12.2.0).
When format is not explicitly passed, Image.save() calls _import_plugin_for_extension(".pdf") which imports only PdfImagePlugin. Since this succeeds and registers "PDF" in SAVE, the guard at line 2689 (if format.upper() not in SAVE: init()) does not trigger init(). As a result, JpegImagePlugin is never loaded and Image.SAVE["JPEG"] does not exist.
When format="pdf" is explicitly passed, Image.save() calls preinit() (line 2652), which imports JpegImagePlugin (among others), so Image.SAVE["JPEG"] is available.
PdfImagePlugin._write_image() directly accesses Image.SAVE["JPEG"] (line 151, for DCTDecode) and Image.SAVE["JPEG2000"] (line 154, for JPXDecode), but these are implicit cross-plugin dependencies that the new lazy loading mechanism does not account for.
Affected image modes
- L, RGB, CMYK → triggers DCTDecode → requires
Image.SAVE["JPEG"] - LA, RGBA → triggers JPXDecode → requires
Image.SAVE["JPEG2000"]
Possible fixes
- Have
PdfImagePluginexplicitly import its plugin dependencies (JpegImagePlugin,Jpeg2KImagePlugin) - Or have
_write_image()callpreinit()/ lazy-load the required plugin before accessingImage.SAVE
Related: #9428