Reorders, folds, and validates a pipeline so that two requests with the same semantic effect produce identical fingerprints — and so that order-sensitive libvips operations land in a position where they actually work.
Canonical operation order
Mirrors the order Sharp applies operations internally (Sharp wraps the same libvips primitives this library uses). The order encodes years of experience with libvips' constraints — most importantly that resize must run early enough to benefit from libvips' shrink-on-load fast path, and that operations like blur, sharpen, and modulate must run in a specific bracket around resize.
The ordering this module enforces:
%Trim{}— trim has to run before resize so the resize sees only the meaningful pixels.%Background{}— flatten alpha against the chosen background before any colour-shifting op runs.%Resize{}— runs as early as possible so libvips can shrink-on-load.%Rotate{}and%Flip{}— Cloudflare's rotate is free-angle and runs post-resize.%Border{}— embedded after resize because it grows the canvas.%Adjust{}— Sharp's "modulate" stage. Runs after geometry so the new pixels participate in tone shifts.%Colorspace{}— runs after%Adjust{}(whose multipliers expect RGB) and before%ReplaceColor{}(whose chroma-key match operates on the post-conversion bytes).%ReplaceColor{}— colour-substitution. Runs after Adjust so tone shifts on the source colour have already settled, and before the sigma-based ops so blur/sharpen operate on the post-replace pixels.%Blur{}— Sharp explicitly runs blur before sharpen.%Sharpen{}.%Draw{}— composite layers go on top of the finished base.%Pipeline.Ops.Segment{}— placeholder; ordered last for now.
Cardinality
These ops must appear at most once per pipeline. The provider's options parser already de-duplicates per request, but the normaliser is the source of truth so any future programmatic pipeline-builder gets the same guarantee:
Resize,Trim,Flip,Rotate,Background,Border,Adjust,Colorspace,ReplaceColor,Sharpen,Blur,Segment.
Draw may appear at most once but holds an arbitrary list of
layers internally, so multiple overlay requests collapse onto
layers of one Draw op.
No-op folding
Drops ops whose fields make them semantically inert:
%Resize{width: nil, height: nil}%Rotate{angle: 0}%Flip{direction: nil}%Adjust{}with every multiplier1.0%Sharpen{sigma: 0},%Blur{sigma: 0}%Border{}with every side0%Trim{mode: :explicit}with every side0
Idempotence
normalise(normalise(p)) == normalise(p) for every input.
Errors
Returns {:error, %Image.Plug.Error{tag: :invalid_option}} when
the pipeline contains more than one of an op kind that must be
unique.
Summary
Functions
Normalises a pipeline. See the moduledoc for the rules applied.
Functions
@spec normalise(Image.Plug.Pipeline.t()) :: {:ok, Image.Plug.Pipeline.t()} | {:error, Image.Plug.Error.t()}
Normalises a pipeline. See the moduledoc for the rules applied.
Arguments
pipelineis anImage.Plug.Pipelinestruct.
Returns
{:ok, pipeline}on success.{:error, %Image.Plug.Error{tag: :invalid_option}}on a cardinality violation.
Examples
iex> alias Image.Plug.Pipeline
iex> alias Image.Plug.Pipeline.Ops.{Resize, Rotate, Sharpen}
iex> p =
...> Pipeline.new()
...> |> Pipeline.append(%Sharpen{sigma: 1.0})
...> |> Pipeline.append(%Resize{width: 200})
...> |> Pipeline.append(%Rotate{angle: 90})
iex> {:ok, normalised} = Image.Plug.Pipeline.Normaliser.normalise(p)
iex> Enum.map(normalised.ops, & &1.__struct__)
[Image.Plug.Pipeline.Ops.Resize,
Image.Plug.Pipeline.Ops.Rotate,
Image.Plug.Pipeline.Ops.Sharpen]