# PhoenixImage
On-the-fly image resizing for Phoenix with disk caching and a responsive `<.image>` component.
Images are resized on first request using [libvips](https://www.libvips.org/) (via the [`image`](https://hex.pm/packages/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`:
```elixir
def deps do
[
{:phoenix_image, "~> 0.1.0"}
]
end
```
## Setup
### 1. Configure
```elixir
# config/config.exs
config :phoenix_image, otp_app: :my_app
```
### 2. Add to supervision tree
```elixir
# 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
```elixir
# lib/my_app_web/router.ex
scope "/" do
forward "/images", PhoenixImage.Plug
end
```
### 4. Import the component
```elixir
# 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:
```heex
<%# 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:
- Generates URLs like `/images/fill-400x300/images/photo.jpg`
- Sets `width` and `height` attributes on the `<img>` tag to prevent layout shift
- Generates a `srcset` with 1x and 2x variants for retina displays (when the source image is large enough)
- Passes through all other HTML attributes (`class`, `alt`, `loading`, etc.)
### Resize operations
| Operation | URL pattern | Description |
|-----------|------------|-------------|
| `fill` | `/images/fill-400x300/path` | Crop to exact dimensions using attention detection |
| `width` | `/images/width-800/path` | Resize by width, proportional height |
| `height` | `/images/height-600/path` | Resize by height, proportional width |
| `max` | `/images/max-800x600/path` | Fit within bounds, no crop |
| `original` | `/images/original/path` | Serve original file |
### Allowed dimensions
For security, only whitelisted dimensions are accepted. The defaults are:
```
64, 96, 100, 150, 192, 200, 288, 300, 400, 600, 800, 900, 1200, 1536
```
Requests with other dimensions return a 400 error.
## Configuration
All options are set via application config under `:phoenix_image`:
```elixir
config :phoenix_image,
# Required: which OTP app's priv/static to read source images from
otp_app: :my_app,
# URL prefix for image routes (must match your `forward` path)
# Default: "/images"
prefix: "/images",
# Whitelisted dimensions (width/height values)
# Default: [64, 96, 100, 150, 192, 200, 288, 300, 400, 600, 800, 900, 1200, 1536]
allowed_dims: [64, 96, 100, 150, 192, 200, 288, 300, 400, 600, 800, 900, 1200, 1536],
# 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/images"
cache_dir: "priv/static/cache/images"
```
## 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:
```bash
mix phoenix_image.reset_cache
```
## How it works
1. A request hits `/images/fill-400x300/images/photo.jpg`
2. `PhoenixImage.Plug` parses the operation and validates dimensions
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
- Elixir ~> 1.15
- libvips (system dependency for the `image` library)
On macOS: `brew install libvips`
On Ubuntu/Debian: `apt-get install libvips-dev`
## License
MIT