KeyError: 'JPEG' when saving RGB images to PDF without explicit format argument
Regression
## What did you do?
Save multiple RGB images to a PDF file using a file path (without explicitly passing `format="pdf"`):
```python
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:
```python
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
1. Have `PdfImagePlugin` explicitly import its plugin dependencies (`JpegImagePlugin`, `Jpeg2KImagePlugin`)
2. Or have `_write_image()` call `preinit()` / lazy-load the required plugin before accessing `Image.SAVE`
Related: #9428
关闭于 2026-05-06 1 条评论