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:
"rotate"— 0, 90, 180 or 270 degrees clockwise"flip_h","flip_v"— mirror left-right / top-bottom"straighten"— a small rotation, −45..45 degrees clockwise, scaled about the centre just enough that no blank corner shows"redact"— regions to hide, each%{"x", "y", "w", "h", "style"}"crop"—%{"x", "y", "w", "h"}"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
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
Functions
@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}
@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}
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.
@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.
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)
%{}
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}
@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}
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
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}