PhoenixKit.Modules.Storage.ImageEdit (phoenix_kit v2.37.1)

Copy Markdown View Source

The description of an image edit, and the ImageMagick arguments that apply it. Pure: no database, no files.

An edit is a map with string keys (it is stored as JSON on the file) and is always applied to the unedited original, in this fixed order:

  1. "rotate" — 0, 90, 180 or 270 degrees clockwise
  2. "flip_h", "flip_v" — mirror left-right / top-bottom
  3. "straighten" — a small rotation, −45..45 degrees clockwise, scaled about the centre just enough that no blank corner shows
  4. "redact" — regions to hide, each %{"x", "y", "w", "h", "style"}
  5. "crop"%{"x", "y", "w", "h"}
  6. "brightness", "contrast" — −100..100

Regions and the crop are percentages (0..100) of the image as it stands after steps 1–3 — what the editor shows. Keeping redaction before the crop means changing the crop never moves a redacted area.

Redaction styles really remove the detail rather than soften it: a Gaussian blur of known strength can be partly undone, so "blur" first shrinks the region to a coarse grid (every cell becomes one averaged colour) and only then scales it back up smoothly; "pixelate" scales the same grid back with hard edges; "fill" paints the region black.

An edit that changes nothing normalises to nil.

Summary

Types

t()

A normalised edit (string keys, as stored).

Functions

The largest crop of pixel shape aw:ah, centred in the frame the edit produces from an original of size.

The size of the frame the percentages refer to — the image after rotation (a quarter turn swaps the sides); flipping and straightening keep it.

Whether the edit moves pixels (rotate, flip, straighten or crop), as opposed to only changing their values (redaction, brightness, contrast). Annotations are drawn in the unedited image's pixel space, so a geometric edit would misplace them.

The parts of an edit that move pixels (see geometric?/1), for comparing two edits: annotations stay in place as long as these do not change.

ImageMagick (convert) arguments that apply edit to the first frame of input, whose size after auto-orient is size, and write output.

Mirrors the result :horizontally (left–right) or :vertically, keeping the crop and the redacted areas on the pixels they cover. A straightening turns the other way in a mirror, so its sign flips too.

Validates and normalises an edit from a form or an API caller (atom or string keys, numbers or numeric strings). Values are clamped into range and anything that changes nothing is dropped; {:ok, nil} means "no edit".

The size of the result, for an original of size (after auto-orient).

The scale that keeps a w x h frame fully covered after rotating the image by degrees about its centre (either direction).

Turns the result a quarter to the :left or :right, keeping the crop and the redacted areas on the pixels they cover.

Types

t()

@type t() :: %{optional(String.t()) => term()}

A normalised edit (string keys, as stored).

Functions

centred_crop(edit, arg, size)

@spec centred_crop(
  t() | nil,
  {pos_integer(), pos_integer()},
  {pos_integer(), pos_integer()}
) :: map()

The largest crop of pixel shape aw:ah, centred in the frame the edit produces from an original of size.

iex> PhoenixKit.Modules.Storage.ImageEdit.centred_crop(%{}, {1, 1}, {400, 200})
%{"x" => 25.0, "y" => 0.0, "w" => 50.0, "h" => 100.0}

frame_size(arg1, size)

@spec frame_size(t() | nil, {pos_integer(), pos_integer()}) ::
  {pos_integer(), pos_integer()}

The size of the frame the percentages refer to — the image after rotation (a quarter turn swaps the sides); flipping and straightening keep it.

iex> PhoenixKit.Modules.Storage.ImageEdit.frame_size(%{"rotate" => 90}, {400, 300})
{300, 400}

geometric?(edit)

@spec geometric?(t() | nil) :: boolean()

Whether the edit moves pixels (rotate, flip, straighten or crop), as opposed to only changing their values (redaction, brightness, contrast). Annotations are drawn in the unedited image's pixel space, so a geometric edit would misplace them.

geometry(edit)

@spec geometry(t() | nil) :: map()

The parts of an edit that move pixels (see geometric?/1), for comparing two edits: annotations stay in place as long as these do not change.

magick_args(edit, size, input, output)

@spec magick_args(t() | nil, {pos_integer(), pos_integer()}, String.t(), String.t()) ::
  [String.t()]

ImageMagick (convert) arguments that apply edit to the first frame of input, whose size after auto-orient is size, and write output.

mirror(edit, axis)

@spec mirror(t() | nil, :horizontal | :vertical) :: t()

Mirrors the result :horizontally (left–right) or :vertically, keeping the crop and the redacted areas on the pixels they cover. A straightening turns the other way in a mirror, so its sign flips too.

iex> PhoenixKit.Modules.Storage.ImageEdit.mirror(%{"straighten" => 5.0}, :horizontal)
%{"flip_h" => true, "straighten" => -5.0}

iex> PhoenixKit.Modules.Storage.ImageEdit.mirror(%{"flip_h" => true}, :horizontal)
%{}

normalize(params)

@spec normalize(map() | nil) :: {:ok, t() | nil} | {:error, atom()}

Validates and normalises an edit from a form or an API caller (atom or string keys, numbers or numeric strings). Values are clamped into range and anything that changes nothing is dropped; {:ok, nil} means "no edit".

iex> PhoenixKit.Modules.Storage.ImageEdit.normalize(%{rotate: -90, flip_h: "true"})
{:ok, %{"rotate" => 270, "flip_h" => true}}

iex> PhoenixKit.Modules.Storage.ImageEdit.normalize(%{"crop" => %{"x" => 0, "y" => 0, "w" => 100, "h" => 100}})
{:ok, nil}

output_size(edit, size)

@spec output_size(t() | nil, {pos_integer(), pos_integer()}) ::
  {pos_integer(), pos_integer()}

The size of the result, for an original of size (after auto-orient).

iex> PhoenixKit.Modules.Storage.ImageEdit.output_size(
...>   %{"rotate" => 90, "crop" => %{"x" => 0, "y" => 0, "w" => 50, "h" => 25}},
...>   {400, 300}
...> )
{150, 100}

straighten_scale(w, h, degrees)

@spec straighten_scale(number(), number(), number()) :: float()

The scale that keeps a w x h frame fully covered after rotating the image by degrees about its centre (either direction).

iex> PhoenixKit.Modules.Storage.ImageEdit.straighten_scale(100, 100, 0)
1.0

turn(edit, direction)

@spec turn(t() | nil, :left | :right) :: t()

Turns the result a quarter to the :left or :right, keeping the crop and the redacted areas on the pixels they cover.

The pipeline mirrors after rotating, so while exactly one mirror is on, a visual turn to the right is a rotation to the left.

iex> PhoenixKit.Modules.Storage.ImageEdit.turn(%{}, :right)
%{"rotate" => 90}

iex> PhoenixKit.Modules.Storage.ImageEdit.turn(%{"flip_h" => true}, :right)
%{"flip_h" => true, "rotate" => 270}

iex> PhoenixKit.Modules.Storage.ImageEdit.turn(
...>   %{"crop" => %{"x" => 10.0, "y" => 20.0, "w" => 30.0, "h" => 40.0}},
...>   :right
...> )
%{"crop" => %{"x" => 40.0, "y" => 10.0, "w" => 40.0, "h" => 30.0}, "rotate" => 90}