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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions