# Lenses and transforms

Use reusable named lenses or one-off transform selectors to resize and convert retained images through the CalmLens CDN. Transformations produce derived responses without creating new asset records. CDN URLs use `https://cdn.calmlens.com/v1`.

## Choose a transform

| Need | Use |
| --- | --- |
| A reusable thumbnail or image preset | A named lens. Update its version when its definition changes. |
| A one-off size or format | A direct transform selector in the URL. |

## Named lenses

Lenses are named CDN transforms. They let you request a derived version of an asset without creating another asset record. Lens and transform selectors work on both `/content` and `/preview`. See [Delivering assets](/docs/guides/delivering-assets) for retention, visibility, authorization, and availability requirements.

Use a named lens by appending `/@:lensIdentifier` to the CDN URL. The `@` prefix makes the selector unambiguously a named lens.

```text
/assets/:projectId/:assetIdentifier/content/@thumbnail
```

Example:

```bash
curl -X GET "https://cdn.calmlens.com/v1/assets/your-project-id/your-asset-id/content/@thumbnail"
```

When signing a private lens URL, include the same lens identifier in the signing request:

```bash
curl -X POST "https://api.calmlens.com/v1/assets/your-asset-id/signed-url" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "resource": "content",
  "lens": "thumbnail",
  "expiresInSeconds": 900
}'
```

The CDN validates the signature against the lens, then applies it.

## Direct transforms

For one-off derived assets, append a direct transform selector without `@`. The selector uses the existing transform syntax and can contain one or more comma-separated transforms.

```text
/assets/:projectId/:assetIdentifier/content/imageTransform[format=png,width=1024]
```

**python**

```python
import os
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id=os.environ["CALMLENS_PROJECT_ID"],
)

with client.cdn.serve({"assetIdentifier": "your-asset-id", "transform": "imageTransform[format=png,width=1024]"}) as response:
    content = response.read()
print(len(content))
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "io"
    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewClient(calmlens.Options{APIKey: os.Getenv("CALMLENS_API_KEY"), ProjectID: os.Getenv("CALMLENS_PROJECT_ID")})
    if err != nil { log.Fatal(err) }
    ctx := context.Background()
    response, err := client.CDN.Serve(ctx, calmlens.CDNServeOptions{CDNAssetURLOptions: calmlens.CDNAssetURLOptions{AssetIdentifier: "your-asset-id", Transform: "imageTransform[format=png,width=1024]"}})
    if err != nil { log.Fatal(err) }
    defer response.Body.Close()
    content, err := io.ReadAll(response.Body)
    if err != nil { log.Fatal(err) }
    fmt.Println(len(content))
}

```

**JavaScript**

```javascript
const response = await client.cdn.serve({
  assetIdentifier: assetId,
  transform: "imageTransform[format=png,width=1024]",
});
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.cdn.serve(asset_identifier: "your-asset-id", transform: "imageTransform[format=png,width=1024]")
puts result
client.close
```

**rust**

```rust
use calmlens::{models::*, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    let result = client.cdn().serve(CdnRequest { asset_identifier: "your-asset-id".into(), transform: Some("imageTransform[format=png,width=1024]".into()), ..Default::default() }).await?;
    println!("{}", result.status());
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensClient;
import com.calmlens.Models.*;

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    try (var result = client.cdn.serve(new CdnRequest().assetIdentifier("your-asset-id").transform("imageTransform[format=png,width=1024]"))) {
      System.out.println(result.status());
    }
  }
}
```

**curl**

```sh
curl --globoff "https://cdn.calmlens.com/v1/assets/$CALMLENS_PROJECT_ID/$ASSET_ID/content/imageTransform[format=png,width=1024]"
```

