berrypod/lib/simpleshop_theme/media.ex
jamey bb358f890b consolidate image serving and clean up pipeline
Move all image URL logic into ProductImage.url/2 and thumbnail_url/1,
remove dead on-demand generation code from Optimizer, strip controller
routes down to SVG recolor only, fix mockup startup check to verify all
variant formats, and isolate test image cache directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 17:47:41 +00:00

174 lines
3.8 KiB
Elixir

defmodule SimpleshopTheme.Media do
@moduledoc """
The Media context for managing images and file uploads.
"""
import Ecto.Query, warn: false
alias SimpleshopTheme.Repo
alias SimpleshopTheme.Media.Image, as: ImageSchema
alias SimpleshopTheme.Images.Optimizer
alias SimpleshopTheme.Images.OptimizeWorker
@doc """
Uploads an image and stores it in the database.
For non-SVG images:
- Converts to lossless WebP for storage (26-41% smaller than PNG)
- Extracts source dimensions for responsive variant generation
- Enqueues background job to generate optimized variants (AVIF, WebP, JPEG at multiple sizes)
## Examples
iex> upload_image(%{image_type: "logo", filename: "logo.png", ...})
{:ok, %Image{}}
"""
def upload_image(attrs) do
attrs = prepare_image_attrs(attrs)
case Repo.insert(ImageSchema.changeset(%ImageSchema{}, attrs)) do
{:ok, image} ->
# Enqueue background job for non-SVG images
unless image.is_svg do
OptimizeWorker.enqueue(image.id)
end
{:ok, image}
error ->
error
end
end
# Prepares image attributes, converting to lossless WebP and extracting dimensions
defp prepare_image_attrs(%{data: data, content_type: content_type} = attrs)
when is_binary(data) do
if is_svg?(content_type, attrs[:filename]) do
attrs
else
case Optimizer.to_optimized_webp(data) do
{:ok, webp_data, width, height} ->
attrs
|> Map.put(:data, webp_data)
|> Map.put(:content_type, "image/webp")
|> Map.put(:file_size, byte_size(webp_data))
|> Map.put(:source_width, width)
|> Map.put(:source_height, height)
|> Map.put(:variants_status, "pending")
{:error, _reason} ->
# If conversion fails, store original image
attrs
end
end
end
defp prepare_image_attrs(attrs), do: attrs
defp is_svg?(content_type, filename) do
content_type == "image/svg+xml" or
String.ends_with?(filename || "", ".svg")
end
@doc """
Uploads an image from a LiveView upload entry.
This handles consuming the upload and extracting metadata from the entry.
## Examples
iex> upload_from_entry(socket, :logo_upload, fn path, entry -> ... end)
{:ok, %Image{}}
"""
def upload_from_entry(path, entry, image_type) do
file_binary = File.read!(path)
upload_image(%{
image_type: image_type,
filename: entry.client_name,
content_type: entry.client_type,
file_size: entry.client_size,
data: file_binary
})
end
@doc """
Gets a single image by ID.
## Examples
iex> get_image(id)
%Image{}
iex> get_image("nonexistent")
nil
"""
def get_image(id) do
Repo.get(ImageSchema, id)
end
@doc """
Gets the current logo image.
## Examples
iex> get_logo()
%Image{}
"""
def get_logo do
Repo.one(
from i in ImageSchema,
where: i.image_type == "logo",
order_by: [desc: i.inserted_at],
limit: 1
)
end
@doc """
Gets the current header image.
## Examples
iex> get_header()
%Image{}
"""
def get_header do
Repo.one(
from i in ImageSchema,
where: i.image_type == "header",
order_by: [desc: i.inserted_at],
limit: 1
)
end
@doc """
Deletes an image.
## Examples
iex> delete_image(image)
{:ok, %Image{}}
"""
def delete_image(%ImageSchema{} = image) do
Repo.delete(image)
end
@doc """
Lists all images of a specific type.
## Examples
iex> list_images_by_type("logo")
[%Image{}, ...]
"""
def list_images_by_type(type) do
Repo.all(from i in ImageSchema, where: i.image_type == ^type, order_by: [desc: i.inserted_at])
end
end