Image Rendering
Render and manipulate images on the fly
Basic usage
You can generate image URLs by using the image helper function. The function accepts a Media model instance or a static asset filename as the first argument.
<img src="{{ image($block->image) }}">
// or
<img src="{{ image('logo.png') }}">You can also pass a tuple of width and height as the second argument to define the image size.
<img src="{{ image($media, [300, 300]) }}">Pass a string or an array of filters as the third argument to apply filters to the image.
<img src="{{ image($media, [300, 300], ['crop', 'bw']) }}">URL format
The image helper function generates URLs in the following format:
Media model
- Format: /img/
imageId/options/name.format
/img/dGVpG23oQz/300x0_fit/my-image.jpgStatic assets
You can also render static assets located inside your public/images directory by only passing the filename.
- Format: /img/
assetFile/options/name.format
/img/logo.png/300x0_fit/logo.pngFilters/Manipulations
You can manipulate an image on the fly by defining size and filter options in the URL section after the image identifier:
Format:
- /img/dGVpG23oQz/
widthxheight/my-image.jpg - /img/dGVpG23oQz/
widthxheight_filters/my-image.jpg
Resize to fit (default)
Resizes the image to fit into 300 x 300 px and keep the current aspect-ratio.
/300x300/
// or
/300x300_fit/Resize and fill
Resizes the image to fit into 300 x 300 px by keeping the current aspect-ratio and filling the rest of the image with a given color (Default is white).
/300x300_fill/Fill color
You can pass any RGB value with the fill filter to define a custom color (red in this example):
/300x300_fill[255,0,0]/Crop
Resizes the image to 300 x 300 px and crops from top/bottom or sides.
/300x300_crop/Focus point
You can also set where the image is cropped by adding a crop position (Accepts x,y values (0-1) or keywords like top-left, top, top-right, left, center, right, bottom-left, bottom or bottom-right).
/300x300_crop[top-left]/
// or
/300x300_crop[0.25,0]/Flip
Flip/mirror an image horizontally (default) or vertically (Accepts h or v).
/300x300_flip/
// or
/300x300_flip[v]/
/300x300_flip[h]/Contrast
Change contrast with the contrast filter. You can pass a value to define the amount (between 0 - 2, where 1 would be no change in contrast):
// more contrast (120%)
/300x300_contrast[1.2]/
// less contrast (50%)
/300x300_contrast[0.5]/Brightness
Change brightness with the brightness filter. You can pass a value to define the amount (between 0 - 2, where 1 would be no change in brightness, values > 1 would lighten the image, values < 0 would darken the image):
// lighten (120% brightness)
/300x300_brightness[1.2]/
// darken (75% brightness)
/300x300_brightness[0.75]/Blur
Blur an image. Accepts blur amount between 0 - 100 (default: 25).
// default blur (25)
/300x300_blur/
// specific blur amount
/300x300_blur[50]/Grayscale (Black/White)
Make an image black and white.
/300x300_bw/Invert colors
Inverts the colors of an image.
/300x300_invert/Pixelate
Pixelate an image. (Accepts size as a pixel value)
/300x300_pixelate[25]/Copyright text
Places a copyright text in the botom left corner of an image. Accepts custom font size in pixel value (default: 22).
/300x300_copy/
// Customize font size
/300x300_copy[42]/INFO
The copyright filter is best used as a global filter or tag filter to prevent someone to just remove the copyright part from the URL.
Combining Filters
You can combine any manipulation filters by separating them with _.
// - Crop to 300x300 with position top-left
// - Make black and white
// - Adjust contrast to 1.2 (120%)
// - Blur image with amount 50
// - Invert the colors
/300x300_crop[top-left]_bw_contrast[1.2]_blur[50]_invert/Important
Filters are applied in the order you define them in the URL.
Custom Filters
You can create custom image manipulators/filters by making a new class which implements the Intervention\Image\Filters\FilterInterface interface from Intervention Image.
class MyCustomFilter implements FilterInterface
{
public function __construct(
protected ?int $width = null,
protected ?int $height = null,
protected ?array $options = null,
protected ?Media $image = null,
) {
}
public function applyFilter(Image $image)
{
// do something with the $image object
return $image;
}
}Your class will receive width, height, the options array and the image Media model as constructor arguments.
Registering Your Custom Filters
After creating your own custom image filter, you will need to register it in the config/image.php config file:
'filters' => [
...
'my-custom-filter' => \App\ImageFilters\MyCustomFilter::class,
],You can now use your custom filter with the key defined in the config array.
Global Filters
Global filters will be applied on every image without specifiying it in the URL.
You can define some filters as global by adding their name to the global-filters array in the config/image.php config file.
Example: All images will receive the fit and bw (Grayscale) filters.
'global-filters' => [
'fit', 'bw'
],Tag Filters
Tag filters will be applied on every image with a given tag value (without specifiying it in the URL).
You can define them as tag filters by adding an array of tag names to the tag-filters in the config/image.php config file.
Example:
Images with the tag value of fancy will receive the filters defined in the array.
'tag-filters' => [
'fancy' => [
'bw', 'contrast[1.2]', 'brightness[0.8]'
],
]How a variant is served
Every URL the image() helper builds names one variant: an image, a size, filters and a format. The first time a variant is rendered it is written to public/img/... under exactly that URL path, so every later request is answered by the web server from disk and never reaches PHP. public/img is excluded from the deploy's rsync and carried from release to release by the hard-linked copy, so variants survive deploys. php artisan evolve:clear-images (or the image cache action in the panel) deletes them.
URLs with the front controller in the path, such as /index.php/img/dGVpG23oQz/300x0_fit/my-image.jpg, answer with a 301 to the same URL without /index.php 2.12. Before 2.12 they rendered without being cached and raised warnings.
Queued conversions 2.12
Rendering a variant means decoding the original with GD or Imagick, resizing, encoding and running jpegoptim or pngcrush. The starter's <x-image> component requests 17 variants per picture (eight srcset widths as WebP, the same eight in the image's own format, and the blurred placeholder). Before 2.12 all of them rendered inside visitors' requests the first time a page with a new image was opened, which made that page slow and put a burst of heavy PHP requests on the server.
Since 2.12 the work moves into the queue:
- On upload, and when an image is replaced, renamed, or gets a new focal point, the
GenerateImageConversionsjob renders every variant listed inimage.conversions. - A variant that does not exist yet is answered with the original file (its real content type,
Cache-Control: public, max-age=60), and aGenerateImageVariantjob is queued for exactly that URL. The browser picks up the rendered variant on a later visit. - After a deploy,
evolve:images:warm --missingqueues whatever is still missing.
Lifecycle of a variant
- An editor uploads
team.jpg. The file is stored, thenGenerateImageConversionsis queued for the media item. The job is unique per image until it starts, so repeated saves queue it once. - A visitor opens the page before the worker ran. For
/img/dGVpG23oQz/1024x0/team.webpthe controller finds no file underpublic/img, servesteam.jpgitself and queuesGenerateImageVariantfor that path. A cache lock (10 minutes) makes sure a page full of cold images queues each variant once, not once per hit. - The worker renders the variant and writes
public/img/dGVpG23oQz/1024x0/team.webp. - From then on Apache serves the file directly.
Requests that are cheap or that the renderer cannot process are still handled in the request: size 0x0 (the original, copied) and file types the renderer does not support. A variant that fails to render, in the job or in the request, is reported to the error log (and Sentry) and the original is served. A visitor never gets a 500 for an image.
The list of variants
config/image.php holds the list the component requests and the job pre-renders. The component reads the same keys, so both stay in line as long as the project uses the package's component.
'conversions' => [
// srcset widths, rendered in `format` and in the image's own extension
'srcset-widths' => [3840, 2560, 1920, 1440, 1280, 1024, 756, 512],
// the <source> format, the component's `format` prop default
'format' => 'webp',
// the <img src> width for browsers without srcset support
'fallback-width' => 1280,
// the blurred placeholder behind a lazy image, [width, height]
'placeholder-size' => [3, 3],
// values of the component's `size` prop to pre-render; [0, 0] keeps the aspect ratio
'sizes' => [[0, 0]],
],| Key | Default | Effect |
|---|---|---|
image.conversions.srcset-widths | [3840, 2560, 1920, 1440, 1280, 1024, 756, 512] | Widths of the srcset, rendered twice (in format and in the original's extension). |
image.conversions.format | webp | Format of the <source> element. |
image.conversions.fallback-width | 1280 | Width of the <img src>. |
image.conversions.placeholder-size | [3, 3] | Size of the blurred placeholder. |
image.conversions.sizes | [[0, 0]] | size prop values to pre-render. Each entry multiplies the srcset. |
QUEUE_CONNECTION (Laravel) | sync in older projects | sync keeps rendering in the request. Anything else enables the queued path. |
The media name is part of the URL, and it is translatable. Paths are therefore built for every locale that has a name, and variants that differ only in the name are rendered once and copied.
Sizes not on the list still work. They are rendered by the per-variant job on their first request. To pre-render a size the theme uses a lot, add it:
'sizes' => [[0, 0], [800, 400]], // plus the cropped card imagesProjects with their own image component
A project that published components/image.blade.php and changed widths, format or size props keeps working, but the job pre-renders what image.conversions lists, not what the copy requests. Mirror the changes in config/image.php. Whatever is not listed renders on its first request through the per-variant job.
Queue driver
QUEUE_CONNECTION | Upload | Cold variant | evolve:images:warm |
|---|---|---|---|
sync | nothing is queued | rendered in the request, as before 2.12 | renders inline, can take long |
database, redis, ... | variants queued | original served, variant queued | only enqueues work |
With the sync driver nothing changes compared to earlier releases, so switching is a per-site decision. On Onlime there is no daemon; the worker runs from cron, see Deployment:
* * * * * cd ~/webhome/public_html/www/current && php84 artisan queue:work --stop-when-empty --max-time=55 >> /dev/null 2>&1The jobs table must exist for the database driver, and the cache store must support locks (file, database and redis do).
Warming
php artisan evolve:images:warm # every image, all variants re-rendered (forced)
php artisan evolve:images:warm --missing # only images with missing variants, only those variantsThe command prints a summary (Queued 12 images with 57 missing variants.). Add the --missing form to the deploy's after hooks; since public/img survives deploys it is normally a no-op. Run it once without --missing after changing image.conversions, or after evolve:clear-images.
'hooks' => [
'after' => [
'php84 artisan evolve:images:warm --missing',
],
],Monitoring
No separate check was added. The queue health check is enabled automatically as soon as the queue driver is not sync (or explicitly with HEALTH_CHECK_QUEUE, see Settings Store). The scheduler dispatches its heartbeat job every minute, and that job waits behind the image jobs. A stopped worker cron or a growing backlog therefore fails the Queue check on /evolve/health and in Oh Dear.
Pitfalls
- Switching the queue without a worker. Images keep working (originals are served), but pages get heavier and nothing is ever rendered. Other queued jobs stop too: Shopify imports, server side tracking events, image metadata. Add the cron line in the same step.
- Originals are large. Until the worker ran, visitors download the original upload. On a site with a backlog this costs bandwidth;
evolve:images:warm --missingright after the deploy keeps the window short. - Failing images retry every 10 minutes at most. An image that cannot be rendered (corrupt file, too large for the memory limit) keeps being served as the original. The error is in the log and in Sentry.
ImageRepositoryis gone.Racerfish\Evolve\Repositories\ImageRepositoryandMedia::updateEdited()were removed in 2.12. UseRacerfish\Evolve\Services\ImageRenderer.
Verifying on a server
# 1. Is the queue in use, and is the worker running?
grep QUEUE_CONNECTION current/.env
crontab -l | grep queue:work
# 2. Upload an image in the panel, wait a minute, then look for its variants
ls current/public/img/<hash>/
# 3. Nothing left over after a deploy
php84 artisan evolve:images:warm --missing # "Queued 0 images with 0 missing variants."A cold variant answers with Cache-Control: public, max-age=60 and the original's content type; the same URL a minute later is a static file served by Apache.