Skip to main content

README.md

# PhoenixImage

An on-demand image optimization library for Phoenix applications.
`phx_image` provides a Plug that fetches source images, resizes/transcodes
them, and serves them with caching headers. It also provides a Next.js-like
`<.image />` function component for Phoenix templates.

## Features

- **On-Demand Optimization:** Fetch, resize, and convert images on the fly via HTTP.
- **Aspect-Preserving Resizing:** Keep original aspect ratio while fitting requested dimensions.
- **Multiple Formats:** Supports outputting as `webp`, `avif`, `jpg`, and `png`.
- **High Performance:** Powered by `libvips` via the `image` library for extremely fast processing.
- **Built-in Caching Headers:** Automatically sets `Cache-Control` headers for long-term browser caching.

## Installation

The package can be installed by adding `phx_image` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:phx_image, "~> 0.1.0"}
  ]
end
```

`PhoenixImage.Component` depends on `:phoenix_live_view`. Add it explicitly if
you use the component:

```elixir
def deps do
  [
    {:phx_image, "~> 0.1.0"},
    {:phoenix_live_view, "~> 1.0"}
  ]
end
```

## Usage

### Demo App

This repo includes a Phoenix demo app in `demo/` for manual testing.

```bash
cd demo
mix deps.get
mix phx.server
```

Open `http://localhost:4000` and use the form to test `/images/optimize`.

### As a Plug

You can mount the `PhoenixImage.Plug` in your Phoenix router or as part of a standalone Plug pipeline:

```elixir
# In your router.ex
scope "/images" do
  pipe_through :browser
  get "/optimize", PhoenixImage.Plug, []
end
```

### As a Component (`<.image />`)

Add to your Phoenix components module:

```elixir
defmodule MyAppWeb.CoreComponents do
  use Phoenix.Component
  import PhoenixImage.Component, only: [image: 1]
end
```

Use in HEEx:

```heex
<.image
  src="https://example.com/photo.jpg"
  alt="Scenic view"
  width={1200}
  height={800}
  sizes="100vw"
/>
```

Required sizing rule:

- `fill={true}` OR both `width` and `height`.

Configure component defaults:

```elixir
config :phx_image, :image_component,
  optimize_path: "/images/optimize",
  device_sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
  image_sizes: [16, 32, 48, 64, 96, 128, 256, 384],
  allowed_hosts: ["example.com", "cdn.example.com"]
```

### Plug Options

`PhoenixImage.Plug` supports:

- `:cache_control` (default: `public, max-age=31536000, immutable`)
- `:allowed_hosts` (default: `[]`): extra hosts allowed for absolute `src`
  URLs in addition to the request host.

### Query Parameters

- `src` (required): source image location.
  - Absolute `http://` or `https://` URL, or
  - Root-relative path like `/images/logo.png` (resolved against request host).
- `w` (optional): positive integer width, max `8192`.
- `h` (optional): positive integer height, max `8192`.
- `q` (optional): quality from `1` to `100`.
- `f` (optional): output format `webp|avif|jpg|png`. Default: `webp`.
- `upscale` (optional): `true|false`. Default: `false`.

### Resize Behavior

- `w` only: scales to width, preserves aspect ratio.
- `h` only: scales to height, preserves aspect ratio.
- `w` + `h`: fits inside the box while preserving aspect ratio
  (does not crop to exact dimensions).
- By default, requests that would enlarge the image are clamped to source size.
- With `upscale=true`, enlarge is allowed up to `2x` source size.

### Example

Requesting a 400px wide WebP version of an external image:

```text
GET /images/optimize?src=https://example.com/large-photo.jpg&w=400&f=webp
```

The response will include:
- The processed image binary.
- `Content-Type: image/webp`.
- `Cache-Control: public, max-age=31536000, immutable`.
- `x-phoenix-image-upscale: skipped` when a requested enlargement was clamped.

### Relative `src` Example

```text
GET /images/optimize?src=/images/logo.png&w=320
```

### Error Semantics

- `400 Bad Request`: missing/invalid parameters.
- `403 Forbidden`: `src` host is not same-host and not in `:allowed_hosts`.
- `404 Not Found`: upstream source returned 404.
- `500 Internal Server Error`: upstream/image-processing failures.

## Requirements

- **libvips:** This library requires `libvips` to be installed on your system.
  - macOS: `brew install vips`
  - Linux: `apt install libvips-dev` (or equivalent for your distribution)

## Compatibility

- Elixir `~> 1.15`
- Plug `~> 1.19`
- Optional component dependency: `phoenix_live_view ~> 1.0`
- System dependency: `libvips` with desired output codecs (`webp`, `avif`, etc.)

## Security Notes

- Validate `src` inputs and restrict with `:allowed_hosts` for remote fetches.
- Avoid exposing unrestricted optimizer endpoints on untrusted networks.

## License

Apache-2.0. See [LICENSE](LICENSE).

Documentation can be generated locally with:

```bash
mix docs
```

Docs are published automatically to GitLab Pages from the default branch.

## Releasing

Use the custom task to run preflight checks, tag, push, and publish:

```bash
mix release.publish --yes
```

Useful flags:

- `--no-publish`: run checks and create/push tag without publishing to Hex.
- `--no-tag`: publish without creating a tag.
- `--no-push`: avoid pushing `HEAD` and tag.
- `--skip-precommit`: skip `mix precommit`.
- `--remote origin`: choose push remote.