Skip to main content

guides/image_kit_conformance.md

# ImageKit conformance

This guide documents `image_plug`'s conformance to the [ImageKit URL grammar](https://imagekit.io/docs/transformations) — what we implement, where we differ, and what we deliberately don't ship.

The reference is [ImageKit's published transformation docs](https://imagekit.io/docs/transformations). When this guide and ImageKit's docs disagree, treat ImageKit's docs as the contract and file an issue against `image_plug`.

## URL forms

| Form | ImageKit | `image_plug` | Notes |
| --- | --- | --- | --- |
| `<host>/<endpoint>/tr:<transforms>/<source>` (path-prefix) ||| The default form. Source path resolved by the configured `Image.Plug.SourceResolver`. |
| `<host>/<endpoint>/<source>?tr=<transforms>` (query-string) ||| Both forms recognised on inbound. The component-side adapter emits the path-prefix form. |
| `<host>/<endpoint>/<source>` (no transforms) ||| Passthrough — the source is streamed unchanged. |
| Chained transforms (`tr:w-200,h-100:rt-90`) || ⚠️ | Recognised; the v0.1 IR doesn't model multi-stage pipelines, so all stages flatten to one comma-joined option set. Order-dependent recipes collapse to last-write-wins. |
| Signed URLs (`?ik-s=<hex>` + `?ik-t=<unix>`) ||| HMAC-SHA1 over the path-and-query (excluding `ik-s`). Wire-format-compatible with ImageKit's hosted signed URLs. |
| Remote-image endpoint (absolute `https://...` source) ||| Recognised; resolved by `SourceResolver.HTTP` when configured. |
| Named transformations (`tr:n-<name>` or `t-<name>`) ||| Server-side aliases; not modelled by the IR. Returns `:unsupported_option`. |

## Provider configuration

```elixir
plug Image.Plug,
  provider: {Image.Plug.Provider.ImageKit,
             mount: "",
             endpoint: "your_imagekit_id",
             strict?: true,
             signing: %{keys: [secret], required?: true}},
  source_resolver: ...
```

* `:mount` — path prefix to strip first. Defaults to `""` (root).

* `:endpoint` — additional path segment to strip after `:mount`. ImageKit URLs commonly include a per-account endpoint id (e.g. `/your_imagekit_id/`). Defaults to `""`.

* `:strict?``true` (default) rejects unknown ImageKit option keys with `:unknown_option`. `false` logs and ignores.

* `:signing``nil` or `%{keys: [...], required?: bool}`. Wire format matches ImageKit's hosted SHA-1 signed URLs.

## Option-key conformance

Every option ImageKit documents in [the transformation reference](https://imagekit.io/docs/transformations). ✅ = full conformance. ⚠️ = partial / behavioural difference. ❌ = not implemented.

### Sizing

| Key | Status | Notes |
| --- | --- | --- |
| `w-<n>` || Positive integer. |
| `h-<n>` || Positive integer. |
| `dpr-<n>` | ⚠️ | ImageKit accepts up to `auto`; we cap at 3. |
| `c-maintain_ratio` / `cm-maintain_ratio` || Maps to `Resize{fit: :contain}`. |
| `c-force` / `cm-force` || Maps to `Resize{fit: :squeeze}`. |
| `c-at_least` / `c-at_max` || Maps to `Resize{fit: :scale_down}`. |
| `c-at_max_enlarge` | ⚠️ | Approximated as `:contain`; the upscaling-when-smaller behaviour is not implemented. |
| `c-extract` / `cm-extract` || Maps to `Resize{fit: :crop}` (absolute-pixel crop). |
| `c-pad_extract` / `cm-pad_extract` || Maps to `Resize{fit: :pad}`. |
| `c-pad_resize` / `cm-pad_resize` || Maps to `Resize{fit: :pad}`. |
| `fo-<position>` || `top`, `bottom`, `left`, `right`, `top_left`, etc. → compass gravities. |
| `fo-face` || Face-aware crop via YuNet when the optional [`:image_vision`](https://hex.pm/packages/image_vision) dep is loaded; falls back to libvips' `:attention` saliency crop otherwise. |
| `fo-auto` | ⚠️ | Maps to libvips' `:entropy` crop; ImageKit's content-aware crop is approximated. |
| `fo-custom` + `x-<n>` + `y-<n>` || 0..1 normalised focal point. |

### Format / output

| Key | Status | Notes |
| --- | --- | --- |
| `q-<n>` || 1..100. |
| `f-jpg` / `f-jpeg` / `f-png` / `f-webp` / `f-avif` || |
| `f-auto` || Same Accept-driven negotiation as the other providers. |
| `lo-true` / `lo-false` (lossless) || Sets `Format.lossy`; threaded to libvips for WebP / AVIF (lossless wire format) and PNG (palette quantisation). |
| `pr-true` / `pr-false` (progressive) || Sets `Format.progressive`; threaded to libvips on JPEG / PNG. |
| `cp-<n>` (chroma subsampling) || `cp-0` = `:auto` (libvips default); `cp-1` = `:on` (4:2:0); `cp-2` / `cp-3` = `:off` (4:4:4 full chroma). Threaded to libvips on JPEG / AVIF. |

### Effects

| Key | Status | Notes |
| --- | --- | --- |
| `bg-<hex>` || Hex (with or without leading `#`). |
| `e-blur-<n>` || 0..2000; mapped to libvips sigma via `sigma = N / 100`. |
| `e-sharpen-<n>` || 0..100; `sigma = N / 10`. |
| `e-usm-<radius>-<sigma>-<amount>-<threshold>` | ⚠️ | Approximated by mapping the `<sigma>` component to libvips `Sharpen` sigma; the radius/amount/threshold tweaks are not modelled. |
| `e-grayscale` / `e-greyscale` || Approximated as `Adjust{saturation: 0}`. |
| `e-contrast` | ⚠️ | ImageKit's auto-contrast toggle; we approximate as `Adjust{contrast: 1.1}` (a mild bump). Real visual difference on low-contrast inputs. |
| `e-shadow` / `e-shadow-bl-<n>_st-<n>_x-<n>_y-<n>_c-<hex>` || Wraps `Image.drop_shadow/2`. Each component is optional; defaults: `bl=10` (sigma 5.0), `st=50`, `x=0`, `y=10`, `c=000000`. ImageKit's `bl` is doubled by libvips' Gaussian sigma convention (`sigma = bl / 2`). |
| `e-gradient` || Needs a gradient overlay helper — returns `:unsupported_option`. |
| `e-removedotbg` / `e-bgremove` | ⚠️ | `Image.Background.remove/2` ships in the optional [`:image_vision`](https://hex.pm/packages/image_vision) library (BiRefNet-lite). Not yet wired into an `image_plug` IR op — pending an `Ops.RemoveBackground{}` op behind a `Code.ensure_loaded?/1` guard. |
| `e-changebg` / `e-edit` || Generative-AI calls; not implemented. |
| `e-retouch` | ⚠️ | Maps to `Image.enhance/2`, a sensible-defaults stack of luminance equalisation + saturation boost + mild sharpen. ImageKit's hosted version is ML-driven; output is visually similar but not byte-identical. |
| `e-upscale` || Model-driven super-resolution; not implemented. |

### Geometry

| Key | Status | Notes |
| --- | --- | --- |
| `rt-<n>` | ⚠️ | ImageKit accepts arbitrary integer plus `auto`; we accept multiples of 90 only. |
| `b-<W>_<color>` / `b-<W>-<color>` || Uniform-width border. Per-side border not supported in ImageKit's grammar. |
| `r-<n>` / `r-max` (rounded corners) || Not implemented in v0.1. |

### Overlays

| Key | Status | Notes |
| --- | --- | --- |
| `oi-<image-path>` | ⚠️ | Single-layer base form supported; the path is resolved through the configured `SourceResolver`. ImageKit's nested overlay syntax (`l-image,i-<path>,...,l-end`) is not implemented. |
| `ot-<text>` || Text overlays not implemented in v0.1. |
| `obg-<color>` || Overlay-background not implemented. |

### Misc

| Key | Status | Notes |
| --- | --- | --- |
| `ik-s` || HMAC signature (handled by `Image.Plug.Provider.ImageKit.Signing`). |
| `ik-t` || Used by signing; the verifier rejects after this unix-seconds timestamp. |
| `t-<name>` || Named (server-side alias) transformations not modelled by the IR. Returns `:unsupported_option`. |
| `ar-<W>-<H>` (aspect-ratio shortcut) || When given alongside exactly one of `w`/`h`, derives the other from the ratio. With both `w` and `h` already explicit, `ar-` is a no-op. |
| `z-<n>` (zoom) | ⚠️ | Acts on the largest detected face when the optional [`:image_vision`](https://hex.pm/packages/image_vision) dependency is loaded. `0.0` keeps loose context, `1.0` tight-crops to the face bounding box. Without `:image_vision`, the option is parsed but does not affect the output (the regular thumbnail / `fo-` gravity flow still runs). |

## Behavioural differences

### Multi-stage chained transforms collapse to one stage

ImageKit lets you chain transforms with `:`: `tr:w-200,h-100:rt-90:e-blur-300`. The v0.1 IR doesn't model multi-pass pipelines — all options compose into one Resize op, one Adjust op, etc.

The provider flattens chained stages by joining them with `,` and processes them as a single set. For most useful transforms (resize + format + quality + a single effect) this is identical to ImageKit. Recipes that genuinely require ordering lose information: only the last write per op kind survives.

### Path-prefix vs query-string

ImageKit accepts both `tr:w-200/sample.jpg` and `sample.jpg?tr=w-200`. The provider recognises both — and even accepts a mix (`tr:w-200/sample.jpg?tr=q-80` produces the merged set `w-200,q-80`). The component-side adapter only emits the path-prefix form (the documented default). Mixing is permitted on inbound for compatibility with hand-rolled URLs.

### Canonical-string for signing

ImageKit's HMAC payload is the path-and-query of the request, with the `ik-s` parameter removed but the `ik-t` parameter retained. The hash is HMAC-SHA1 keyed by the secret, hex-encoded lowercase. We replicate this exactly. URLs signed by ImageKit's hosted service verify against an `image_plug` deployment with the same secret, and vice-versa.

### `e-contrast` is approximated

ImageKit's `e-contrast` is a single boolean toggle (auto-contrast on the input). The Image library doesn't expose a content-aware contrast knob, so we approximate as `Adjust{contrast: 1.1}` — a fixed mild bump. Visible on low-contrast inputs; close-enough on the rest.

## Conformance summary

| Category | Conformance | Notes |
| --- | --- | --- |
| URL forms | Full | Path-prefix + query-string + signed all wire-compatible; chained transforms flatten. |
| Sizing options (`w`/`h`/`c-`/`fo-`/`x`/`y`) | High | `c-at_max_enlarge` and `fo-auto` approximated. |
| Output format (`f-`, `q-`, `dpr-`) | High | `lo-`/`pr-`/`cp-` deferred. |
| Effects (`bg-`/`e-blur`/`e-sharpen`/`e-grayscale`/`e-contrast`/`e-usm`) | Medium | Common effects work; shadow / gradient / AI-driven calls deferred. |
| Geometry (`rt-`/`b-`) | Medium | `rt-` 90-multiples only; `r-` (rounded corners) not implemented. |
| Overlays (`oi-`) | Partial | Base layer form only. |
| Signed URLs | Full | Wire-format-compatible with ImageKit's hosted service. |

## Reporting gaps

Open an issue at the project's GitHub. Include the request URL, the expected behaviour per ImageKit's docs (with a link), and the actual response.