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.pngis requested asimage_url('logo.png', {w: 200}). - Serving several themes. Glide reads from one directory at a time. Point
sourceat 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$themefrom 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 examplePath::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
ssignature. 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=300tow=3000in 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.
| Parameter | Description |
|---|---|
w | The target width in pixels. |
h | The 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).
|
q | The compression quality from 0 to 100 for JPEG and WebP output. Defaults to 90. |
fm | The output format: jpg, pjpg (progressive JPEG), png, gif, or webp. Defaults to the source format. |
dpr | Device pixel ratio from 1 to 8. Multiplies w and h for high-density screens. |
bg | Background color (hex or named), used by fit: 'fill' and when converting transparent images to JPEG. |
or | Orientation: auto, 0, 90, 180, or 270. |
blur, sharp, filt | Blur (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). Usewrestlers/hogan.webp, notpublic/images/wrestlers/hogan.webp. - An exception about
APP_KEYorIMAGE_SIGN_KEY— set one of them in your.envfile. - Cache errors — make sure
storage/cache/images/exists and is writable by your web server.