README.md

# 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