PhoenixImage

On-the-fly image resizing for Phoenix with disk caching and a responsive <.image> component.

Images are resized on first request using libvips (via the image library), cached to disk, and served with immutable cache headers. The <.image> component renders <img> tags with correct width, height, and srcset attributes to prevent layout shift and support retina displays.

Installation

Add phoenix_image to your list of dependencies in mix.exs:

def deps do
  [
    {:phoenix_image, "~> 0.2.0"}
  ]
end

Setup

1. Configure

# config/config.exs
config :phoenix_image, otp_app: :my_app

2. Add to supervision tree

# lib/my_app/application.ex
def start(_type, _args) do
  children = [
    # ...
    PhoenixImage,
    MyAppWeb.Endpoint
  ]

  Supervisor.start_link(children, strategy: :one_for_one)
end

3. Add route

# lib/my_app_web/router.ex
scope "/" do
  forward "/resize", PhoenixImage.Plug
end

4. Import the component

# lib/my_app_web.ex
defp html_helpers do
  quote do
    # ...
    import PhoenixImage.Component
  end
end

Usage

The <.image> component

Place your source images in priv/static/ as usual. Then use the component in your templates:

<%# Crop to exact dimensions (attention-based crop) %>
<.image src="/images/photo.jpg" fill={{400, 300}} alt="A photo" class="rounded" />

<%# Resize by width, height calculated proportionally %>
<.image src="/images/photo.jpg" width={800} alt="A photo" />

<%# Resize by height, width calculated proportionally %>
<.image src="/images/photo.jpg" height={600} alt="A photo" />

<%# Fit within max dimensions, no crop %>
<.image src="/images/photo.jpg" max={{800, 600}} alt="A photo" />

<%# Serve original size (still sets width/height to prevent layout shift) %>
<.image src="/images/photo.jpg" original alt="A photo" />

The component automatically:

Resize operations

Operation URL pattern Description
fill/resize/fill-400x300/path Crop to exact dimensions using attention detection
width/resize/width-800/path Resize by width, proportional height
height/resize/height-600/path Resize by height, proportional width
max/resize/max-800x600/path Fit within bounds, no crop
original/resize/original/path Serve original file

URL signing

All image URLs are automatically signed with an HMAC-SHA256 signature. Only URLs generated by the <.image> component are accepted — manually crafted or tampered URLs return a 403 error. The signature is deterministic, so URLs are stable and fully cacheable by CDNs.

Configuration

All options are set via application config under :phoenix_image:

config :phoenix_image,
  # Required: which OTP app&#39;s priv/static to read source images from
  otp_app: :my_app,

  # URL prefix for resize routes (must match your `forward` path)
  # Default: "/resize"
  prefix: "/resize",

  # Signing secret for URL HMAC signatures
  # Falls back to the Phoenix endpoint&#39;s secret_key_base if not set
  # Generate with: mix phx.gen.secret 32
  secret: "your-random-secret-here",

  # JPEG output quality (1-100)
  # Default: 90
  quality: 90,

  # Directory within priv/ where source images live
  # Default: "priv/static"
  source_dir: "priv/static",

  # Directory within priv/ for cached resized images
  # Default: "priv/static/cache/resize"
  cache_dir: "priv/static/cache/resize"

Cache management

Resized images are cached to disk on first request. Subsequent requests serve the cached file directly with Cache-Control: public, max-age=31536000, immutable.

To clear the cache and force regeneration:

mix phoenix_image.reset_cache

How it works

  1. A request hits /resize/fill-400x300/images/photo.jpg?s=abc123...
  2. PhoenixImage.Plug verifies the URL signature and parses the operation
  3. PhoenixImage.Processor checks for a cached version on disk
  4. If not cached, it resizes the source image using Image.thumbnail/3 (libvips)
  5. The result is written to the cache directory and served with immutable headers
  6. Meanwhile, PhoenixImage.Component reads the original image dimensions (cached in ETS via PhoenixImage.Dimensions) to set correct width/height attributes at render time

Requirements

On macOS: brew install libvips On Ubuntu/Debian: apt-get install libvips-dev

License

MIT