SeptemCore LogoSeptemCore
PrimitivesFiles

Image Processing

Image processing pipeline: react-easy-crop frontend editor sends crop coordinates to the backend. bimg/libvips performs crop, resize, format conversion to WebP/AVIF, and compression 80–90%. Timeout 30s. Max 10 MB. SDK: uploadImage(), processImage().

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

ComponentLibraryRole
Backendbimg (libvips, Go binding)Crop, resize, convert, compress. C library used by Netflix and AWS
Frontendreact-easy-cropInteractive crop editor: grid, zoom, drag, aspect ratio lock, rotation
Output formatsWebP, AVIF, PNG, JPEG, SVGAuto-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

FormatWhen usedNotes
webpDefault for all images30–40% smaller than JPEG at same quality
avifExplicit request or AVIF-capable CDN config50% smaller than JPEG, slower encode
pngTransparency detected or explicitly requestedLossless, larger file size
jpegExplicit request for JPEG outputLegacy compatibility
svgPassthrough only — not processed by bimgSVG is not rasterized

Processing Limits and Timeouts

ParameterValueEnv variable
Max image size10 MBFILES_MAX_IMAGE_SIZE_MB=10
Processing timeout30 secondsFILES_IMAGE_PROCESSING_TIMEOUT_SEC=30
Timeout behaviourOriginal saved; thumbnails = pending → background retry
Quality range1–100 (default 85)
Max output dimensions8 000 × 8 000 pxlibvips 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);
  }}
/>
PropDescription
bucketTarget logical bucket (avatars, assets, documents, exports, modules)
aspectRatioLock crop ratio: '1:1' for avatars, '3:1' for banners, '16:9' for covers
maxSizeMbClient-side size validation before upload (in addition to server 10 MB limit)
outputFormatTarget format: 'webp' (default), 'avif', 'png', 'jpeg'
presetsThumbnail presets to generate. icon_32 and card_300 are always generated
onUploadCompleteCallback receiving the full upload result

Error Reference

ScenarioHTTPCode
File exceeds 10 MB413PAYLOAD_TOO_LARGE
Processing timeout (30 s)202Original saved; thumbnails retried in background
Invalid crop coordinates400validation-error
Unsupported source format422UNSUPPORTED_FORMAT
File not found404not-found

On this page