# 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.