Image Processing

Rhapsody uses League Glide, an on-demand image manipulation library built on Intervention Image, to resize, crop, convert, and compress images simply by changing the URL. Every variant is generated once, cached on disk, and served from the cache afterwards, saving storage space and improving performance.


Installation

First, add the library to your project using Composer:

composer require league/glide

Glide needs either the gd or the imagick PHP extension. GD is enabled by default in most PHP installations, including WAMP.

The ImageController

An ImageController is included in the application skeleton to handle all image requests. For each request it validates the URL signature, asks Glide for the matching image variant (generating and caching it the first time), and returns it with the correct Content-Type and caching headers.

By default, source images are read from the public/images/ directory (see Changing the Image Directory to use another one), and processed images are stored in storage/cache/images/. Ensure the cache directory is writable by your web server. Image paths are always relative to the source directory, so the file public/images/wrestlers/hogan.webp is requested as wrestlers/hogan.webp.

Routing

A wildcard route captures all image requests. It is already registered in the skeleton's routes/web.php file and should not be removed:


// in routes/web.php
use App\Controllers\ImageController;

Router::get('/img/{path}', [ImageController::class, 'show']);

This route directs any URL starting with /img/ to the ImageController.

Changing the Image Directory

The source directory is set in the show() method of app/Controllers/ImageController.php, on the source line of the Glide server configuration. To serve images from somewhere else, such as a theme folder, change that path:


use Rhapsody\Core\Helpers\Path;

$server = ServerFactory::create([
    // Default: Path::public('images')
    'source'         => Path::public('themes/default/images'),
    'cache'          => $cachePath,
    'max_image_size' => self::MAX_IMAGE_PIXELS,
]);

Path is a small helper in Rhapsody\Core\Helpers that returns absolute filesystem paths. Path::public('themes/default/images') resolves to public/themes/default/images inside your project, using the correct directory separator for your operating system. Use it (or the related Path::root(), Path::storage(), and Path::resources() methods) instead of hard-coding absolute paths, so the controller works on any server.

A few things to keep in mind when you change the directory:

  • Image paths are relative to the new directory. With the example above, the file public/themes/default/images/logo.png is requested as image_url('logo.png', {w: 200}).
  • Serving several themes. Glide reads from one directory at a time. Point source at the parent folder, Path::public('themes'), and include the theme in the image path (image_url('default/images/logo.png', {w: 200})) to reach every theme from a single setting.
  • Choosing the theme at runtime. If you build the directory from a variable, such as Path::public('themes/' . $theme . '/images'), take $theme from your application's configuration, never from the request. A value supplied by a visitor could point the processor at an arbitrary directory.
  • Originals do not have to be public. Every image is served through the controller, so the source directory can live outside public/, for example Path::storage('uploads/images').
  • Clear the image cache after changing the directory. Cached variants are stored in folders named after the image path, so a file with the same name in the old and new directories can be served from the old cache. Delete the contents of storage/cache/images/ once.

Usage in Twig

Use the image_url() Twig function to build the image source. It takes the image path (relative to public/images/) and an optional object of parameters, and returns a complete, signed URL.

Example: Resizing an Image

Let's say you have an image located at public/images/wrestlers/hogan.webp.

Original HTML:

<img src="{{ base_url }}/public/images/wrestlers/hogan.webp">

Optimized HTML with resizing:

<!-- A 150x150 square, cropped to fill -->
<img src="{{ image_url('wrestlers/hogan.webp', {w: 150, h: 150, fit: 'crop'}) }}" alt="">

<!-- A 300px wide version at 80% quality -->
<img src="{{ image_url('wrestlers/hogan.webp', {w: 300, q: 80}) }}" alt="">

<!-- Convert to WebP -->
<img src="{{ image_url('wrestlers/hogan.jpg', {w: 300, fm: 'webp'}) }}" alt="">

Example: Responsive Images

Generate each size you need in the template and let the browser choose:

