# Image transformations
Source: https://sovaria.in/docs/storage-image-transforms

Resize and re-encode stored images on the fly (thumbnails, avatars, WebP/AVIF) through public URLs, signed links or downloads, in every SDK: options, limits, caching, plans and security.

Store one original and ask for the size you need: Sovaria resizes and re-encodes the image when it is requested and caches the result. The options are supabase's: `width`, `height`, `resize` (`cover`, `contain`, `fill`), `quality` and `format`.

TypeScript:
```ts
const avatars = sovaria.storage.from('avatars')

// a public bucket: a URL for <img src>
const { data } = avatars.getPublicUrl(`${user.id}/me.png`, { transform: { width: 96, height: 96, resize: 'cover', format: 'webp' } })

// a private bucket: a signed link to the resized image
const { data: link } = await avatars.createSignedUrl(`${user.id}/me.png`, 3600, { transform: { width: 320 } })

// or the bytes
const { data: blob } = await avatars.download(`${user.id}/me.png`, { transform: { width: 640, quality: 70 } })
```

Python:
```python
avatars = sovaria.storage.from_("avatars")
url = avatars.get_public_url(f"{user_id}/me.png", {"transform": {"width": 96, "height": 96, "format": "webp"}})
link = await avatars.create_signed_url(f"{user_id}/me.png", 3600, {"transform": {"width": 320}})
png = await avatars.download(f"{user_id}/me.png", {"transform": {"width": 640, "quality": 70}})
```

Swift:
```swift
let avatars = sovaria.files.from("avatars")
let url = try avatars.getPublicURL(path: "\(userId)/me.png", transform: TransformOptions(width: 96, height: 96, format: "webp"))
let link = try await avatars.createSignedURL(path: "\(userId)/me.png", expiresIn: 3600, transform: TransformOptions(width: 320))
let bytes = try await avatars.download(path: "\(userId)/me.png", options: TransformOptions(width: 640, quality: 70))
```

Kotlin:
```kotlin
val avatars = sovaria.files.from("avatars")
val url = avatars.publicUrl("$userId/me.png", transform = TransformOptions(width = 96, height = 96, format = "webp"))
val link = avatars.createSignedUrl("$userId/me.png", 3600, transform = TransformOptions(width = 320))
val bytes = avatars.download("$userId/me.png", TransformOptions(width = 640, quality = 70))
```

Dart:
```dart
final avatars = sovaria.files.from('avatars');
final url = avatars.getPublicUrl('$userId/me.png', transform: const TransformOptions(width: 96, height: 96, format: 'webp'));
final link = await avatars.createSignedUrl('$userId/me.png', 3600, transform: const TransformOptions(width: 320));
final bytes = await avatars.download('$userId/me.png', transform: const TransformOptions(width: 640, quality: 70));
```

HTTP:
```bash
# signed in (private or public bucket)
curl -s "$URL/api/v1/store/render/image/avatars/$USER_ID/me.png?width=96&height=96&format=webp" -H "Authorization: Bearer $TOKEN" -o thumb.webp
# public bucket, no key
curl -s "$URL/api/v1/store/public/$PROJECT_ID/avatars/$USER_ID/me.png?width=96&height=96" -o thumb.png
```

## Options

| Option | Values | Default |
|---|---|---|
| `width`, `height` | 1–2,500 pixels. One of them keeps the aspect ratio. | the original size |
| `resize` | `cover`: fill the box and crop the rest · `contain`: fit inside the box · `fill`: stretch to the box | `cover` |
| `quality` | 20–100 (JPEG, WebP, AVIF) | 80 |
| `format` | `origin` (the stored format), `webp`, `avif`, `png`, `jpeg` | `origin` |

- Images are never made bigger than the original: a 100-pixel avatar asked for at 2,000 pixels stays 100 pixels.
- Animated GIF and WebP give their first frame. JPEG orientation (EXIF) is applied.
- Without any option the stored file is returned as it is.

## Limits and plans

|  |  |
|---|---|
| Plans | Paid plans (the `image_transforms` feature). On other plans the render, signed-transform and public-transform calls answer `403 PLAN_IMAGE_TRANSFORMS_REQUIRED`; plain downloads keep working. |
| Source images | JPEG, PNG, WebP, GIF and AVIF up to 25 MB and 50 megapixels. A bigger one is `413`; any other file (SVG, PDF, HTML…) is `422` and is never decoded. |
| Bandwidth | Transformed images count toward download bandwidth like any download ([Plan limits](https://sovaria.in/docs/plan-limits)). |

The size limits and the transform cache are platform settings Sovaria staff tune in Control Center; the numbers above are the current values.

## Caching

- Each result is cached by the file, its version and the options, so the same thumbnail is made once. Replacing the file makes new ones.
- Answers carry an `ETag`; a browser that sends `If-None-Match` gets `304 Not Modified`.
- `Cache-Control` is the file's own `cacheControl` (set at upload) or one hour; signed transform links are private and cached no longer than the link lives.

## Security

- Access is the bucket's: the render call needs `READ` on the file; a signed transform link is created only for a user who may read it; public transforms work only on public buckets.
- The image always comes from your own project's storage — never from a URL you pass — so transforms can't be pointed at another site or an internal address.
- The options are part of a signed link's signature: someone holding a 96-pixel link can't turn it into a full-size one.
- Pixel counts are checked from the file header before decoding, so a "decompression bomb" is refused without being opened.
