---
title: "Rebuild every uploaded image · Agent Wranglers"
url: https://agentwranglers.se/playbook/api-design/files-and-uploads/process-images
updated: 2026-10-20
licence: "All rights reserved"
---

# Rebuild every uploaded image

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Files and uploads](https://agentwranglers.se/playbook/api-design/files-and-uploads)
- Rule: Decode each uploaded image, turn it upright, shrink it to a size limit and save it again as a new file without metadata. Never serve the original.
- How: Photos carry EXIF data, which often holds the GPS position of a person's home and the camera's serial number. Rebuilding the image drops it and any hidden payload. Refuse images over a pixel limit, such as 40 million pixels, before decoding, to stop decompression bombs. Read the size from the header and compare it yourself. Pillow's own `MAX_IMAGE_PIXELS` only warns until twice the limit. Apply the EXIF orientation first, then drop every tag. Cap the long side, such as at 2,048 pixels, and save as WebP or AVIF. For the sizes a page shows, see [small, modern images](https://agentwranglers.se/playbook/website/performance/image-optimization).
- Check: Upload a photo with GPS tags and a 90-degree orientation tag. A metadata reader such as ExifTool finds no GPS or EXIF tags in the served file, and it shows upright. A 20,000 by 20,000 pixel PNG is refused.

Example:

```js
import sharp from 'sharp';

const MAX_INPUT_PIXELS = 40_000_000;
const MAX_SIDE = 2048;

export function rebuildPhoto(input) {
  return sharp(input, { limitInputPixels: MAX_INPUT_PIXELS })
    .rotate()
    .resize({ width: MAX_SIDE, height: MAX_SIDE, fit: 'inside', withoutEnlargement: true })
    .webp({ quality: 80 })
    .toBuffer();
}
```

```python
from io import BytesIO
from PIL import Image, ImageOps

MAX_INPUT_PIXELS = 40_000_000
MAX_SIDE = 2048

def rebuild_photo(data: bytes) -> bytes:
    original = Image.open(BytesIO(data))
    if original.width * original.height > MAX_INPUT_PIXELS:
        raise ValueError("image has too many pixels")
    upright = ImageOps.exif_transpose(original).convert("RGB")
    upright.thumbnail((MAX_SIDE, MAX_SIDE))
    clean = Image.new("RGB", upright.size)
    clean.paste(upright)
    out = BytesIO()
    clean.save(out, "WEBP", quality=80)
    return out.getvalue()
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers api design playbook: https://agentwranglers.se/playbook/api-design/files-and-uploads/process-images`.

- Previous: [Scan files for malware before anyone opens them](https://agentwranglers.se/playbook/api-design/files-and-uploads/scan-uploads)
- Next: [Serve private files through short-lived links](https://agentwranglers.se/playbook/api-design/files-and-uploads/signed-downloads)
- [Files and uploads](https://agentwranglers.se/playbook/api-design/files-and-uploads)
