Image Transforms
Pixelflare uses Cloudflare's Image Resizing API to serve images at different sizes and with various transformations. There are two ways to get transformed images:
- Preset variants - predefined sizes like
w1024,thumb,og-image - Custom transforms - on-demand transformations via URL query parameters
Both are cached in R2 after generation, so subsequent requests are fast.
Preset Variants
Defined in packages/config/src/image-variants.ts:
| Variant | Dimensions | Fit Mode | Description |
|---|---|---|---|
original | - | - | No transformation |
w128 | 128px width | scale-down | Tiny |
w256 | 256px width | scale-down | Small |
w512 | 512px width | scale-down | Medium |
w1024 | 1024px width | scale-down | Large |
w1536 | 1536px width | scale-down | X-Large |
w2048 | 2048px width | scale-down | XX-Large |
thumb | 128x128 | cover | Square thumbnail |
og-image | 1200x630 | cover | Social media card |
All preset variants use quality 85 and strip metadata by default.
Requesting Preset Variants
GET /{owner}/{album}/{filename}/{variant}
GET /i/{shortId}/{variant}Examples:
https://cdn.example.com/alice/photos/sunset.jpg/w1024
https://cdn.example.com/i/abc123/thumbCustom Transforms
Add query parameters to any image URL to apply transformations on-the-fly.
GET /{owner}/{album}/{filename}?w=800&h=600&fit=cover&q=90
GET /i/{shortId}?w=400&blur=10Available Parameters
All parameters are optional. Parameter validation and parsing happens in packages/config/src/custom-transform-params.ts.
| Param | Type | Range | Description |
|---|---|---|---|
w | number | 1-9999 | Width in pixels |
h | number | 1-9999 | Height in pixels |
fit | string | see below | Resize mode |
q | number | 1-100 | Quality (default 85) |
f | string | avif, webp, jpeg, png, auto | Output format |
g | string | auto, center, face, top, bottom, left, right | Gravity/focus point |
blur | number | 1-250 | Blur radius |
sharpen | number | 1-10 | Sharpen amount |
br | number | 0.5-2.0 | Brightness |
co | number | 0.5-2.0 | Contrast |
sat | number | 0-2.0 | Saturation |
gam | number | 0.5-2.0 | Gamma |
r | number | 0, 90, 180, 270 | Rotation degrees |
flip | string | h, v, hv | Flip direction |
bg | string | 6-digit hex | Background color (no #) |
trim | string | top:N,left:N,... | Crop margins (max 1000px per side) |
meta | string | keep, copyright, none | Metadata handling |
Fit modes:
scale-down- shrink to fit, never enlarge (default)contain- fit within dimensions, may letterboxcover- fill dimensions, may cropcrop- same as cover but allows gravity positioningpad- fit within dimensions, pad with background color
Rate Limits
Custom transforms have two safeguards:
- Rate limit: Max 10 new variants per 60 seconds per IP
- Variant limit: Max 100 custom variants per image
Once a custom variant is cached, it can be accessed unlimited times. The rate limit only applies to generating new variants.
How It Works
Request Flow
graph TD
A[Request] --> B{Has query params?}
B -->|No| C{Variant in path?}
B -->|Yes| D[Parse custom params]
C -->|No| E[Serve original from R2]
C -->|Yes| F[Check R2 for variant]
D --> G{Valid params?}
G -->|No| H[400 Bad Request]
G -->|Yes| I[Check R2 cache]
I -->|Hit| J[Serve from R2]
I -->|Miss| K{Rate limit OK?}
K -->|No| L[429 Too Many Requests]
K -->|Yes| M{Under 100 variants?}
M -->|No| N[400 Variant Limit]
M -->|Yes| O[Transform via CF API]
O --> P[Cache to R2]
P --> J
F -->|Hit| Q[Serve from R2]
F -->|Miss| R[Transform on-demand or queue]
R --> QStorage Layout
Images and variants are stored in R2 with this structure:
{owner}/{album}/
├── {filename} # Original
├── {filename}/__variants/
│ ├── w128.jpg # Preset variants
│ ├── w256.jpg
│ ├── w512.jpg
│ ├── w1024.jpg
│ ├── thumb.jpg
│ └── og-image.jpg
├── {filename}-custom-w400-h300-fitcover # Custom variants
└── {filename}-custom-w800-q90-blur5The variant key for custom transforms is built from all parameters in a consistent order, e.g., w400-h300-q85-fitcover-gface. This is handled by buildCustomVariantKey() in the config package.
Cloudflare Image Resizing
Transformations use Cloudflare's Image Resizing API via the cf.image fetch option. The relevant code is in packages/api/src/lib/variants.ts.
For this to work, images must be accessible via URL (not raw bytes). The API needs either:
- An R2 custom domain configured (
R2_CUSTOM_DOMAINenv var), or - The original image accessible via the CDN
When a transform succeeds, Cloudflare adds a cf-resized header to the response.
Frontend Components
The custom variant builder UI is in packages/frontend/src/lib/components/image-details/CustomVariantBuilder.svelte. It renders form fields based on the field definitions in packages/config/src/custom-transform-fields.ts.
Features:
- Auto-generated form fields from config
- Input validation with min/max clamping
- Color picker for background
- Test button to preview in popup
- Save button to save parameter combinations
Saved Custom Variants
Users can save their favourite parameter combinations for reuse. Saved variants are stored in the user_settings table and appear in the variant dropdown with a star prefix.
Limits:
- Max 20 saved variants per user
- Names must be 1-50 characters
- Names can only contain letters, numbers, spaces, hyphens, underscores
The management UI is at /app/settings/embed-settings, implemented in SavedVariantsSettings.svelte.
API:
# Save a variant
PATCH /v1/settings
{ "saved_custom_variant": { "name": "Blog Thumb", "params": { "w": 400, "h": 300, "fit": "cover" } } }
# Delete a variant
PATCH /v1/settings
{ "delete_variant_id": "variant_abc123" }
# Rename a variant
PATCH /v1/settings
{ "update_variant": { "id": "variant_abc123", "name": "New Name" } }Adding a New Transform Parameter
To add a new parameter, you need to update three places:
1. Validation logic
In packages/config/src/custom-transform-params.ts:
// Add to interface
export interface CustomTransformParams {
// ...
yourparam?: number;
}
// Add to param keys array
export const CUSTOM_PARAM_KEYS = [
// ...
'yourparam',
] as const;
// Add validation in parseCustomTransformParams()
const yourparam = searchParams.yourparam;
if (yourparam) {
const value = parseInt(yourparam, 10);
if (isNaN(value) || value < 1 || value > 100) return null;
params.yourparam = value;
hasParams = true;
}
// Add to buildCustomVariantKey()
if (params.yourparam) parts.push(`yourparam${params.yourparam}`);
// Add to customParamsToImageTransformOptions()
if (params.yourparam !== undefined) options.yourCloudflareOption = params.yourparam;2. UI field definition
In packages/config/src/custom-transform-fields.ts:
export const CUSTOM_TRANSFORM_FIELDS: CustomTransformField[] = [
// ...
{
key: 'yourparam',
label: 'Your Param',
type: 'number', // or 'select' or 'color'
placeholder: '50',
min: 1,
max: 100,
step: 1,
grid: 'half', // or 'full'
},
];3. OpenAPI schema (optional)
If you want the parameter documented in the API spec, update packages/api/src/schemas/cdn.ts.
Error Responses
| Code | Error | Cause |
|---|---|---|
| 400 | INVALID_PARAMS | Invalid parameter value |
| 400 | VARIANT_LIMIT_EXCEEDED | Image has 100+ custom variants |
| 429 | RATE_LIMITED | Too many new variants (>10/min) |
| 404 | NOT_FOUND | Image doesn't exist |
| 403 | FORBIDDEN | Private image, not authenticated |
Response Headers
Transformed images include these headers:
Cache-Control: public, max-age=31536000, immutable- cache for 1 yearX-Request-ID- request ID for tracingX-Variant-Status- one of:original- served original, no transformtransformed- generated on-demandcached- served from R2 cachegenerating- queued for async generation