# `PhoenixKit.Modules.Storage.ImageEdit`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.37.1/lib/modules/storage/services/image_edit.ex#L1)

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`.

# `t`

```elixir
@type t() :: %{optional(String.t()) =&gt; term()}
```

A normalised edit (string keys, as stored).

# `centred_crop`

```elixir
@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`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Mirrors the result `:horizontal`ly (left–right) or `:vertical`ly,
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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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}

---

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