Quick fix

Re-download or re-upload the file; it is incomplete. If you need to process partial files you trust (for example, a camera card with a damaged photo), set ImageFile.LOAD_TRUNCATED_IMAGES = True before opening; Pillow then fills the missing area with grey.

What the error looks like

We cut the last 40% off a JPEG. Opening still works, because only the header is read; the error comes on first use:

thumb.py
from PIL import Image

img = Image.open("cut.jpg")          # a JPEG whose last 40% is missing
print("opened:", img.format, img.size)
img.thumbnail((400, 400))            # first real use of the pixels
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
opened: JPEG (1600, 1067)
Traceback (most recent call last):
  ...
OSError: image file is truncated (48 bytes not processed)

"N bytes not processed" is the leftover data the decoder could not finish. Any operation that needs pixels triggers it: load(), resize(), thumbnail(), convert(), save() and so on.

Common causes

Recover what is there (trusted files only)

recover.py
from PIL import Image, ImageFile

ImageFile.LOAD_TRUNCATED_IMAGES = True        # decode whatever is there

img = Image.open("cut.jpg")
img.load()
img.save("recovered.png")
print("bottom-right pixel:", img.getpixel((img.width - 1, img.height - 1)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
bottom-right pixel: (128, 128, 128)
A photo whose bottom part is solid grey because the file was truncated
Real output: everything after the cut is filled with grey (128, 128, 128).

Only enable this for files you trust and for one-off recovery. In a service that accepts uploads, a truncated file should be rejected, not silently half-processed.

Detect truncated files before they break a pipeline

Fully decode each file inside a try block. In this test folder, the 90% file and the 10% file both fail:

find_truncated.py
from pathlib import Path
from PIL import Image

data = open("photo.jpg", "rb").read()
Path("check").mkdir()
Path("check/good.jpg").write_bytes(data)
Path("check/cut_90.jpg").write_bytes(data[: int(len(data) * 0.9)])
Path("check/cut_10.jpg").write_bytes(data[: int(len(data) * 0.1)])


def check(path):
    try:
        with Image.open(path) as img:
            img.load()                        # full decode; verify() does not catch truncation
        return "ok"
    except OSError as e:
        return f"BAD: {e}"


for p in sorted(Path("check").iterdir()):
    print(f"{p.name:<12} {check(p)}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
cut_10.jpg   BAD: image file is truncated (28 bytes not processed)
cut_90.jpg   BAD: image file is truncated (108 bytes not processed)
good.jpg     ok

verify() does not catch it

Image.verify() checks structure without decoding, so it is fast but misses truncation:

verify_vs_load.py
from PIL import Image

data = open("photo.jpg", "rb").read()
open("cut.jpg", "wb").write(data[: int(len(data) * 0.9)])

with Image.open("cut.jpg") as img:
    img.verify()                      # header and structure checks only
print("verify() passed")

with Image.open("cut.jpg") as img:
    img.load()
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
verify() passed
Traceback (most recent call last):
  ...
OSError: image file is truncated (108 bytes not processed)

For a cheap pre-check on JPEGs, you can test that the file ends with the end-of-image marker:

jpeg_end_marker.py
data = open("photo.jpg", "rb").read()
cut = data[: int(len(data) * 0.9)]
for name, blob in [("complete", data), ("truncated", cut)]:
    print(f"{name:<10} ends with JPEG end marker FF D9: {blob.endswith(bytes([0xFF, 0xD9]))}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
complete   ends with JPEG end marker FF D9: True
truncated  ends with JPEG end marker FF D9: False

Fix the download, not the image

When the files come from your own code, the real fix is upstream: check response.raise_for_status(), compare the downloaded size with the Content-Length header, and write to a temporary name and rename only after the download completes. The cannot identify image file page covers the related case where even the header is missing.

The outputs and images on this page are the real results of running every example with Pillow 12.3.0 on Python 3.13 (Linux) on 2026-10-15. Every example is also re-run automatically on macOS (Darwin 25.6.0, Pillow 12.3.0), Linux (Linux 6.17.0-1022-azure, Pillow 12.3.0), Windows (Windows 2025Server, Pillow 12.3.0); the line under each example shows the result per operating system. The DejaVu Sans font files sit next to each script so font examples behave the same everywhere. Sample photos: NASA/Sam Lott, public domain. Spotted something wrong? Tell us.