Fast Go service. Fetch image from URL, crop pixels off the requested sides, pad the opposite side with white so the canvas keeps its original size. Redis-cached.
- Output dimensions == input dimensions (always).
- Cropping a side pushes content that way and whitens the opposite strip:
left=Ncuts N px off the left, adds N px white on the right.right=Ncuts N px off the right, adds N px white on the left.top=Ncuts N px off the top, adds N px white on the bottom.bottom=Ncuts N px off the bottom, adds N px white on the top.
- Opposite sides combine:
left=10&right=10cuts 20 px of content width. left+right >= width(ortop+bottom >= height) →400(nothing left).- Source format preserved: JPEG→JPEG, PNG→PNG, GIF→GIF. WebP decoded (pure Go) and returned as PNG — stdlib has no WebP encoder.
- Animated GIFs: first frame only.
GET /crop?url=<image-url>[&top=<int>&right=<int>&bottom=<int>&left=<int>][&q=<1-100>]
GET /healthz
url(required) — http/https image URL.top,right,bottom,left(default 0) — pixels cropped per side, must be>= 0, clamped toMAX_CROP.q(optional) — JPEG quality override.
Response: the image bytes. X-Cache: HIT|MISS. Cache-Control: public, max-age=86400.
Example:
curl "http://localhost:8080/crop?url=https://example.com/a.jpg&left=40&top=20" -o out.jpg
Same 1000×1000 source, one crop per side. Content shifts toward the cut; the opposite strip is filled white; the canvas stays 1000×1000.
![]() left=300 — white on the right
|
![]() right=300 — white on the left
|
![]() top=200 — white on the bottom
|
![]() bottom=200 — white on the top
|
docker compose up --build
Or locally (needs a Redis on localhost:6379):
go run .
Config via env — see .env.example.
- Redis cache keyed by
sha256(url|top|right|bottom|left|q); hits skip fetch+decode entirely. singleflightcoalesces concurrent identical misses into one unit of work (no thundering herd on cold cache).- Pooled HTTP transport, body size cap, per-request timeouts.
- Static
CGO_ENABLED=0binary on distroless — small image, fast cold start.
Apple M4 Pro (12 cores), 1000×1000 JPEG (~48 KB), left=300, no network
(in-process imageproc.Process, the per-MISS CPU cost).
Single call — decode + crop + encode:
| Stage | Time | Share |
|---|---|---|
| Process | ~13.9ms | 100% |
| JPEG decode | ~6.7ms | 48% |
| crop + white | ~1.3ms | 9% |
| JPEG encode | ~5.9ms | 43% |
1000 calls fired concurrently from one start barrier (GOMAXPROCS=12):
| Metric | Value |
|---|---|
| wall (all 1000) | ~1.68s |
| throughput | ~596 img/s |
| latency avg / p50 | ~861ms / ~852ms |
| latency p95 / p99 | ~1.60s / ~1.66s |
| heap peak | ~30 MB |
Under a 1k burst the box is CPU-bound: jobs queue behind 12 cores, so per-request latency climbs (~0.85s avg) while sustained throughput holds ~600 img/s. Cache HITs bypass decode/encode entirely (Redis roundtrip only), so steady-state real traffic is far faster — bottleneck is JPEG decode+encode (~91%), not the crop step.
- SSRF guard: every dial (including redirect hops) is checked; private,
loopback, link-local, CGNAT and multicast IPs are blocked. Disable with
ALLOW_PRIVATE=true(local testing only). - Optional
ALLOWED_HOSTSallowlist. - Fetch size hard-capped by
MAX_IMAGE_BYTES.



