An image's compressed size is not its decoded size. Put separate byte and pixel budgets in front of your local image-processing step, then decode only after both pass. This small Pillow utility is a resource gate for a staging pipeline, not a complete upload-security service.
Define a bounded local contract
The input is a local regular staging file that your application owns. The example allows one-frame PNG, JPEG and WebP images, up to 8,000,000 compressed bytes and 8,000,000 pixels. Those are explicit example policy values, not universal safe limits. Size worker memory and concurrency for your actual transforms.
This real page example is 1168×880 and 91,834 bytes in the downloaded fixture. The checker should inspect those properties without assuming that a convincing thumbnail means a trustworthy file. It does not validate the watch face, lettering or product claims.
Inspect before allocating the full image
Pillow documents Image.open as lazy and explains its decompression-bomb warning. Keep that warning enabled. Turn it into an error in this operation, apply your own lower pixel budget and reopen the same bounded bytes after verify().
from pathlib import Path
import io
import warnings
from PIL import Image
def checked_image(path, max_bytes=8_000_000, max_pixels=8_000_000):
# Read at most the budget plus one byte from a local staging file.
with Path(path).open('rb') as f:
data = f.read(max_bytes + 1)
if len(data) > max_bytes:
raise ValueError('compressed-byte budget exceeded')
with warnings.catch_warnings():
warnings.simplefilter('error', Image.DecompressionBombWarning)
with Image.open(io.BytesIO(data)) as im:
if im.format not in {'PNG', 'JPEG', 'WEBP'}:
raise ValueError('unsupported format')
width, height = im.size
if width < 1 or height < 1 or width * height > max_pixels:
raise ValueError('pixel budget exceeded')
if getattr(im, 'n_frames', 1) != 1:
raise ValueError('animated input is out of scope')
im.verify()
# verify() invalidates the decoder; reopen the same bounded bytes.
with Image.open(io.BytesIO(data)) as im:
im.load()
return {'width': width, 'height': height, 'bytes': len(data)}
The bounded read avoids loading an arbitrarily large compressed file into this process. Reopening the same bytes avoids a path-changing-between-checks problem inside the decode step. Header parsing and verify() still perform work; run the worker with external memory and time limits when handling untrusted uploads. Do not disable Pillow's protections to make a failing file pass.
Test rejection without building a giant file
Run checked_image("cover.jpg") and checked_image("workspace.jpg") on known local fixtures. Then call the function with max_bytes=1 and separately max_pixels=1. Each call must raise ValueError before full decoding. That exercises policy branches without creating a decompression bomb.
The second fixture is 1360×768 and 109,572 bytes. Both normal checks passed; both one-byte and one-pixel limits rejected each fixture, for six checks total. Pixel validation does not establish that the dashboard labels or chart data are correct.
Connect it to a generation workflow
The fixtures are existing examples on the FLUX 2 Klein 9B Base page. On September 8, 2026, the live form offered an optional image list up to ten items, a prompt, aspect ratio, Advanced Settings and Generate. The configured 1:1 prompt settled at a 6-credit estimate with Public · watermarked visibility; an earlier initial quote was lower, so do not hard-code that transient value.
No generation is required to test this utility. If you later create your own authorized fixture, record the selected model and current quote, download the output to a controlled staging directory and run the gate before resizing or compositing it.
Know what this does not protect
This is not antivirus, content moderation, a proof of authenticity or an SSRF defense. It does not fetch remote URLs, accept archives or support animated inputs. A production worker also needs maintained dependencies, format-specific testing, process isolation, timeouts and a concurrency budget. Do not treat width times height as a precise measurement of peak RAM: decoded modes, temporary buffers and transforms add overhead.
If you need another visual fixture, open the Klein prompt workspace, but keep asset creation and resource validation as separate steps. A deterministic file gate should not depend on rerolling an AI image.


Top comments (0)