Skip to main content

guides/usage.md

# Usage

A practical walk-through of `<.image>` and `<.picture>` and the four URL builders that back them. For an overview of what the library does and the per-CDN feature gap table, see the [README](../README.md).

## The two components

`Image.Components` exposes two function components designed to be `import`ed at the top of any LiveView or HEEx-using module:

```elixir
defmodule MyAppWeb.GalleryLive do
  use MyAppWeb, :live_view
  import Image.Components

  def render(assigns) do
    ~H"""
    <.image src="/uploads/cat.jpg" provider={:cloudflare} width={600} fit={:cover} />

    <.picture
      src="/uploads/cat.jpg"
      provider={:cloudflare}
      formats={[:avif, :webp]}
      width={1200}
    />
    """
  end
end
```

`<.image>` renders a single `<img>`; `<.picture>` renders a `<picture>` with one `<source srcset>` per format in `:formats` (default `[:avif, :webp]`) plus a fallback `<img>`. There is no JavaScript, no LiveView-specific behaviour, and no runtime overhead beyond URL string construction — both compile to plain HTML.

## How a request flows

```
                       ┌─────────────────────────────┐
  HEEx attribute set ─►│ Image.Components.image/1    │
                       │   build_pipeline/1          │
                       │   Image.Components.URL.…/2  │
                       └──────────────┬──────────────┘
                                      │  src="/img/cdn-cgi/image/…/cat.jpg"
                       ┌─────────────────────────────┐
                       │ Browser fetches the URL     │
                       └──────────────┬──────────────┘
                       ┌─────────────────────────────┐
                       │ image_plug forward route    │
                       │   Provider parser           │
                       │   → same Pipeline IR        │
                       │   Interpreter               │
                       │   → libvips transforms      │
                       └──────────────┬──────────────┘
                            transformed bytes back
```

The IR is the contract — the four URL builders project it to URLs; the four `image_plug` providers parse those URLs back to it; the interpreter executes it against `Vix.Vips.Image`. A round-trip from attribute set to served bytes is just two ends of the same struct.

## Choosing a provider

`provider` is required and accepts one of `:cloudflare`, `:cloudinary`, `:imgix`, `:imagekit`. The four URL grammars are very different, but `Image.Components` papers over those differences: the same component call works against any of them.

```heex
<.image src="/cat.jpg" provider={:cloudflare} width={600} fit={:cover} />
<.image src="/cat.jpg" provider={:cloudinary} width={600} fit={:cover} />
<.image src="/cat.jpg" provider={:imgix}      width={600} fit={:cover} />
<.image src="/cat.jpg" provider={:imagekit}   width={600} fit={:cover} />
```

The four `src=` URLs that result are different — but each one, served by a correctly mounted `image_plug` provider, produces the same transformed image.

## Hosts and mounts

`host=` is prepended verbatim. Use it to point at a real CDN edge, or to scope under a path on your own domain:

```heex
<%!-- Self-hosted via image_plug, mounted at /img on this app --%>
<.image src="/cat.jpg" provider={:cloudflare} host="/img" />

<%!-- Real Cloudflare Images (or Cloudflare Workers) --%>
<.image src="/cat.jpg" provider={:cloudflare} host="https://images.example.com" />

<%!-- imgix source --%>
<.image src="/cat.jpg" provider={:imgix} host="https://my-source.imgix.net" />
```

The Cloudflare URL projector adds `/cdn-cgi/image/…` after the host. The Cloudinary projector adds `/<account>/image/upload/…`. Read the per-provider grammar in the README's feature gap table for details.

## Picking a face-aware crop

