Editing uploaded images after the fact — crop, rotate, flip, straighten,
redact, brightness and contrast (PhoenixKit.Modules.Storage.ImageEdit).
The model
An edited image keeps its uuid: every module that stored the uuid keeps
showing it, now edited. Its "original" instance becomes the edited bytes,
so every reader of the original — variants, tiles, downloads, public URLs —
gets the edit without knowing about it. The unedited original is not
destroyed: its instance rows move to a hidden, system-managed child file
(backup/1) that is never listed and never served by the public file
routes. The owner reaches it through the editor (download, restore,
delete) and GET /api/files/:uuid/unedited.
The edit is data (file.edits) and is always applied to the unedited
original, so it can be changed or reverted at any time — until the owner
deletes the unedited original, which bakes the edit in. With the media
setting storage_image_edit_mode set to "replace_original", every save
bakes immediately.
While an edit renders
Saving bumps edit_revision and sets edit_state to "pending";
PhoenixKit.Modules.Storage.ApplyImageEditJob renders and swaps.
Until it finishes — and if it fails — every variant of the file is served
as a neutral placeholder: a half-applied redaction must never show the
bytes it hides.
Who may edit
Every function takes opts with :scope (a PhoenixKit.Users.Auth.Scope):
the file's owner, an Owner/Admin, or a holder of the "media" permission
(the admin media library's) may edit. Trusted internal callers pass system: true instead.
Anything else is {:error, :forbidden}.
Summary
Functions
An edited file's hidden unedited backup, or nil.
Whether file is some edited image's hidden backup. Such a file is never
served by the public file routes.
The file_name of an edited image's hidden backup.
Whether scope may edit file (see the moduledoc). Also accepts the
opts keyword the other functions take.
Deletes the unedited original of an edited file, baking the current edit
in. It cannot be undone. The file keeps serving its edited bytes.
Saves an edit of file and queues its rendering. params is anything
ImageEdit.normalize/1 accepts; an edit equal to the current one changes
nothing.
Whether an edit of file is rendering (or failed); it is served as a placeholder.
Whether file can be edited at all: an active, user-owned still image in a
format ImageMagick can write back.
Whether images of mime_type can be edited (ImageMagick writes them back).
Whether file has an unedited backup (its edit can be changed or reverted).
What saving an edit does: "keep_original" (default) keeps the unedited
original as a hidden backup; "replace_original" bakes the edit in.
The media setting key for mode/0.
Queues the rendering again for an edit that failed (or seems stuck).
Reverts file to its unedited original and queues the swap back. Nothing
to revert is {:error, :not_edited}; an annotated file whose edit moved
pixels is {:error, {:annotated, count}}, as for edit/3.
Renders params as a new file (a copy in the same folder, owned by the
file's owner, with edited_from_uuid pointing back). The copy is created
by a job; {:ok, job} means it was queued. Annotations do not matter —
the copy has none. An edit that changes nothing is {:error, :no_edit}.
The instance an edit is applied to: the backup's original when the file has been edited, else the file's own original.
Functions
@spec backup(PhoenixKit.Modules.Storage.File.t()) :: PhoenixKit.Modules.Storage.File.t() | nil
An edited file's hidden unedited backup, or nil.
Whether file is some edited image's hidden backup. Such a file is never
served by the public file routes.
The file_name of an edited image's hidden backup.
@spec can_edit?( PhoenixKit.Modules.Storage.File.t(), PhoenixKit.Users.Auth.Scope.t() | keyword() | nil ) :: boolean()
Whether scope may edit file (see the moduledoc). Also accepts the
opts keyword the other functions take.
@spec delete_unedited_original(PhoenixKit.Modules.Storage.File.t(), keyword()) :: {:ok, PhoenixKit.Modules.Storage.File.t()} | {:error, term()}
Deletes the unedited original of an edited file, baking the current edit
in. It cannot be undone. The file keeps serving its edited bytes.
@spec edit(PhoenixKit.Modules.Storage.File.t(), map(), keyword()) :: {:ok, PhoenixKit.Modules.Storage.File.t()} | {:error, term()}
Saves an edit of file and queues its rendering. params is anything
ImageEdit.normalize/1 accepts; an edit equal to the current one changes
nothing.
Returns {:ok, file} (now pending), or {:error, reason}:
:forbidden,:not_editable,:not_found(deleted meanwhile):not_queued— saved, but the rendering job could not be queued; the file stays a placeholder untilretry/2succeeds{:invalid_edit, reason}fromImageEdit.normalize/1{:annotated, count}— the file has annotations, and the edit changes where pixels are (crop, turn, mirror, straighten differ from the current edit's): they would no longer line up. Redaction and brightness/contrast are allowed; "save as copy" always is.
Whether an edit of file is rendering (or failed); it is served as a placeholder.
@spec editable?(PhoenixKit.Modules.Storage.File.t() | nil) :: boolean()
Whether file can be edited at all: an active, user-owned still image in a
format ImageMagick can write back.
Whether images of mime_type can be edited (ImageMagick writes them back).
Whether file has an unedited backup (its edit can be changed or reverted).
@spec mode() :: String.t()
What saving an edit does: "keep_original" (default) keeps the unedited
original as a hidden backup; "replace_original" bakes the edit in.
The media setting key for mode/0.
The values mode/0 accepts.
@spec retry(PhoenixKit.Modules.Storage.File.t(), keyword()) :: {:ok, PhoenixKit.Modules.Storage.File.t()} | {:error, term()}
Queues the rendering again for an edit that failed (or seems stuck).
@spec revert(PhoenixKit.Modules.Storage.File.t(), keyword()) :: {:ok, PhoenixKit.Modules.Storage.File.t()} | {:error, term()}
Reverts file to its unedited original and queues the swap back. Nothing
to revert is {:error, :not_edited}; an annotated file whose edit moved
pixels is {:error, {:annotated, count}}, as for edit/3.
@spec save_copy(PhoenixKit.Modules.Storage.File.t(), map(), keyword()) :: {:ok, Oban.Job.t()} | {:error, term()}
Renders params as a new file (a copy in the same folder, owned by the
file's owner, with edited_from_uuid pointing back). The copy is created
by a job; {:ok, job} means it was queued. Annotations do not matter —
the copy has none. An edit that changes nothing is {:error, :no_edit}.
@spec source_instance(PhoenixKit.Modules.Storage.File.t()) :: PhoenixKit.Modules.Storage.FileInstance.t() | nil
The instance an edit is applied to: the backup's original when the file has been edited, else the file's own original.