Direct transforms are not saved as lenses. Use a [versioned lens](#versioned-custom-lenses) when a transform should have a stable project-owned name and cache invalidation on definition changes.

### Image transform options

The supported transform type is `imageTransform`. Options are comma-separated `key=value` pairs inside brackets.

| Option | Accepted values |
| --- | --- |
| `format` | `jpg`, `png`, `webp`, `avif`, or `gif` |
| `width`, `height` | Non-negative integers |
| `fit` | `scale-down`, `contain`, `pad`, `squeeze`, `cover`, or `crop` |
| `quality` | Integer from `0` through `255` |
| `rotate` | `0`, `90`, `180`, or `270` |
| `flip` | `h`, `v`, or `hv` |
| Other adjustments | `background`, `blur`, `brightness`, `contrast`, `gamma`, `saturation`, `sharpen`, `gravity`, `border`, `segment`, and `trim` |

Unknown transform types and malformed selectors return [`400 asset/invalid-transform`](/docs/api/cdn#error-asset-invalid-transform). Unsupported options return [`400 asset/invalid-transform-options`](/docs/api/cdn#error-asset-invalid-transform-options).

## Versioned custom lenses

Lenses are versioned, project-scoped definitions for reusable CDN transformations. Use them when a derived asset needs a stable URL and predictable cache invalidation.

Create or update a project lens with the SDK's idempotent `client.ensureLens()` method:

**python**

```python
import os
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id=os.environ["CALMLENS_PROJECT_ID"],
)

normal_lens = client.ensure_lens({'identifier': 'picmagic-normal', 'name': 'PicMagic normal', 'type': 'image', 'active': True, 'version': 1, 'transforms': [{'type': 'imageTransform', 'options': {'fit': 'scale-down', 'width': 1024, 'format': 'png'}}]})
with client.cdn.serve({"assetIdentifier": "your-asset-id", "resource": "content", "lens": normal_lens["identifier"]}) as response:
    content = response.read()
print(len(content))
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "encoding/json"
    "io"
    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewClient(calmlens.Options{APIKey: os.Getenv("CALMLENS_API_KEY"), ProjectID: os.Getenv("CALMLENS_PROJECT_ID")})
    if err != nil { log.Fatal(err) }
    ctx := context.Background()
    var options calmlens.EnsureLensOptions
    if err := json.Unmarshal([]byte("{\"identifier\": \"picmagic-normal\", \"name\": \"PicMagic normal\", \"type\": \"image\", \"active\": true, \"version\": 1, \"transforms\": [{\"type\": \"imageTransform\", \"options\": {\"fit\": \"scale-down\", \"width\": 1024, \"format\": \"png\"}}]}"), &options); err != nil { log.Fatal(err) }
    normalLens, err := client.EnsureLens(ctx, options)
    if err != nil { log.Fatal(err) }
    response, err := client.CDN.Serve(ctx, calmlens.CDNServeOptions{CDNAssetURLOptions: calmlens.CDNAssetURLOptions{AssetIdentifier: "your-asset-id", Resource: "content", Lens: normalLens.Identifier}})
    if err != nil { log.Fatal(err) }
    defer response.Body.Close()
    content, err := io.ReadAll(response.Body)
    if err != nil { log.Fatal(err) }
    fmt.Println(len(content))
}

```

**JavaScript**

```javascript
const normalLens = await client.ensureLens({
  identifier: "picmagic-normal",
  name: "PicMagic normal",
  type: "image",
  active: true,
  version: 1,
  transforms: [
    {
      type: "imageTransform",
      options: { fit: "scale-down", width: 1024, format: "png" },
    },
  ],
});

const response = await client.cdn.serve({
  assetIdentifier: assetId,
  resource: "content",
  lens: normalLens.identifier,
});
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.ensure_lens(identifier: "picmagic-normal", name: "PicMagic normal", type: "image", active: true, version: 1, transforms: [{"type" => "imageTransform", "options" => {"fit" => "scale-down", "width" => 1024, "format" => "png"}}])
puts result
client.close
```

**rust**

```rust
use calmlens::{models::*, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    let result = client.ensure_lens(LensDefinition { identifier: "picmagic-normal".into(), name: "PicMagic normal".into(), lens_type: "image".into(), active: true, version: 1, transforms: Patch::Value(vec![Transform::image(ImageTransformOptions { fit: Some("scale-down".into()), width: Some(1024), format: Some("png".into()), ..Default::default() })]), ..Default::default() }).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensClient;
import com.calmlens.Models.*;

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    var result = client.ensureLens(new LensDefinition().identifier("picmagic-normal").name("PicMagic normal").lensType("image").active(true).version(1).transforms(Patch.value(java.util.Arrays.asList(Transform.image(new ImageTransformOptions().fit("scale-down").width(1024).format("png"))))));
    System.out.println(result);
  }
}
```

**curl**

```sh
curl -X PUT "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/lenses" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "lenses": [
    {
      "identifier": "picmagic-normal",
      "name": "PicMagic normal",
      "type": "image",
      "active": true,
      "version": 1,
      "transforms": [
        {
          "type": "imageTransform",
          "options": {
            "fit": "scale-down",
            "width": 1024,
            "format": "png"
          }
        }
      ]
    }
  ]
}'

curl "https://cdn.calmlens.com/v1/assets/$CALMLENS_PROJECT_ID/$ASSET_ID/content/@picmagic-normal"
```

Versioning rules:

- Create a new lens with any positive version.
- Repeating the same version and complete definition is idempotent, so it is safe to run on every deploy.
- Any other update must use a version greater than the current version; versions do not need to be consecutive.
- Reusing or lowering the current version for a different definition returns `409 lens/version-conflict`.
- Lens identifiers contain lowercase letters, numbers, and hyphens and are at most 50 characters.

To change the width or output format, submit the complete new definition with version `2`. Always submit the complete definition when updating a lens. CalmLens includes the resolved lens version in its CDN cache and ETag identities, so a new version never serves a cached result from the prior definition. SDK lens URLs use the required `@` prefix automatically.

To ensure multiple lenses in one request, use `client.ensureLenses()`:

**JavaScript**

```javascript
const [normalLens, uhdLens] = await client.ensureLenses({
  lenses: [normalDefinition, uhdDefinition],
});
```

**python**

```python
import os
import json
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id="your-project-id",
)

options = json.loads("{\"lenses\":[{\"identifier\":\"picmagic-normal\",\"name\":\"PicMagic normal\",\"type\":\"image\",\"active\":true,\"version\":1,\"transforms\":[{\"type\":\"imageTransform\",\"options\":{\"fit\":\"scale-down\",\"width\":1024,\"format\":\"png\"}}]},{\"identifier\":\"picmagic-uhd\",\"name\":\"PicMagic UHD\",\"type\":\"image\",\"active\":true,\"version\":1,\"transforms\":[{\"type\":\"imageTransform\",\"options\":{\"width\":3840,\"format\":\"png\"}}]}]}")
response = client.ensure_lenses(options)
print(response)
```

**go**

```go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"

    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewClient(calmlens.Options{
        APIKey: os.Getenv("CALMLENS_API_KEY"),
        ProjectID: "your-project-id",
    })
    if err != nil { log.Fatal(err) }

    var options calmlens.EnsureLensesOptions
    if err := json.Unmarshal([]byte("{\"lenses\":[{\"identifier\":\"picmagic-normal\",\"name\":\"PicMagic normal\",\"type\":\"image\",\"active\":true,\"version\":1,\"transforms\":[{\"type\":\"imageTransform\",\"options\":{\"fit\":\"scale-down\",\"width\":1024,\"format\":\"png\"}}]},{\"identifier\":\"picmagic-uhd\",\"name\":\"PicMagic UHD\",\"type\":\"image\",\"active\":true,\"version\":1,\"transforms\":[{\"type\":\"imageTransform\",\"options\":{\"width\":3840,\"format\":\"png\"}}]}]}"), &options); err != nil { log.Fatal(err) }
    response, err := client.EnsureLenses(context.Background(), options)
    if err != nil { log.Fatal(err) }
    fmt.Println(response)
}
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.ensure_lenses(lenses: [{"identifier" => "picmagic-normal", "name" => "PicMagic normal", "type" => "image", "active" => true, "version" => 1, "transforms" => [{"type" => "imageTransform", "options" => {"fit" => "scale-down", "width" => 1024, "format" => "png"}}]}, {"identifier" => "picmagic-uhd", "name" => "PicMagic UHD", "type" => "image", "active" => true, "version" => 1, "transforms" => [{"type" => "imageTransform", "options" => {"width" => 3840, "format" => "png"}}]}])
puts result
client.close
```

**rust**

```rust
use calmlens::{models::*, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    let result = client.ensure_lenses(EnsureLenses { lenses: vec![LensDefinition { identifier: "picmagic-normal".into(), name: "PicMagic normal".into(), lens_type: "image".into(), active: true, version: 1, transforms: Patch::Value(vec![Transform::image(ImageTransformOptions { fit: Some("scale-down".into()), width: Some(1024), format: Some("png".into()), ..Default::default() })]), ..Default::default() }, LensDefinition { identifier: "picmagic-uhd".into(), name: "PicMagic UHD".into(), lens_type: "image".into(), active: true, version: 1, transforms: Patch::Value(vec![Transform::image(ImageTransformOptions { width: Some(3840), format: Some("png".into()), ..Default::default() })]), ..Default::default() }], ..Default::default() }).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensClient;
import com.calmlens.Models.*;

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    var result = client.ensureLenses(new EnsureLenses().lenses(java.util.Arrays.asList(new LensDefinition().identifier("picmagic-normal").name("PicMagic normal").lensType("image").active(true).version(1).transforms(Patch.value(java.util.Arrays.asList(Transform.image(new ImageTransformOptions().fit("scale-down").width(1024).format("png"))))), new LensDefinition().identifier("picmagic-uhd").name("PicMagic UHD").lensType("image").active(true).version(1).transforms(Patch.value(java.util.Arrays.asList(Transform.image(new ImageTransformOptions().width(3840).format("png"))))))));
    System.out.println(result);
  }
}
```

**curl**

```sh
curl -X PUT "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/lenses" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "lenses": [
    {
      "identifier": "picmagic-normal",
      "name": "PicMagic normal",
      "type": "image",
      "active": true,
      "version": 1,
      "transforms": [
        {
          "type": "imageTransform",
          "options": {
            "fit": "scale-down",
            "width": 1024,
            "format": "png"
          }
        }
      ]
    },
    {
      "identifier": "picmagic-uhd",
      "name": "PicMagic UHD",
      "type": "image",
      "active": true,
      "version": 1,
      "transforms": [
        {
          "type": "imageTransform",
          "options": {
            "width": 3840,
            "format": "png"
          }
        }
      ]
    }
  ]
}'
```

Use the single-lens endpoint for one definition or the bulk endpoint to ensure several definitions in one request. The endpoint details below document the complete REST request and response schemas.

## Lens API reference

See the [Lenses reference](/docs/api/lenses) for objects, request schemas and endpoint details.

### The lens object

[View lens fields](/docs/api/lenses#the-lens-object).

### Endpoint details

[Ensure a lens](/docs/api/lenses#ensure-lens) or [ensure several lenses](/docs/api/lenses#ensure-lenses).
