An upload named scene.png can contain JPEG bytes. A browser may still display it, so a successful preview is not proof that the filename matches the file. This matters when a downstream job chooses a decoder, extension or response Content-Type from the submitted name.
This tutorial builds a small Python gate for JPEG and PNG. It compares the final suffix with Pillow's detected format, verifies the opened file, and returns a canonical MIME value. It rejects mismatches instead of silently renaming or converting them.
Two real fixtures, one deliberate mismatch
The portrait above is Voor's published FLUX example: an emerald-blouse fashion portrait. The downloaded fixture identifies as JPEG, 832 × 1216. It passes when submitted as portrait.jpg.
The second fixture is a published fjord-and-cabin image, also JPEG, 1344 × 768. We pass its unchanged bytes with the supplied name fjord.png. That deliberately mismatched name must fail. The picture itself has not been edited, corrupted or regenerated.
For context, the FLUX Dev generator currently exposes a Prompt field and Generate button, aspect ratio and output count. On September 22, the inspected form selected 1:1 and one output, displaying 9 credits and Public · watermarked. Advanced Settings exposed seed, prompt guidance, generation steps, output format, Go fast, megapixels, output quality and prompt strength. Reference upload and Generate required sign-in in the signed-out view. No generation was run for these fixtures. The examples' dimensions do not establish the settings that originally produced them.
The portrait's published input is “Editorial fashion portrait of woman in emerald silk blouse, soft Rembrandt studio lighting, shallow depth of field, Vogue magazine cover quality, photorealistic”. The fjord input is “Cinematic wide landscape at golden hour, misty Norwegian fjord with wooden cabin, dramatic clouds, film still photography, anamorphic feel”. These prompts explain fixture provenance; this validation works on ordinary images too.
Define the acceptance contract
Accept .jpg, .jpeg and .png, ignoring suffix case. Reject missing or unrecognized suffixes, zero bytes, payloads above the chosen 8 MiB budget, unreadable files, and mismatched formats. The byte budget is an example application policy, not a Voor limit.
Pillow's image tutorial explains that opening an image reads its header to identify the format. Its Image reference documents lazy opening and verify(). Filename matching alone does not inspect that header.
Save this as format_gate.py:
from io import BytesIO
from pathlib import PurePosixPath
from PIL import Image, UnidentifiedImageError
SUFFIXES = {'.jpg': 'JPEG', '.jpeg': 'JPEG', '.png': 'PNG'}
MIME = {'JPEG': 'image/jpeg', 'PNG': 'image/png'}
def inspect_upload(filename: str, data: bytes) -> dict:
suffix = PurePosixPath(filename).suffix.lower()
expected = SUFFIXES.get(suffix)
if expected is None:
raise ValueError('unsupported or missing extension')
if not data or len(data) > 8 * 1024 * 1024:
raise ValueError('empty file or byte budget exceeded')
try:
with Image.open(BytesIO(data), formats=['JPEG', 'PNG']) as im:
detected = im.format
size = im.size
if detected != expected:
raise ValueError(f'extension expects {expected}; bytes identify {detected}')
im.verify()
except (UnidentifiedImageError, OSError, SyntaxError) as exc:
raise ValueError('unreadable image') from exc
return {'format': detected, 'mime': MIME[detected], 'size': size}
The return value derives MIME from the detected format. A caller-supplied Content-Type is not an input to this function and cannot make a mismatch pass. Use a server-generated storage key rather than treating the submitted filename as a filesystem path.
Test the failures as well as the happy path
Install Pillow in your project environment, then save this as test_format_gate.py beside the implementation:
import unittest
from io import BytesIO
from PIL import Image
from format_gate import inspect_upload
def fixture(fmt):
out = BytesIO()
Image.new('RGB', (3, 2), 'teal').save(out, format=fmt)
return out.getvalue()
class FormatGateTests(unittest.TestCase):
def test_jpeg_aliases_and_case(self):
for name in ['a.jpg', 'a.jpeg', 'A.JPG']:
with self.subTest(name=name):
self.assertEqual(inspect_upload(name, fixture('JPEG'))['mime'], 'image/jpeg')
def test_png(self):
self.assertEqual(inspect_upload('a.png', fixture('PNG'))['size'], (3, 2))
def test_mismatch_both_directions(self):
for name, fmt in [('a.png', 'JPEG'), ('a.jpg', 'PNG')]:
with self.subTest(name=name):
with self.assertRaisesRegex(ValueError, 'extension expects'):
inspect_upload(name, fixture(fmt))
def test_bad_names(self):
for name in ['a', 'a.jpg.exe', 'a.webp']:
with self.subTest(name=name), self.assertRaises(ValueError):
inspect_upload(name, fixture('JPEG'))
def test_not_an_image(self):
with self.assertRaisesRegex(ValueError, 'unreadable'):
inspect_upload('a.jpg', b'<html>not an image</html>')
def test_byte_budget(self):
for data in [b'', b'x' * (8 * 1024 * 1024 + 1)]:
with self.subTest(length=len(data)), self.assertRaises(ValueError):
inspect_upload('a.jpg', data)
if __name__ == '__main__':
unittest.main()
Run:
python -m pip install Pillow
python -m unittest -v test_format_gate.py
The executed example used Python 3.14.7 and Pillow 12.3.0. All six tests passed, including the subcases for both mismatch directions and uppercase JPEG suffixes. The tiny generated fixtures are unit-test data, not the tutorial's two illustrations.
Inspect the actual downloaded examples
After saving the displayed source examples locally as cover.jpg and workspace.jpg, this reproduces the key comparison:
from pathlib import Path
from format_gate import inspect_upload
print('portrait.jpg', inspect_upload('portrait.jpg', Path('cover.jpg').read_bytes()))
try:
print(inspect_upload('fjord.png', Path('workspace.jpg').read_bytes()))
except ValueError as exc:
print('fjord.png', 'REJECT', exc)
print('fjord.jpg', inspect_upload('fjord.jpg', Path('workspace.jpg').read_bytes()))
Observed output:
portrait.jpg {'format': 'JPEG', 'mime': 'image/jpeg', 'size': (832, 1216)}
fjord.png REJECT extension expects PNG; bytes identify JPEG
fjord.jpg {'format': 'JPEG', 'mime': 'image/jpeg', 'size': (1344, 768)}
The final call shows that the rejected payload was a valid JPEG with the wrong submitted suffix. Renaming a file is not format conversion. If your product intentionally accepts such mismatches, implement that as an explicit normalization policy and generate a matching extension and MIME value after decoding and re-encoding.
Put this gate in the right place
Check the request's streaming byte limit before accumulating a large payload in memory. This function's len(data) check occurs after the bytes already exist. Keep the original immutable byte buffer for the rest of the job so a later path replacement cannot change what was inspected.
This is a format-identity check, not a complete hostile-upload security boundary. Add your application's pixel budget, animation policy, full decode/re-encode validation and isolation separately. verify() does not promise that every file is safe or every pixel will decode, and this code deliberately does not strip metadata or scan for malware. When rejecting a request, return a clear format-mismatch error without echoing a private filename into a public log.
For a fresh source file to test, open FLUX Dev's export controls, check the current cost and visibility, and inspect the downloaded bytes regardless of the selected output-format label. The local validator requires no Voor account or paid generation.
AI disclosure: this article and code were prepared by an automated assistant; the included tests and two real-file checks were executed. Images are existing public Voor examples, not newly generated test results.


Top comments (0)