Use `gravity={:face}` together with `face_zoom`. `face_zoom` defaults to `0.0` which is a loose crop with full padding — usually visually indistinguishable from a centred crop. Set it to a non-zero value (Cloudflare's documented default is `0.6`) to actually see the face-aware behaviour.

```heex
<.image
  src="/portrait.jpg"
  provider={:cloudflare}
  width={300}
  height={300}
  fit={:cover}
  gravity={:face}
  face_zoom={0.6}
/>
```

Behind the scenes, `image_plug`'s `Image.Plug.FaceAware.face_crop/2` invokes `Image.FaceDetection.crop_largest/2` from `image_vision`. If `image_vision` is not in the consumer's deps, the request still succeeds — it falls through to the libvips attention-saliency crop. See [`image_plug`'s face-aware guide](https://hexdocs.pm/image_plug/face_aware.html) for the full story.

`face_zoom` projects to:

* Cloudflare: `face-zoom=<float>`
* Cloudinary: `z_<float>`
* ImageKit: `z-<float>`
* imgix: silently dropped (no equivalent in imgix's URL grammar)

## Adjust effects: per-CDN encoding

`brightness`, `contrast`, `saturation`, `gamma` are all multipliers where `1.0` means "no change", but the four CDNs encode them very differently:

```heex
<%!-- Same intent, four different on-wire URLs --%>
<.image src="/cat.jpg" provider={:cloudflare} contrast={1.4} />
<%!-- → /cdn-cgi/image/contrast=1.4/cat.jpg                      (raw multiplier) --%>

<.image src="/cat.jpg" provider={:cloudinary} contrast={1.4} />
<%!-- → /demo/image/upload/e_contrast:40/cat.jpg                 (centred percentage) --%>

<.image src="/cat.jpg" provider={:imgix} contrast={1.4} />
<%!-- → /cat.jpg?con=40                                          (centred percentage) --%>

<.image src="/cat.jpg" provider={:imagekit} contrast={1.4} />
<%!-- → /demo/cat.jpg                                            (silently dropped) --%>
```

ImageKit's URL grammar has no parameterised contrast — only an unparameterised `e-contrast` toggle. `Image.Components.URL.imagekit/2` faithfully drops the value rather than approximating it. If you need the same visual contrast across all four providers, ImageKit will be the odd one out.

## Vignette and tint

These two are honest single-CDN features:

* `vignette={0.6}` projects to Cloudinary's `e_vignette:60`. The other three providers drop it.
* `tint="#80a0c0"` projects to imgix's `monochrome=80a0c0`. The other three providers drop it.

```heex
<.image src="/cat.jpg" provider={:cloudinary} vignette={0.6} />
<.image src="/cat.jpg" provider={:imgix} tint="#80a0c0" />
```

`tint` accepts a hex string (`"#aabbcc"` or `"aabbcc"`) or an `[r, g, b]` integer list. Both forms are normalised to `[r, g, b]` before they enter the IR, so the type invariant on `Image.Plug.Pipeline.Ops.Tint.color` (`[non_neg_integer()]`) holds regardless of how the component was called.

## `<.picture>` for content negotiation

Use `<.picture>` when you want the browser to pick the best supported format from a short list:

```heex
<.picture
  src="/photo.jpg"
  provider={:cloudflare}
  formats={[:avif, :webp]}
  width={1200}
  fit={:cover}
/>
```

This emits one `<source type="image/avif" srcset="…">`, one `<source type="image/webp" srcset="…">`, and a fallback `<img>` whose `src` uses the `format=` you set explicitly (or the original format if you didn't). The browser walks the `<source>` rows in order and picks the first one whose MIME type it can decode.

The full transform set (width, height, blur, contrast, etc.) is shared across all rows — only the format differs.

## Pre-computing pipelines without rendering

If you need the URL or pipeline without going through HEEx — for example, in a JSON API response or a job that pre-warms a CDN — call the URL builders directly:

```elixir
alias Image.Components.URL
alias Image.Plug.Pipeline
alias Image.Plug.Pipeline.Ops

pipeline = %Pipeline{
  ops: [%Ops.Resize{width: 600, fit: :cover, gravity: :face, face_zoom: 0.6}],
  output: %Ops.Format{type: :webp, quality: 80}
}

URL.cloudflare(pipeline, source_path: "/cat.jpg", host: "/img")
# => "/img/cdn-cgi/image/width=600,fit=cover,gravity=face,face-zoom=0.6,format=webp,quality=80/cat.jpg"
```

Or build the same pipeline from a flat attribute map via the `@doc false` `Image.Components.build_pipeline` helper:

```elixir
pipeline =
  Image.Components.build_pipeline(%{
    width: 600,
    fit: :cover,
    gravity: :face,
    face_zoom: 0.6,
    format: :webp,
    quality: 80
  })
```

`build_pipeline/1` is what `<.image>` and `<.picture>` use internally. It's hidden from ExDoc because it's not part of the stable public API surface, but it's useful for callers that want the same attr-map → IR translation.

## See also

* The README's feature gap table — what each CDN's URL grammar can carry.
* `Image.Components.URL` — module docs for the four URL builders, including provider semantic differences.
* [`image_plug`'s usage guide](https://hexdocs.pm/image_plug/usage.html) — how to mount the four providers in a Phoenix endpoint or Plug.Router.
* [`image_plug`'s face-aware guide](https://hexdocs.pm/image_plug/face_aware.html) — the seam between `image_plug` and `image_vision` and what happens when `image_vision` is absent.
* [`image_playground`](https://github.com/elixir-image/image_playground) — exercises every transform in this guide live, with sliders.