DEV Community

Denny Guide
Denny Guide

Posted on

Batch-Converting 500 PNGs to JPG: I Timed My Pillow Script Against the Alternatives

Every few months I get the same folder: a few hundred PNGs exported from a design tool, all needing to be JPG before they can go into a CMS that has a hard 25 MB per-image cap. PNGs of screenshots and photos are heavy, and nobody on the content team is going to open an image editor 500 times.

So I have a script for this. But the last time I posted it, someone asked the obvious question: "is that actually faster than just using an online converter?" Fair. Let's measure it.

The task

  • 500 PNG files, 800×600
  • About 20% have an alpha channel (logos/icons), the rest are opaque
  • Target: JPG, quality 85, white background where alpha gets flattened
  • Don't touch the source files

The script

import os
from PIL import Image

SRC, DST = "src", "out"
os.makedirs(DST, exist_ok=True)

for name in sorted(os.listdir(SRC)):
    if not name.lower().endswith(".png"):
        continue

    img = Image.open(os.path.join(SRC, name))

    # Gotcha 1: JPEG has no alpha channel. RGBA/LA/P images must be
    # flattened onto a background first, or Pillow raises
    # "cannot write mode RGBA as JPEG".
    if img.mode in ("RGBA", "LA", "P"):
        rgba = img.convert("RGBA")
        bg = Image.new("RGB", rgba.size, (255, 255, 255))
        bg.paste(rgba, mask=rgba.split()[-1])  # alpha as the mask
        img = bg
    elif img.mode != "RGB":
        img = img.convert("RGB")

    # Gotcha 2: write to a different directory, never in place.
    # A buggy quality setting shouldn't be able to destroy originals.
    img.save(
        os.path.join(DST, os.path.splitext(name)[0] + ".jpg"),
        "JPEG",
        quality=85,
        optimize=True,  # ~5-10% smaller, costs a bit of CPU
    )
Enter fullscreen mode Exit fullscreen mode

The three gotchas, because they always bite someone

1. Alpha flattening. The naive img.convert("RGB") on an RGBA image doesn't composite onto a background — it just drops the alpha channel. Transparent areas turn black, and you find out after the batch finished. The paste with the alpha band as mask is the fix; white is usually what you want for web, but for dark sites use a dark background.

2. Palette mode (P). GIF-style indexed PNGs show up in the wild more than you'd think, and they're in the same "can't save directly as JPEG" bucket. The mode check above catches them.

3. Never convert in place. Same input/output directory plus a glob that matches both .png and .jpg is how people destroy their originals. Separate out/ folder, always.

The numbers

Ran on my M-series MacBook, Python 3.13, Pillow 12.2:

Metric Result
Files 500
Wall time 6.7 s
Throughput ~75 images/s
Input total 744.7 MB
Output total 171.7 MB (23% of input)

Worst-case input, by the way: these were noise images, which PNG compresses terribly. With real screenshots and photos the ratio is usually closer to 60–70%, because PNG is even more hopeless on photographic content than on noise.

CPU during the run: one core pegged, the rest idle. You could parallelize with a ProcessPoolExecutor and probably hit 4–6× that, but at 6.7 seconds for 500 images, why would you.

When the script is the wrong tool

Honesty time — "I have a script" is not always the right answer:

  • You're on someone else's machine. Installing Python to convert one folder is using a cannon to kill a mosquito.
  • It's a one-off. If you'll do this twice a year, writing and debugging a script is slower than the task itself, even the first time.
  • A non-technical teammate needs it. "Run my Python script" is not a deliverable. "Open this page, drop the folder" is.

For those cases I keep a browser bookmark around — recently I've been using iLoveConvert's PNG to JPG tool for exactly this shape of problem: no signup, files auto-deleted after 24 hours, and it takes a whole folder in one drop. It's obviously not beating 75 images/s on my own hardware, but it doesn't need to — it just needs to work on any machine in ten seconds.

Rule of thumb

  • Recurring batch + your own machine → script. It's faster, free, offline, and you control the quality settings.
  • One-off, foreign machine, or handing to someone else → online tool. The 30 seconds you save by not fiddling matters more than raw throughput.

The script above is deliberately boring. Boring is good: it's the version that still runs in three years when Pillow has renamed something else.

Top comments (0)