Documentation
ImageFn is a URL-only Bun.Image transform server with a static editor UI. No storage, R2 bucket, or AWS dependency — transforms are driven entirely by query parameters.
How to use
Web editor
- Open the editor at
/. - Paste an HTTPS image URL in Source.
- Set width, height, fit, rotate, format, and quality under Transform.
- Click Apply transform to preview the result.
- Copy the image URL, editor link, or download the output from Share.
Image URL vs editor URL
Both URLs use the same query string. Only the path changes:
/i?...— returns transformed image bytes (use in<img>tags)./?...— opens the editor with the same settings pre-filled.
# Embed the transformed image
/i?url=https%3A%2F%2Fexample.com%2Fphoto.jpg&w=800&fmt=webp&q=80
# Reopen the same edit in the UI
/?url=https%3A%2F%2Fexample.com%2Fphoto.jpg&w=800&fmt=webp&q=80
Common examples
Resize to 1200px wide, keep aspect ratio:
/i?url=https%3A%2F%2Fexample.com%2Fphoto.jpg&w=1200&fit=inside&fmt=webp&q=82
Square cover crop:
/i?url=https%3A%2F%2Fexample.com%2Fphoto.jpg&w=600&h=600&fit=fill&fmt=jpeg&q=85
Let the server pick the best format from the browser Accept header:
/i?url=https%3A%2F%2Fexample.com%2Fphoto.jpg&w=900&fmt=auto&q=80
API usage
Transform image
GET /i?url=<https-image-url>&w=800&h=600&fit=inside&rot=90&fmt=webp&q=80
Legacy /api/image?... redirects to /i?... with 308.
| Param | Description |
|---|---|
url | Required HTTPS source image URL |
w, h | Output width/height from 1 to 12000 |
fit | pad (default) fits inside the box on a white canvas; inside fits without padding; fill crops/stretches to cover |
withoutEnlargement | true never upscales during resize |
filter | Resize kernel: lanczos3 (default), mitchell, box, etc. |
rot | Rotate by 90, 180, or 270 |
flip | true flips vertically |
flop | true flips horizontally |
fmt | webp, jpeg, jpg, png, avif, or auto |
q | Quality 1–100 (ignored for PNG) |
brightness | Optional 0–3 (Bun.Image modulation) |
saturation | Optional 0–3 (Bun.Image modulation) |
placeholder=true | Returns JSON with a tiny blurred data URL |
Health check
GET /health
Returns status: "ok", region, bun, storageConfigured, and the configured pixel/cache limits. Use this endpoint for Azure Standard Load Balancer and Azure Front Door probes. GET /api/health is an alias.
Azure multi-region API
When an Azure Storage Account is configured, raw originals and transformed variants are stored in Blob Storage, so every region can serve any request without local state.
Upload a raw original
POST /api/upload # multipart/form-data, field name: file
curl -F "file=@photo.jpg" https://<host>/api/upload
The bytes are validated with new Bun.Image(bytes).metadata() and written to the raw-images container. The response includes the assigned blob name and a ready-to-use sampleTransformUrl.
Transform (Azure-cached)
GET /api/transform?url=<https-image-url>&w=800&h=600&fit=cover&format=webp&q=80
GET /api/transform?blob=<blobName>&w=800&format=webp&q=80
| Param | Description |
|---|---|
url or blob | HTTPS source image, or a blob name previously stored in raw-images |
w, h | Output dimensions, 1–4096. Sources are decoded up to MAX_IMAGE_PIXELS (~256 MP) and MAX_SOURCE_BYTES (25 MiB) |
fit | inside (default), cover, contain, fill, or pad |
format | webp (default), jpeg/jpg, or png |
q | Quality 1–100 |
A deterministic SHA-256 key is computed from (source + width + height + fit + format + quality) and looked up in the processed-cache container first. The response advertises where the bytes came from:
X-Cache: HIT-AZURE-BLOB— primary account (RA-GRS secondary failover on outage).X-Cache: HIT-AZURE-REPLICA— region-local Object Replicated account.X-Cache: MISS-TRANSFORMED— freshly encoded withBun.Image, then written through to Azure.
Azure environment variables
| Variable | Description |
|---|---|
AZURE_REGION | Region reported by /health (e.g. eastus, westus2, northeurope) |
AZURE_STORAGE_ACCOUNT_NAME | Primary RA-GRS account; use DefaultAzureCredential (VM Managed Identity), no keys needed |
AZURE_REPLICA_STORAGE_ACCOUNT_NAME | Optional region-local read replica (Object Replication) |
AZURE_REPLICA_STORAGE_CONNECTION_STRING | Optional access-key string for the replica account (takes precedence for the replica client) |
AZURE_RAW_CONTAINER | Raw originals container (default raw-images) |
AZURE_CACHE_CONTAINER | Transformed variants container (default processed-cache) |
AZURE_STORAGE_CONNECTION_STRING | Access-key fallback for labs without Managed Identity. Takes precedence — when set, DefaultAzureCredential is never constructed |
Run bun run storage:init once per account to create the containers, then bun run start. A connection-string VM boot script is provided at deploy/azure/cloud-init.yaml.
Deployment
Local development
bun install
cp .env.example .env
bun run dev
Open http://localhost:8787. Use bun run start for a non-watch server.
Docker build and run
Build the image:
docker build -t image-fn .
# or
bun run docker:build
Run the container on port 8787:
cp .env.example .env
docker run --rm -p 8787:8787 --env-file .env image-fn
# or
bun run docker:run
The Docker image includes a healthcheck against /api/health. Verify manually:
curl http://localhost:8787/api/health
ALLOWED_HOST_SUFFIXES to known image hosts. The in-memory cache is per-container and resets on restart.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
PORT | 8787 | HTTP port |
ENVIRONMENT | development / production in Docker | Runtime mode |
CORS_ORIGIN | * | Allowed CORS origin |
ALLOWED_HOST_SUFFIXES | * | Comma-separated allowed source host suffixes |
CACHE_TTL_SECONDS | 3600 | In-memory transform cache TTL |
MAX_IMAGE_PIXELS | 268435456 | Source decode pixel limit |
MAX_OUTPUT_PIXELS | 41943040 | Output pixel limit |
MAX_SOURCE_BYTES | 26214400 (25 MB) | Remote source byte limit |
MAX_CACHE_BYTES | 134217728 (128 MB) | Total in-memory cache budget |
SOURCE_FETCH_TIMEOUT_MS | 10000 | Remote fetch timeout |
Security and limits
- Source URLs must be HTTPS. Localhost, private IPs, link-local,
.local, and.internalhosts are blocked. - Remote fetches respect
MAX_SOURCE_BYTES, decode respectsMAX_IMAGE_PIXELS, and output respectsMAX_OUTPUT_PIXELS. - Source fetches time out after
SOURCE_FETCH_TIMEOUT_MS. - The app is stateless — no persistent volume required.
About
I’m Ved Gupta. I built this project to make live interview support faster and less distracting.
Contact: vedgupta@protonmail.com
If you find the project useful, please star the repo on GitHub.