Packages

phoenix_kit

1.7.206
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules storage services annotation_thumbnail.ex
Raw

lib/modules/storage/services/annotation_thumbnail.ex

defmodule PhoenixKit.Modules.Storage.AnnotationThumbnail do
@moduledoc """
Bakes Etcher annotation shapes into a single PNG thumbnail variant.
A file with annotations gets one extra variant — `"thumbnail_annotated"` — a
small square PNG with the shapes drawn on top (via ImageMagick `convert
-draw`). The media browser grid uses it instead of the plain thumbnail, so the
markup is visible without rendering shapes live for every viewer.
It's a single deterministic variant slot: each regeneration overwrites it (no
history piles up). When the last annotation is removed, the variant is dropped
and the grid falls back to the plain thumbnail.
Shapes are stored in image-pixel coordinates, so they're drawn at original
scale on the original image (the geometry → draw mapping lives in
`Etcher.Raster`, which owns the shape format), then the whole thing is resized
+ center-cropped to the thumbnail square — the crop clips edge shapes exactly
like the grid.
Regeneration is meant to run in the background (see `AnnotationThumbnailJob`),
debounced so a flurry of edits collapses into one render.
"""
# `Etcher.Raster` (the server-side shape renderer) ships in newer Etcher; tolerate
# older versions at compile time and degrade gracefully at runtime.
@compile {:no_warn_undefined, Etcher.Raster}
require Logger
alias PhoenixKit.Annotations
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.VariantGenerator
alias PhoenixKit.Settings
@variant_name "thumbnail_annotated"
# Square size of the baked thumbnail (px).
@size 400
# Media setting gating the whole feature (baking + display). Off by default.
@setting_key "storage_annotated_thumbnails_enabled"
@doc "The variant name this module produces."
def variant_name, do: @variant_name
@doc """
Whether baked annotated thumbnails are enabled (media setting; default `false`).
Gates generation and display — see the media settings page, "Media
Configuration" section.
"""
def enabled?, do: Settings.get_boolean_setting(@setting_key, false)
@doc """
Regenerate (or remove) the baked annotated thumbnail for `file_uuid`.
Returns `{:ok, instance}` when (re)generated, `{:ok, :removed}` when there's
nothing to draw, or `{:error, reason}`.
"""
def refresh(file_uuid) when is_binary(file_uuid) do
case Storage.get_file(file_uuid) do
%Storage.File{file_type: "image"} = file ->
if enabled?() do
annotations = Annotations.list_for_file(file_uuid)
case draw_args(annotations, base_dimension(file)) do
[] ->
remove_variant(file)
{:ok, :removed}
draw_args ->
generate(file, draw_args)
end
else
# Feature toggled off — drop any stale baked variant.
remove_variant(file)
{:ok, :removed}
end
_ ->
# Non-image (or missing) file — nothing to annotate.
{:ok, :removed}
end
rescue
e ->
Logger.warning("AnnotationThumbnail.refresh failed for #{file_uuid}: #{inspect(e)}")
{:error, :exception}
end
defp generate(file, draw_args) do
with {:ok, original_path, file} <- Storage.retrieve_file(file.uuid),
output <- temp_png(),
:ok <- run_convert(original_path, output, draw_args) do
# Drop any existing instance first so a fresh one is created with the new
# checksum (create_variant_instance returns the existing row otherwise).
remove_variant(file)
result =
VariantGenerator.store_prepared_variant(file, @variant_name, output, "png", "image/png")
File.rm(original_path)
result
else
error ->
Logger.warning("AnnotationThumbnail.generate failed for #{file.uuid}: #{inspect(error)}")
{:error, :generate_failed}
end
end
defp run_convert(input_path, output_path, draw_args) do
# `draw_args` already carries `-fill none` + the per-shape `-stroke/-draw`
# (from Etcher.Raster); we just splice it in before the resize/crop so shapes
# are drawn in the source image's pixel space and scale with it.
args =
[input_path] ++
draw_args ++
["-resize", "#{@size}x#{@size}^", "-gravity", "center", "-extent", "#{@size}x#{@size}"] ++
["png:#{output_path}"]
case System.cmd("convert", args, stderr_to_stdout: true) do
{_out, 0} -> :ok
{out, code} -> {:error, "convert exited #{code}: #{out}"}
end
end
# Etcher owns the geometry → draw mapping. Convert our `Annotation` structs to
# the generic wire shape and hand them to `Etcher.Raster`, supplying the render
# policy (stroke width scaled to the source image so it survives the
# down-scale). Degrades to no overlay when the installed Etcher predates the
# server renderer.
defp draw_args(annotations, base_dim) do
if Code.ensure_loaded?(Etcher.Raster) do
annotations
|> Enum.map(fn a -> %{"kind" => a.kind, "geometry" => a.geometry, "style" => a.style} end)
|> Etcher.Raster.to_draw_args(stroke_width: max(round(base_dim / 200), 3))
else
[]
end
end
# ── helpers ──────────────────────────────────────────────────────────────
defp remove_variant(file) do
case Storage.get_file_instance_by_name(file.uuid, @variant_name) do
%Storage.FileInstance{} = instance -> Storage.delete_file_instance(instance)
_ -> :ok
end
end
defp base_dimension(file) do
max(file.width || 0, file.height || 0)
|> case do
0 -> 1000
dim -> dim
end
end
defp temp_png do
name = :crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
Path.join(System.tmp_dir!(), "phoenix_kit_annotated_#{name}.png")
end
end