<img src="{{ image_url('wrestlers/hogan.webp', {w: 400}) }}"
     srcset="{{ image_url('wrestlers/hogan.webp', {w: 400}) }} 400w,
             {{ image_url('wrestlers/hogan.webp', {w: 800}) }} 800w"
     sizes="(max-width: 600px) 400px, 800px"
     alt="">

Signed URLs

Every distinct set of query parameters creates a separate cached file, so unrestricted resizing would let anyone fill your disk and CPU by requesting thousands of sizes. To prevent this, image variants must be signed by your application:

  • A request with query parameters must carry a valid s signature. Missing, invalid, or edited signatures return 403.
  • A request without query parameters (the plain original image, e.g. /img/wrestlers/hogan.webp) is public and needs no signature.
  • The signature covers the image path and every parameter, so changing w=300 to w=3000 in a signed URL invalidates it.

Always build URLs with image_url() in Twig. Outside of templates, use the same signer in PHP:


use Rhapsody\Core\Helpers\ImageSigner;

$url = ImageSigner::url('wrestlers/hogan.webp', ['w' => 300, 'q' => 80]);

URLs assembled in JavaScript cannot be signed, because the signing key must stay on the server. Generate each size you need in Twig (for example in a srcset) instead of building query strings in the browser.

Signing Key

Signatures are made with the IMAGE_SIGN_KEY value from your .env file when it is set. Otherwise a separate key is derived from your APP_KEY (the raw APP_KEY is never used directly). If neither is set, generating a URL throws an exception.

# .env (optional)
IMAGE_SIGN_KEY=a-long-random-string

Changing the key invalidates every previously signed URL, including URLs saved in cached pages. Set IMAGE_SIGN_KEY if you want to rotate the image key independently of APP_KEY.

Available Parameters

Pass these as the second argument of image_url(). Glide supports many more, documented in the Glide documentation.

ParameterDescription
wThe target width in pixels.
hThe target height in pixels. If only one dimension is provided, the image is scaled proportionally.
fit How the image is fitted to w and h: contain (default) fits inside the box keeping the aspect ratio; max does the same but never enlarges a smaller image; fill fits inside the box and fills the leftover space with the bg color; stretch ignores the aspect ratio; crop fills the box exactly, cropping the overflow (also crop-top-left, crop-bottom-right and the other position variants).
qThe compression quality from 0 to 100 for JPEG and WebP output. Defaults to 90.
fmThe output format: jpg, pjpg (progressive JPEG), png, gif, or webp. Defaults to the source format.
dprDevice pixel ratio from 1 to 8. Multiplies w and h for high-density screens.
bgBackground color (hex or named), used by fit: 'fill' and when converting transparent images to JPEG.
orOrientation: auto, 0, 90, 180, or 270.
blur, sharp, filtBlur (0–100), sharpen (0–100), and filters (greyscale, sepia).

Caching and Limits

The first request for a variant generates it and stores it under storage/cache/images/, in a folder named after the source image, with one file per unique parameter combination. Later requests are served from that file. Responses are sent with Cache-Control: public, max-age=86400, so browsers and CDNs keep them for a day.

If you replace a source image, delete its folder inside storage/cache/images/ (or the whole directory) so variants are regenerated from the new file.

The largest image Glide will generate is 4,000,000 pixels (width × height, equal to 2000×2000). Larger sources and requests are scaled down to fit, even when no size is requested. To change the limit, edit the MAX_IMAGE_PIXELS constant in ImageController.

Troubleshooting

  • 403 Invalid image signature — the URL was built by hand, edited, or built in JavaScript. Generate it with image_url().
  • 404 Image not found — the path is relative to the image source directory (public/images/ by default). Use wrestlers/hogan.webp, not public/images/wrestlers/hogan.webp.
  • An exception about APP_KEY or IMAGE_SIGN_KEY — set one of them in your .env file.
  • Cache errors — make sure storage/cache/images/ exists and is writable by your web server.