This is a next.js web app for resizing images, specifically for low res, blurry previews. These low res previews can be swapped out for the high res image once it loads. This repo has React code examples that do this, which you can see deployed at http://imageresize.soulstealer.io/.
The main usage of this app is the api endpoint, /api/resize, which takes the following paramaters:
POST /api/resize
ContentType: application/json
{
"image_url": "<url to some jpeg/gif>"
}
Result:
{
"content": "<raw byte data>",
"dataURIBase64": "<URI base64 data; this is what is commonly used for inlining image data>"
}
Errors:
500
{
"errorMessage": "..."
}
Builds for Continous Integration are ran by Github Workflows (see ./github/workflows). Builds run within Docker on Github, and the following builds run and are tagged in the following scenarios:
- PR created/updated -
pr-<#>built and tagged docker image pushed to dockerhub. - PR merged -
int(for integration) built and tagged docker image pushed to dockerhub.minorversion of package.json semver version<major.minor.patch>is automatically bumped. - GH release created as PRERELEASE -
stagingbuild and tagged docker image pushed to dockerhub. - GH release created/updated as LATEST RELEASE - from package.json,
<major.minor.patch>built and tagged docker image pushed to dockerhub.
For the moment, kubectl edit deployment imageresize, search for image:, and bump the version manually. In the near future, ArgoCD config will be deployed to do this for us.
dev– start dev serverbuild– bundle application for productionexport– exports static website tooutfolderanalyze– analyzes application bundle with @next/bundle-analyzer
typecheck– checks TypeScript typeslint– runs ESLintprettier:check– checks files with Prettierjest– runs jest testsjest:watch– starts jest watchtest– runsjest,prettier:check,lintandtypecheckscripts
prettier:write– formats all files with Prettier
A new endpoint for generating resized thumbnails, optimized for mobile app scrolling performance.
GET /api/thumbnail?url=<image_url>&width=350&height=350
| Parameter | Required | Default | Description |
|---|---|---|---|
url |
Yes | - | Source image URL |
width |
No | 350 | Target width in pixels |
height |
No | 350 | Target height in pixels |
format |
No | Auto-detect | Output format: jpeg, png, webp, gif |
quality |
No | 80 | JPEG/WebP quality (1-100) |
- Returns the resized image binary with appropriate
Content-Typeheader - For GIFs: Preserves animation (all frames resized)
- Includes
X-Cache: HITorX-Cache: MISSheader
# Auto-detect format (GIF in = GIF out)
curl "http://localhost:3000/api/thumbnail?url=https://example.com/photo.gif&width=350&height=350" -o thumb.gif
# Force JPEG output (static, smaller file)
curl "http://localhost:3000/api/thumbnail?url=https://example.com/photo.gif&width=350&height=350&format=jpeg" -o thumb.jpg
# WebP format (best compression)
curl "http://localhost:3000/api/thumbnail?url=https://example.com/photo.png&width=200&height=200&format=webp&quality=75" -o thumb.webp| Format | Original (992×1488) | Thumbnail (350×350) | Reduction |
|---|---|---|---|
| Animated GIF | ~2MB | ~300KB | 85% |
| Static JPEG | ~500KB | ~10-25KB | 95-98% |
| WebP | ~400KB | ~8-15KB | 96-98% |
| Format | Original | Thumbnail | Reduction |
|---|---|---|---|
| GIF (4 frames) | ~23MB | ~2MB | 91% |
| Static image | ~6MB | ~0.5MB | 92% |
The thumbnail API uses an in-memory LRU (Least Recently Used) cache to avoid re-processing identical requests.
| Setting | Value | Rationale |
|---|---|---|
| Max Size | 300MB | Leaves room for app memory (~512MB-1GB pod) |
| TTL | 24 hours | Images are immutable |
| Eviction | LRU | Removes least-recently-accessed items first |
{url}|{width}x{height}|{format}|q{quality}
Example: https://storage.../image.gif|350x350|gif|q80
| Thumbnail Type | Avg Size | Items in 300MB |
|---|---|---|
| Animated GIF | 300KB | ~1,000 |
| Static JPEG | 15KB | ~20,000 |
| Mixed (50/50) | 150KB | ~2,000 |
The cache logs all operations for monitoring:
[cache] HIT key="https://storage..." size=312.5KB age=3600s
[cache] MISS key="https://storage..."
[cache] STORED key="https://storage..." size=312.5KB cacheSize=45.2MB/300MB
[cache] EVICTED key="https://storage..." reason=evict size=298.1KB
[cache] STATS hits=1523 misses=47 hitRate=97.0% entries=892 size=267.8MB/300MB
| Scenario | Symptom | Solution |
|---|---|---|
| >1k animated GIFs | Cache full, high eviction rate | Switch to JPEG or Phase 2 |
| Multiple k8s pods | Each pod has separate cache | Move to Phase 2 or 3 |
| Frequent deploys | Cold cache, high miss rate | Move to Phase 3 |
| >100k requests/day | Server CPU bottleneck | Add CDN (Phase 2) |
Add GCP Cloud CDN in front of this service.
When to implement:
- Traffic exceeds 50k requests/day
- Running multiple pods (cache not shared)
- Need global edge caching
Implementation:
- The existing
Cache-Control: public, max-age=31536000header is already CDN-compatible - Configure Cloud CDN to cache responses
- CDN handles cache invalidation automatically via TTL
Costs: ~$0.02-0.08/GB served
Write thumbnails to GCS alongside original images.
When to implement:
- Need persistence across deploys
- Want to serve directly from GCS (bypass this service)
- Very high volume (>500k/day)
Implementation:
gs://bucket/original.gif ← Source image
gs://bucket/thumbs/original_350x350.gif ← Generated thumbnail
Flow:
- Check if thumbnail exists in GCS
- If yes, redirect or serve from GCS
- If no, generate, store in GCS, then serve
Costs: ~$0.02/GB/month storage + $0.12/GB egress
Shared cache across multiple pods.
When to implement:
- Running 3+ pods
- Need shared cache without CDN
- Want fine-grained cache control
Implementation:
- Deploy Redis in k8s cluster
- Replace LRU cache with Redis client
- Same key structure, but shared across all pods
Costs: ~$50-100/month for managed Redis, or self-host in k8s
A new endpoint for creating animated GIFs from multiple already-filtered images. This endpoint is designed to work with the React Native mobile app, where CSS filters are applied client-side, and filtered frames are sent to this endpoint for GIF creation.
The mobile app applies CSS filters (brightness, contrast, saturation, etc.) to images in real-time. Users can preview different filter effects and then create an animated GIF showing the progression through different filter states. The app captures each filtered frame and sends them to this endpoint for GIF creation.
POST /api/create-filtered-gif
Content-Type: multipart/form-data
| Field Name | Type | Required | Description |
|---|---|---|---|
images[] |
File[] | Yes | Multiple image files (JPEG, PNG, or GIF). Already filtered. Must have uniform dimensions. Max 10MB per image. |
frameDelay |
Integer | No | Frame delay in milliseconds. Default: 500. Range: 10-10000. |
- Success (200): Returns animated GIF binary with
Content-Type: image/gif - Error (400/500): Returns JSON error object with
errorandmessagefields
Example Input Frames:
test_images/frame1.jpg- First filtered frametest_images/frame2.jpg- Second filtered frametest_images/frame3.jpg- Third filtered frametest_images/frame4.jpg- Fourth filtered frame
Example Output:
test_images/test-output.gif- Animated GIF containing all input frames
curl -X POST http://localhost:3000/api/create-filtered-gif \
-F "images[][email protected]" \
-F "images[][email protected]" \
-F "images[][email protected]" \
-F "images[][email protected]" \
-F "frameDelay=500" \
-o output.gifconst formData = new FormData();
formData.append('images[]', {
uri: frame1Uri,
type: 'image/jpeg',
name: 'frame1.jpg'
});
formData.append('images[]', {
uri: frame2Uri,
type: 'image/jpeg',
name: 'frame2.jpg'
});
formData.append('images[]', {
uri: frame3Uri,
type: 'image/jpeg',
name: 'frame3.jpg'
});
formData.append('images[]', {
uri: frame4Uri,
type: 'image/jpeg',
name: 'frame4.jpg'
});
formData.append('frameDelay', '500');
const response = await fetch('https://api.example.com/api/create-filtered-gif', {
method: 'POST',
body: formData,
});
const gifBlob = await response.blob();
// Save or display the GIF-
Technology: Node.js with Sharp + gifenc (pure JavaScript, no native deps)
- Sharp: Already in use for thumbnail generation, fast native image processing
- gifenc: Pure JavaScript GIF encoder with no native dependencies (unlike
gifencoderwhich requirescanvas) - Avoids Python/Pillow dependency (the
server/repo is legacy)
-
Data Format: multipart/form-data (standard, efficient, better for slow connections)
- Standard for file uploads
- Better for low/spotty internet: streams data incrementally
- More efficient than base64 (33% size overhead)
- Works with all HTTP clients
-
Size Limit: 10MB per image (prevents memory exhaustion)
- Reasonable for mobile app captures (typically 1-5MB)
- Validated before processing (fails fast)
- Protects server resources
-
Uniform Sizes: All images must have identical dimensions (validated)
- Required for GIF animation format
- First image sets dimensions, others must match
- Clear error message on mismatch
-
Frame Delay: Configurable, default 500ms (0.5 seconds per frame)
- Range: 10ms - 10 seconds
- Allows for fast previews or slow dramatic effects
- Specified in milliseconds for precision
| Error | Status | Message Example |
|---|---|---|
| No images provided | 400 | "No images provided. Use 'images' or 'images[]' field." |
| Image size exceeded | 400 | "Image 1 exceeds 10MB limit: 12.5MB" |
| Dimension mismatch | 400 | "Image 2 dimensions (800x600) do not match first image (1024x768). All images must have uniform dimensions." |
| Invalid frame delay | 400 | "frameDelay must be between 10 and 10000 milliseconds" |
| Processing failure | 500 | "Failed to create GIF: [error details]" |
- Processing time: ~100-500ms per frame (depends on image size)
- Memory usage: ~2-3x input size during processing
- Output size: Typically 50-200KB for 4 frames at mobile resolutions (992×1488)
- Concurrent requests: Limited by server memory (each request processes images in memory)
A test script is available at tests/test-gif-endpoint.js that:
- Downloads sample images from URLs
- Sends them to the endpoint
- Saves the resulting GIF to
tests/test-output.gif
Run the test:
cd tests
node test-gif-endpoint.js- Full API Documentation: See API_DOCUMENTATION_CREATE_FILTERED_GIF.md
- Release Notes: See RELEASE_NOTES_GIF_API.md
- Swagger UI: Available at
/swagger - OpenAPI Spec: Available at
/api/swagger.json
