Skip to content

KeyError: 'JPEG' when saving RGB images to PDF without explicit format argument #9545

@Yokomi422

Description

@Yokomi422

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

  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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions