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.
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 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.
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/2call with no options, or only an emptypresets:orresize:list, adds no group. - In
format_qualities:and the encoder options such asjpeg_options:,[:unset]alone is written as:unset, and a repeated leading:unsetis 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.
Summary
Functions
Builds a URL configuration for new/1.
Encrypts a UTF-8 source with the configured source encryption keys.
Appends a processing group to the plan.
Starts a builder with an empty plan.
Like new/1, with a configuration and request controls.
Sets output and encoding options for the whole request.
Signs an existing mount-relative path with the first configured signing key.
Generates the URL for a builder's plan and a source.
Like url/3, but returns the URL or raises ArgumentError.
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.
Checks the plan as the server would, without generating a URL.
Types
@type t() :: %ImagePipe.URL{ config: ImagePipe.URL.Config.t(), plan: ImagePipe.Plan.t() }
Functions
@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(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-encodedString.t/0) - Signing keys, each at least 64 hex digits (32 bytes), such as the output ofopenssl rand -hex 32. A shorter key raisesArgumentError. 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(boolean/0) - Encrypts the source, and any watermark source, in generated URLs, which then useenc/<token>instead ofsrc/<path>. Requires:source_encryption_keys. A server with encryption keys decryptsenc/sources regardless of this option. The default value isfalse.:source_encryption_keys(list of hex-encodedString.t/0) - Keys that encrypt and decrypt sources, each 32 bytes written as 64 hex digits in either case, such as the output ofopenssl 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(:deterministicor:random) - HowImagePipe.URL.url/3picks the initialization vector for an encrypted source.:deterministicderives it from the source with a secret key, so one source always gives the same URL.:randomgives a new URL on every call. The default value is:deterministic.:validate_against(keyword/0) - What the server has configured, soImagePipe.URL.validate/1andImagePipe.URL.url/3check 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/2returns the URL configuration with this filled in, andImagePipe.config/1doesn't accept it.:presets(map/0) - The server's presets: preset names mapped to option fragments such as"w=400/h=300/fit=cover", or toImagePipe.URLbuilders. The default value is%{}.:request_defaults(String.t/0orImagePipe.URL.t/0) - The server's request defaults, as a fragment or builder.:preset_lookup(boolean/0) - Set totruewhen 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 isfalse.:watermarks(list ofatom/0) - The server's watermark names, such as[:logo]. Awatermark: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:presetsas well.:request_watermarks(boolean/0) - The server'srequest_watermarkssetting: whether it accepts watermark sources that a request names. Withfalse, awatermark_source:fails the check. Without this option, watermark sources aren't checked. The check covers presets too, so list the server's:presetsas well.
@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.
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,
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.
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. Checks
that involve several options, such as extend with extend_ratio, happen
in validate/1.
@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.
:orient(:autoor:none) -:autoapplies the original's EXIF orientation, and:noneignores it. The URL option isorient.:page(non_neg_integer/0) - The zero-based page or frame of the original to use. The URL option ispage.:filename(String.t/0) - The file name without extension, of ASCII letters, digits,.,_, and-. The response's extension is appended. The URL option isfilename.:attachment(boolean/0) - Makes browsers download the response. The URL option isattachment.:cachebuster(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 iscb.:expires(pos_integer/0) - A Unix time in seconds. The URL stops working after that second. The URL option isexpires.:debug(boolean/0) - Adds debug response headers when the server allows them withallow_debug_headers. The URL option isdebug.
@spec new(ImagePipe.URL.Config.t(), keyword()) :: t()
Like new/1, with a configuration and request controls.
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
options of the URL, under the names listed under
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.
@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.
@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- withencrypt_source: true, overrides the configured:iv_modefor this URL.:deterministicand:randomwork as the:iv_modevalues. 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/1returns an error: a mistake the builder recorded, 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:ivwas given and the configuration doesn't haveencrypt_source: true.{:error, :invalid_encryption_options}- an option other than:iv, or a malformed:iv.
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.
@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 infit=!fill. No option accepts a value that starts with!, so the server rejects it. - Options that
validate/1reports 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 asnil, 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.
@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. 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.