Image.Plug.Pipeline.Normaliser (image_plug v0.1.0)

Copy Markdown View Source

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:

  1. %Trim{} — trim has to run before resize so the resize sees only the meaningful pixels.

  2. %Background{} — flatten alpha against the chosen background before any colour-shifting op runs.

  3. %Resize{} — runs as early as possible so libvips can shrink-on-load.

  4. %Rotate{} and %Flip{} — Cloudflare's rotate is free-angle and runs post-resize.

  5. %Border{} — embedded after resize because it grows the canvas.

  6. %Adjust{} — Sharp's "modulate" stage. Runs after geometry so the new pixels participate in tone shifts.

  7. %Colorspace{} — runs after %Adjust{} (whose multipliers expect RGB) and before %ReplaceColor{} (whose chroma-key match operates on the post-conversion bytes).

  8. %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.

  9. %Blur{} — Sharp explicitly runs blur before sharpen.

  10. %Sharpen{}.

  11. %Draw{} — composite layers go on top of the finished base.

  12. %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 multiplier 1.0

  • %Sharpen{sigma: 0}, %Blur{sigma: 0}

  • %Border{} with every side 0

  • %Trim{mode: :explicit} with every side 0

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

normalise(pipeline)

@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

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]