Image Processing
Every module that uses kernel.files() automatically gets a full image
processing pipeline. Modules never need to implement their own resize,
crop, or format conversion — the kernel handles it transparently.
Technical Stack
| Component | Library | Role |
|---|---|---|
| Backend | bimg (libvips, Go binding) | Crop, resize, convert, compress. C library used by Netflix and AWS |
| Frontend | react-easy-crop | Interactive crop editor: grid, zoom, drag, aspect ratio lock, rotation |
| Output formats | WebP, AVIF, PNG, JPEG, SVG | Auto-selected by backend |
Upload + Processing Pipeline
When a user uploads an image through the <ImageUpload> component
or uploadImage(), the following steps execute automatically:
1. User selects file in browser
2. Frontend — react-easy-crop editor:
Grid crop overlay (Instagram-style)
Zoom: pinch gesture or slider
Drag to reposition crop area
Aspect ratio lock: 1:1, 16:9, 3:1 (configured per module)
Rotation
3. Frontend sends:
Original file + crop parameters { x, y, width, height, zoom }
4. Backend — bimg/libvips:
Crop by coordinates
Resize to target dimensions
Convert: WebP by default, PNG if transparency detected
Compress: quality 80–90% (visually lossless, size −60–80%)
Generate thumbnail presets (icon_32 + card_300 always, others on-demand)
Timeout: 30 s (FILES_IMAGE_PROCESSING_TIMEOUT_SEC)
→ on timeout: original saved, thumbnails = pending (background retry)
Limit: 10 MB max (FILES_MAX_IMAGE_SIZE_MB)
→ above limit: 413 Payload Too Large
5. Save to S3:
Original file
Processed file (cropped + resized + converted)
Auto-generated thumbnails
SDK — uploadImage()
Upload an image with crop parameters in a single call:
import { kernel } from '@platform/sdk-core';
const result = await kernel.files().uploadImage({
file: imageFile, // File | Blob | Buffer
filename: 'profile-photo.png',
bucket: 'avatars',
crop: {
x: 120,
y: 80,
width: 400,
height: 400,
zoom: 1.2,
},
outputFormat: 'webp', // 'webp' | 'avif' | 'png' | 'jpeg' — default 'webp'
quality: 85, // 1–100 — default 85
aspectRatio: '1:1',
});
// result:
// {
// fileId: '01j9paf1l000000000000000',
// originalUrl: 'https://api.septemcore.com/v1/files/01j9paf1l...',
// processedUrl: 'https://api.septemcore.com/v1/files/01j9paf1l.../processed',
// thumbnails: {
// icon_32: 'https://api.septemcore.com/v1/files/01j9paf1l.../thumbnail/icon_32',
// card_300: 'https://api.septemcore.com/v1/files/01j9paf1l.../thumbnail/card_300'
// },
// size: 84320,
// compressionRatio: '72%',
// status: 'available'
// }
SDK — processImage()
Process an already-uploaded image (run or re-run bimg operations):
const result = await kernel.files().processImage({
fileId: '01j9paf1l000000000000000',
operations: {
crop: { x: 0, y: 0, width: 800, height: 600 },
resize: { width: 400, height: 300 },
format: 'avif',
quality: 80,
rotate: 90,
},
});
// result:
// {
// fileId: '01j9paf1l000000000000000',
// processedUrl: 'https://api.septemcore.com/v1/files/01j9paf1l.../processed',
// size: 61440,
// status: 'available'
// }
REST — POST /files/:id/process
POST https://api.septemcore.com/v1/files/01j9paf1l000000000000000/process
Authorization: Bearer <access_token>
Content-Type: application/json
{
"operations": {
"crop": { "x": 0, "y": 0, "width": 800, "height": 600 },
"resize": { "width": 400, "height": 300 },
"format": "avif",
"quality": 80,
"rotate": 90
}
}
Response 200 OK:
{
"fileId": "01j9paf1l000000000000000",
"processedUrl": "https://api.septemcore.com/v1/files/01j9paf1l.../processed",
"size": 61440,
"status": "available"
}
Output Format Selection
| Format | When used | Notes |
|---|---|---|
webp | Default for all images | 30–40% smaller than JPEG at same quality |
avif | Explicit request or AVIF-capable CDN config | 50% smaller than JPEG, slower encode |
png | Transparency detected or explicitly requested | Lossless, larger file size |
jpeg | Explicit request for JPEG output | Legacy compatibility |
svg | Passthrough only — not processed by bimg | SVG is not rasterized |
Processing Limits and Timeouts
| Parameter | Value | Env variable |
|---|---|---|
| Max image size | 10 MB | FILES_MAX_IMAGE_SIZE_MB=10 |
| Processing timeout | 30 seconds | FILES_IMAGE_PROCESSING_TIMEOUT_SEC=30 |
| Timeout behaviour | Original saved; thumbnails = pending → background retry | — |
| Quality range | 1–100 (default 85) | — |
| Max output dimensions | 8 000 × 8 000 px | libvips internal limit |
Frontend — ImageUpload Component
UI Shell provides a ready-made <ImageUpload> component with the
built-in crop editor. Modules drop it in without implementing any
crop UI:
import { ImageUpload } from '@platform/ui-shell';
<ImageUpload
bucket="avatars"
aspectRatio="1:1"
maxSizeMb={10}
outputFormat="webp"
presets={['icon_32', 'avatar_64', 'card_300']}
onUploadComplete={(result) => {
// result.fileId, result.thumbnails.avatar_64, result.processedUrl
setAvatarUrl(result.thumbnails.avatar_64);
}}
/>
| Prop | Description |
|---|---|
bucket | Target logical bucket (avatars, assets, documents, exports, modules) |
aspectRatio | Lock crop ratio: '1:1' for avatars, '3:1' for banners, '16:9' for covers |
maxSizeMb | Client-side size validation before upload (in addition to server 10 MB limit) |
outputFormat | Target format: 'webp' (default), 'avif', 'png', 'jpeg' |
presets | Thumbnail presets to generate. icon_32 and card_300 are always generated |
onUploadComplete | Callback receiving the full upload result |
Error Reference
| Scenario | HTTP | Code |
|---|---|---|
| File exceeds 10 MB | 413 | PAYLOAD_TOO_LARGE |
| Processing timeout (30 s) | 202 | Original saved; thumbnails retried in background |
| Invalid crop coordinates | 400 | validation-error |
| Unsupported source format | 422 | UNSUPPORTED_FORMAT |
| File not found | 404 | not-found |