# `ImagePipe.URL`
[🔗](https://github.com/hlindset/image_pipe/blob/v0.1.0/image_pipe_url/lib/image_pipe/url.ex#L1)

Builds processing plans and generates signed ImagePipe URLs.

    config = ImagePipe.URL.config(keys: [signing_key], base_url: "https://cdn.example.com/images")

    builder =
      ImagePipe.URL.new(config)
      |> ImagePipe.URL.group(resize: [width: 400, height: 300, fit: :cover], anchor: :smart)
      |> ImagePipe.URL.output(format: :webp, quality: 82)

    {:ok, []} = ImagePipe.URL.validate(builder)
    url = ImagePipe.URL.url!(builder, "images/cat.jpg")
    # "https://cdn.example.com/images/sig=…/w=400/h=300/fit=cover/anchor=smart/format=webp/q=82/src/images%2Fcat.jpg"

A builder is an immutable value that holds a plan and a URL configuration.
Every call returns a new builder, so a builder can be shared and extended
with ordinary functions. Building and generating URLs reads no source,
image, or cache.

An app that serves its own URLs passes the same `base_url`, `keys`, and
source encryption options to `ImagePipe.config/1`. Preset definitions belong to the
server, and URLs carry only preset names. An app that builds URLs for an
image service running elsewhere must match the settings listed in
[Shared URL settings](https://hexdocs.pm/image_pipe/shared-url-settings.html).

## URL option names

Builder options are the URL options with longer names. The URL syntax and
each option's effect are documented in
[Requesting images](https://hexdocs.pm/image_pipe/requesting-images.html)
and the processing pages it links to, with an Elixir example for each
option.

A URL option with a hyphen uses an underscore in the builder, as in
`extend-at` and `extend_at:`. These names differ:

| URL | Builder |
| --- | --- |
| `w`, `h`, `min-w`, `min-h` | `resize: [width:, height:, min_width:, min_height:]` |
| `fit`, `enlarge`, `zoom` | `resize: [fit:, enlarge:, zoom:]` |
| `pad` | `padding:` |
| `bg` | `background:` |
| `preset` | `presets:` |
| `wm`, `wm-src64` | `watermark:`, `watermark_source:` |
| `wm-opacity` and other `wm-` options | `watermark_opacity:` and other `watermark_` options |
| `cb` | `cachebuster:` in `new/1` |
| `output` | `terminal:` |
| `q`, `format-q` | `quality:`, `format_qualities:` |
| `meta` | `metadata:` |
| `profile` | `color_profile:` |
| `progressive` in `jpeg-options` | `interlace:` |
| `subsample` in `jpeg-options` and `avif-options` | `subsample_mode:` |

Named values are atoms, with an underscore for a hyphen: `anchor=top-left`
is `anchor: :top_left`. These values differ:

| URL | Builder |
| --- | --- |
| `flip=h`, `v`, `hv` | `flip: :horizontal`, `:vertical`, `:both` |
| `trim-symmetry=h`, `v`, `hv` | `trim_symmetry: :horizontal`, `:vertical`, `:both` |
| `profile=preserve` | `color_profile: :preserve_source` |
| `profile=srgb`, `display-p3`, `adobe-rgb` | `color_profile: {:convert, :srgb}`, `{:convert, :display_p3}`, `{:convert, :adobe_rgb}` |
| `hdr=tonemap` | `hdr: :tone_map` |
| `output=info,blurhash` | `terminal: {:info, [:blurhash]}` |
| `wm-src64=<base64>` | `watermark_source: "logos/mark.png"`, the plain source |

An option with several comma-separated values in the URL, such as
`gradient`, takes a keyword list, as in
`gradient: [opacity: 0.8, color: "black", direction: :left]`. With
`encrypt_source: true` in the configuration, `watermark_source:` is
written as `wm-enc`. Lengths, percentages, and colors are described under
[option values](https://hexdocs.pm/image_pipe/requesting-images.html#option-values).

## Mistakes in options

`new/1`, `new/2`, `group/2`, and `output/2` check each option as it's
given, and record a mistake in the plan instead of raising. `validate/1`,
`url/3`, `url!/3`, and `url_with_issues/3` report it as an
`ImagePipe.Plan.Spec.Issue`:

    builder = ImagePipe.URL.new() |> ImagePipe.URL.group(resize: [width: 300, fit: :fill])

    ImagePipe.URL.validate(builder)
    # {:error,
    #  [%ImagePipe.Plan.Spec.Issue{reason: :invalid_value,
    #     locations: [{:group, 0, :fit}],
    #     detail: "invalid value for :fit option: expected one of [:contain, :cover, :stretch, :auto], got: :fill",
    #     severity: :error}]}

    ImagePipe.URL.url!(builder, "photo.jpg")
    # ** (ArgumentError) cannot build URL: invalid_value at {:group, 0, :fit}

An unknown option name or a malformed value is an error. The builder
repairs a mistake with one clear meaning, and reports it as a warning:

  * An option given twice in one call, or a key repeated inside a list
    value such as `gradient:`, keeps its last value.
  * A `group/2` call with no options, or only an empty `presets:` or
    `resize:` list, adds no group.
  * In `format_qualities:` and the encoder options such as
    `jpeg_options:`, `[:unset]` alone is written as `:unset`, and a
    repeated leading `:unset` is written once.

Options that aren't a keyword list raise `ArgumentError`.

`url_with_issues/3` returns a URL despite errors, which the server
rejects, so a page can show a broken image instead of failing.

# `t`

```elixir
@type t() :: %ImagePipe.URL{
  config: ImagePipe.URL.Config.t(),
  plan: ImagePipe.Plan.t()
}
```

# `config`

```elixir
@spec config(keyword()) :: ImagePipe.URL.Config.t()
```

Builds a URL configuration for `new/1`.

    config =
      ImagePipe.URL.config(
        base_url: "https://cdn.example.com/images",
        keys: [System.fetch_env!("IMAGE_PIPE_SIGNING_KEY")]
      )

An app that serves its own URLs passes the same options, except
`:validate_against`, to `ImagePipe.config/1`, so the server verifies
signatures and decrypts sources with the same keys. Invalid
options raise `ArgumentError`, and the message never includes a key.

## Options

* `:base_url` (`t:String.t/0`) - Prefix for generated URLs: an HTTP(S) URL such as
  `"https://cdn.example.com/images"`, a root-relative path such as
  `"/images"`, or a relative path such as `"images"`. A trailing `/` is
  removed. Path segments use unescaped ASCII letters, digits, `-`, `.`,
  `_`, or `~`. A base URL with credentials, a query string, a fragment,
  an empty segment, or a `.` or `..` segment raises. With the default,
  URLs start with `/`, such as `/w=400/src/cat.jpg`. The default value is `""`.

* `:keys` (list of hex-encoded `t:String.t/0`) - Signing keys, each at least 64 hex digits (32 bytes), such as
  the output of `openssl rand -hex 32`. A shorter key raises
  `ArgumentError`. The first key signs generated URLs. The server
  accepts a signature from any key in the list, so keep an old
  key while URLs signed with it are in use. Without keys, URLs
  are unsigned and the server rejects signed ones. The default value is `[]`.

* `:encrypt_source` (`t:boolean/0`) - Encrypts the source, and any watermark source, in generated URLs,
  which then use `enc/<token>` instead of `src/<path>`. Requires
  `:source_encryption_keys`. A server with encryption keys
  decrypts `enc/` sources regardless of this option. The default value is `false`.

* `:source_encryption_keys` (list of hex-encoded `t:String.t/0`) - Keys that encrypt and decrypt sources, each 32 bytes written as
  64 hex digits in either case, such as the output of
  `openssl rand -hex 32`. The first key encrypts. The server
  decrypts with any key in the list. The keys must differ from
  the signing keys, and setting them requires `:keys`. The default value is `[]`.

* `:iv_mode` (`:deterministic` or `:random`) - How `ImagePipe.URL.url/3` picks the initialization vector for an
  encrypted source. `:deterministic` derives it from the source with
  a secret key, so one source always gives the same URL.
  `:random` gives a new URL on every call. The default value is `:deterministic`.

* `:validate_against` (`t:keyword/0`) - What the server has configured, so `ImagePipe.URL.validate/1` and
  `ImagePipe.URL.url/3` check plans as the server does. It never changes
  a generated URL, but a copy that differs from the server gives wrong
  validation results. In an app that serves its own URLs,
  `ImagePipe.url_config/2` returns the URL configuration with this
  filled in, and `ImagePipe.config/1` doesn't accept it.

  * `:presets` (`t:map/0`) - The server's presets: preset names mapped to option fragments
    such as `"w=400/h=300/fit=cover"`, or to `ImagePipe.URL` builders. The default value is `%{}`.

  * `:request_defaults` (`t:String.t/0` or `t:ImagePipe.URL.t/0`) - The server's request defaults, as a fragment or builder.

  * `:preset_lookup` (`t:boolean/0`) - Set to `true` when the server has a preset lookup. For a plan
    that names a preset missing from `:presets`, the groups that
    name it and the request-wide options are not checked, because
    only the server can resolve the name. The other groups are
    still checked. The default value is `false`.

  * `:watermarks` (list of `t:atom/0`) - The server's watermark names, such as `[:logo]`. A `watermark:`
    name missing from the list fails the check. Without this
    option, watermark names aren't checked. The check covers
    presets too, so list the server's `:presets` as well.

  * `:request_watermarks` (`t:boolean/0`) - The server's `request_watermarks` setting: whether it accepts
    watermark sources that a request names. With `false`, a
    `watermark_source:` fails the check. Without this option,
    watermark sources aren't checked. The check covers presets
    too, so list the server's `:presets` as well.

# `encrypt_source`

```elixir
@spec encrypt_source(String.t(), ImagePipe.URL.Config.t(), keyword()) ::
  {:ok, String.t()} | {:error, atom()}
```

Encrypts a UTF-8 source with the configured source encryption keys.

Returns `{:ok, token}`. Place the token after the `enc/` source marker and
sign the complete path with `sign_path/2`. `url/3` does both.

`:iv` overrides the configured `:iv_mode` with `:deterministic`, `:random`,
or an explicit 16-byte binary, as in `url/3`.

Returns `{:error, :source_encryption_disabled}` without
`:source_encryption_keys`, `{:error, :invalid_source}` for an empty or
non-UTF-8 source, and `{:error, :invalid_encryption_options}` for any other
option or a malformed `:iv`.

# `group`

```elixir
@spec group(t(), keyword()) :: t()
```

Appends a processing group to the plan.

    ImagePipe.URL.new()
    |> ImagePipe.URL.group(resize: [width: 400, height: 300, fit: :cover], anchor: :smart)
    |> ImagePipe.URL.group(padding: 12, background: "white")
    # "/w=400/h=300/fit=cover/anchor=smart/-/pad=12,12,12,12/bg=ffffff/src/…"

The options of one call form one group, like the options between two `-`
segments in a URL. They run in the fixed
[processing order](https://hexdocs.pm/image_pipe/processing.html#processing-order),
whatever order they're given in. A second call starts a new group that
processes the previous group's result, with fresh settings: a `dpr: 2` in
the first group doesn't apply to the second.

Resize options go in a `resize:` keyword list. `presets:` names presets to
apply to this group, and the group's own options override them. Option
names and values are listed under
[URL option names](#module-url-option-names).

Any option except `presets:` and `resize:` itself, including each
`resize:` option, accepts `:unset` to remove a value set by the group's
presets or the server's request defaults.

Each value is checked here, and a mistake is recorded in the plan, as
described under [Mistakes in options](#module-mistakes-in-options). Checks
that involve several options, such as `extend` with `extend_ratio`, happen
in `validate/1`.

# `new`

```elixir
@spec new(ImagePipe.URL.Config.t() | keyword()) :: t()
```

Starts a builder with an empty plan.

    ImagePipe.URL.new()
    ImagePipe.URL.new(config)
    ImagePipe.URL.new(config, filename: "cat", expires: 1_767_225_600)
    ImagePipe.URL.new(filename: "cat")

`config` comes from `config/1`. Without it, the builder uses
`config([])`: no base URL, signing, or encryption. The options are request
controls, which apply once to the whole request. An empty plan is valid.
Its URL, such as `/src/cat.jpg`, serves the original in a negotiated
format.

## Options

Each option also accepts `:unset`, which removes a value set by a preset or
the server's request defaults. A mistake in an option is recorded in the
plan, as described under [Mistakes in options](#module-mistakes-in-options).

* `:orient` (`:auto` or `:none`) - `:auto` applies the original's EXIF orientation, and `:none` ignores
  it. The URL option is [`orient`](https://hexdocs.pm/image_pipe/crop.html#orient).

* `:page` (`t:non_neg_integer/0`) - The zero-based page or frame of the original to use. The URL option is
  [`page`](https://hexdocs.pm/image_pipe/request.html#page).

* `:filename` (`t:String.t/0`) - The file name without extension, of ASCII letters, digits, `.`, `_`,
  and `-`. The response's extension is appended. The URL option is
  [`filename`](https://hexdocs.pm/image_pipe/request.html#filename).

* `:attachment` (`t:boolean/0`) - Makes browsers download the response. The URL option is
  [`attachment`](https://hexdocs.pm/image_pipe/request.html#attachment).

* `:cachebuster` (`t:String.t/0`) - A token that makes the server store a copy separate from the one for
  the same URL without it: ASCII letters, digits, `.`, `_`, and `-`. The URL option is
  [`cb`](https://hexdocs.pm/image_pipe/request.html#cb).

* `:expires` (`t:pos_integer/0`) - A Unix time in seconds. The URL stops working after that second. The URL option
  is [`expires`](https://hexdocs.pm/image_pipe/request.html#expires).

* `:debug` (`t:boolean/0`) - Adds debug response headers when the server allows them with
  `allow_debug_headers`. The URL option is [`debug`](https://hexdocs.pm/image_pipe/request.html#debug).

# `new`

```elixir
@spec new(ImagePipe.URL.Config.t(), keyword()) :: t()
```

Like `new/1`, with a configuration and request controls.

# `output`

```elixir
@spec output(t(), keyword()) :: t()
```

Sets output and encoding options for the whole request.

    builder
    |> ImagePipe.URL.output(format: :webp, quality: 82)
    |> ImagePipe.URL.output(quality: 60)
    # "/…/format=webp/q=60/src/…"

The options are the [output and encoding](https://hexdocs.pm/image_pipe/output.html)
options of the URL, under the names listed under
[URL option names](#module-url-option-names).
Each call merges its options into the plan. An option given again replaces
its whole previous value, including every entry of `jpeg_options:` or
`format_qualities:`, and any mistake recorded for it. Options not given
keep their value.

An option the plan doesn't set takes the server's configured default when
the URL is served. `:unset` removes a value set by a preset or the request
defaults, so the configured default applies. `format_qualities:` and the
encoder options such as `jpeg_options:` need at least one entry. Clear them
with `:unset`. When the URL is served, their entries combine with a
preset's and the request defaults' entry by entry. A leading `:unset`, as in
`jpeg_options: [:unset, interlace: true]`, drops those inherited entries
first. A mistake in an option is recorded in the plan, as described under
[Mistakes in options](#module-mistakes-in-options).

# `sign_path`

```elixir
@spec sign_path(String.t(), ImagePipe.URL.Config.t()) :: String.t()
```

Signs an existing mount-relative path with the first configured signing key.

Returns `"/sig=<signature>" <> path`, preserving the exact path bytes without
parsing, escaping, or normalizing them. Pass a config from `config/1`.
The path must start with `/` and exclude any signature prefix, query string,
or fragment. Source query parameters must already be escaped in the path.
Raises `ArgumentError` for an invalid path shape or missing signing keys.

Prepend the mount prefix or hostname to the result yourself; `:base_url`
is not applied. This function performs no source or cache I/O and does not
validate processing options or encrypt sources.

# `url`

```elixir
@spec url(t(), String.t(), keyword()) ::
  {:ok, String.t()}
  | {:error, atom() | {:invalid_request, [ImagePipe.Plan.Spec.Issue.t()]}}
```

Generates the URL for a builder's plan and a source.

    {:ok, url} = ImagePipe.URL.url(builder, "photos/cat.jpg")
    url
    #=> "/images/w=400/src/photos%2Fcat.jpg"

The source is the identifier the image service resolves: a path, an HTTP(S)
URL, an S3 identifier, or a custom scheme. Pass it as a UTF-8 string,
unescaped and with any query parameters, such as
`"https://assets.example.com/cat.jpg?v=2"`. `url/3` escapes it once. A
leading `/` is removed unless the source starts with `//` or the rest is a
URL such as `/https://…`. The sources `.` and `..` are written with the
`src64/` marker, because browsers resolve them as path segments. The
`{:file, path}` and `{:binary, bytes}` inputs of `ImagePipe.run/4` have no
URL.

The URL is the configuration's `:base_url`, then `/sig=<signature>` when
the configuration has `:keys`, then the options and the source. The
signature covers everything after it, so a URL keeps a valid signature
when served under another `:base_url`. The same plan, source, and
configuration always give the same URL, whatever order the options were
given in. Preset names stay in the URL as names, and the server resolves
their current definitions.

An `:expires` set with `new/1` is part of the URL. Computing a new expiry
on every call gives a new URL every time. `url/3` doesn't compare the expiry
with the current time.

## Options

  * `:iv` - with `encrypt_source: true`, overrides the configured `:iv_mode`
    for this URL. `:deterministic` and `:random` work as the `:iv_mode`
    values. A 16-byte binary is used as the IV of the main source, and each
    watermark source derives its own IV from it. An explicit IV must be
    unpredictable, or derived from the complete source with a secret key,
    and must never be used for two different sources under the same key.

## Return values

  * `{:ok, url}`.
  * `{:error, {:invalid_request, issues}}` - `validate/1` returns an error:
    a [mistake the builder recorded](#module-mistakes-in-options), or,
    when the configuration has `:validate_against`, options that conflict.
    Warnings don't stop the URL. Without `:validate_against`, the server
    checks how the options combine when it serves the URL.
  * `{:error, :invalid_source}` - the source is empty, not valid UTF-8, or
    not a string.
  * `{:error, :too_many_options}` - the plan has more than 64 option and
    `-` segments, the most the server accepts.
  * `{:error, :source_encryption_disabled}` - a valid `:iv` was given and
    the configuration doesn't have `encrypt_source: true`.
  * `{:error, :invalid_encryption_options}` - an option other than `:iv`,
    or a malformed `:iv`.

# `url!`

```elixir
@spec url!(t(), String.t(), keyword()) :: String.t()
```

Like `url/3`, but returns the URL or raises `ArgumentError`.

The message names each error's reason and location, as in
`cannot build URL: invalid_value at {:group, 0, :fit}`. It never includes
the source, a key, or an option's value.

# `url_with_issues`

```elixir
@spec url_with_issues(t(), term(), keyword()) ::
  {String.t(), [ImagePipe.Plan.Spec.Issue.t()]}
```

Like `url/3`, but always returns a URL, with the issues `validate/1`
reports and any problem with the source or the number of options.

    ImagePipe.URL.new()
    |> ImagePipe.URL.group(resize: [width: 300, fit: :fill])
    |> ImagePipe.URL.url_with_issues("cat.jpg")
    # {"/w=300/fit=!fill/src/cat.jpg",
    #  [%ImagePipe.Plan.Spec.Issue{reason: :invalid_value,
    #     locations: [{:group, 0, :fit}], severity: :error, …}]}

Where `url/3` would return an error, this returns a URL that the server
rejects with `400`, so a page shows a broken image instead of failing. The
URL is signed as `url/3` signs it, and includes everything `url/3` would
reject:

  * An option the builder rejected is written under its name, with `!`
    before its value, as in `fit=!fill`. No option accepts a value that
    starts with `!`, so the server rejects it.
  * Options that `validate/1` reports as errors, such as options that
    conflict, are written as given.
  * An invalid source is written after `src/`. A source that isn't a
    string, such as `nil`, is written as an empty source. The issue's
    reason is `:invalid_source`.
  * A plan with more than 64 option and `-` segments is written in full.
    The issue's reason is `:too_many_options`.

With `encrypt_source: true`, an invalid source and a rejected
`watermark_source:` are written empty, so no source appears in plain text.

The issues list the errors first. Invalid `options`, as listed under
`url/3`, raise `ArgumentError`.

# `validate`

```elixir
@spec validate(t()) ::
  {:ok, [ImagePipe.Plan.Spec.Issue.t()]}
  | {:error, [ImagePipe.Plan.Spec.Issue.t()]}
```

Checks the plan as the server would, without generating a URL.

    ImagePipe.URL.new(config)
    |> ImagePipe.URL.group(resize: [fit: :cover])
    |> ImagePipe.URL.validate()
    # {:ok,
    #  [%ImagePipe.Plan.Spec.Issue{reason: :inert_option,
    #     locations: [{:group, 0, :fit}], detail: {:requires, :resize},
    #     severity: :warning}]}

Returns `{:ok, warnings}` or `{:error, issues}`, lists of
`ImagePipe.Plan.Spec.Issue` structs. The check applies the server's request
defaults and the presets the plan names, then checks how the options
combine. Options that conflict are errors. Options that have no effect, such
as `fit: :cover` without a width or height, are warnings: the server ignores
them, and `{:error, issues}` lists them after the errors. When
`:validate_against` lists `:watermarks`, it also checks `watermark:` names
against them. It reads no source or cache, so a plan that passes can still
fail on a particular image.

The result also lists the
[mistakes the builder recorded](#module-mistakes-in-options). When one of
them is an error, `validate/1` returns those mistakes and skips the other
checks.

The other checks need `:validate_against` in the URL configuration.
Without it, `validate/1` returns only the builder's mistakes. With
`preset_lookup: true`, only the server can resolve a preset missing from
`:presets`, and that preset can set any option of its group and of the
request. For a plan that names one, the
check skips the groups that name it and the request-wide options, and
checks the other groups. In an app that serves its own URLs,
`ImagePipe.validate/2` runs the full check, including the lookup.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
