Download the PHP package fruitcake/laravel-image-variants without Composer

On this page you can find all versions of the php package fruitcake/laravel-image-variants. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package laravel-image-variants

Laravel Image Variants

Unit Tests Packagist License Latest Stable Version Total Downloads Fruitcake

Resize, crop and convert images on the fly — without pre-rendering

image

Ask for a variant in a template and you get a URL back. Nothing is generated at that point, and nothing needs to have been generated ahead of time:

The first request for that URL reaches PHP, which generates the image and writes it to exactly the path the browser asked for — below the storage:link symlink. Every request after that is answered by the web server straight off disk. PHP is never involved again, so there is no route to hit, no cache to consult, and no per-request overhead once an image exists.

That is the whole idea: on-demand generation with the serving cost of a static file.

Requirements

Image work is done through Laravel's own Illuminate\Image layer, so the driver is whatever config/images.php says (gd by default).

WebP and AVIF need a PHP build compiled for them. Both are permitted by the default output_formats, but asking for a format this build cannot encode raises an ImageException, which the controller answers with a 404 — a puzzling one to debug. Check with php -r 'var_dump(function_exists("imagewebp"), function_exists("imageavif"));' before relying on either.

Install

The package registers itself. It needs the public storage symlink, so if you have not already:

Optionally publish the config:

Your web server must serve existing files before handing off to PHP. The stock Laravel nginx config (try_files $uri $uri/ /index.php?$query_string) and the shipped public/.htaccess both do this already. If yours routes everything to index.php unconditionally, every request will regenerate and the point of the package is lost.

Cache headers

The one response that comes from PHP carries Cache-Control: public, max-age=31536000, immutable. Every response after that comes from the web server, which sends whatever it is configured to send — usually just Last-Modified, so browsers revalidate on each visit.

Since variant URLs change whenever their content does, it is worth saying so once in the server config:

Where images come from

Sources are read from a filesystem disk, and every $src you pass is relative to it. The default is Laravel's own public disk — storage/app/public, where uploads normally land:

Any disk works, remote ones included — sources are read through Flysystem, so an s3 disk needs no special handling. Only the first request for a variant reads the source; everything after that is served off local disk, so a remote source costs one read per variant, ever.

To serve images committed under public/ instead, define a disk for them in config/filesystems.php and name it here:

Confining sources to a subdirectory

prefix narrows a disk to one directory without needing a disk of its own. $src is then relative to the prefix, and nothing outside it is addressable:

Sources that do not resolve

A $src that escapes its root, does not exist, or is not one of source_formats throws VariantException and returns 404 — it is a bad URL.

A source that is not configured — no disk set, or a disk missing from filesystems.disks — throws VariantConfigurationException, which is deliberately not caught by the controller. A broken deployment should surface as a 500 in your error tracker, not as images that quietly never appear.

Usage

The facade

The signature is the same throughout:

Variants::make(...) takes the same arguments and returns the Variant object instead, which also exposes ->url(), ->path(), ->hash(), ->format(), ->dimensions() and ->query(), plus the readonly $preset, $operations, $src and $name it was built from. $operations is the full merged set — the smaller subset the URL spells out is $explicit.

Responsive images

srcset() adds a scale operation per width. It can be combined with a preset, as long as that preset is not itself a resize — see One resize at a time below.

In Blade you rarely need to call it yourself; the <x-variant> component below assembles the whole tag.

Dimensions, without generating anything

A browser that knows an image's aspect ratio up front reserves the space and does not shift the page when the image lands. dimensions() works out what a variant will be:

Most presets cost nothing to measure. cover, contain and crop state their output dimensions outright, so a thumb is answered from the operations alone and the source is never opened. Only scale and a one-sided resize need the source's aspect ratio; a local source is then measured from its file header, and a remote one is fetched once and remembered for dimensions.ttl seconds.

It returns null rather than guessing when the answer genuinely depends on decoding the image — an orient, whose result depends on EXIF this has not read, or a rotate off the square, which lands on a bounding box the encoder rounds its own way. In Blade, the component below handles that for you.

Blade

Everything a good image tag needs comes from the same description, so there is a component that writes the whole thing:

Give it widths and it builds the srcset too, using the largest as the src that anything ignoring srcset falls back to:

Prop Meaning
src Source path, relative to the configured disk.
preset A preset name, or :preset="['cover' => [60, 40]]" for ad-hoc operations.
format Output extension; defaults to the source's.
name Filename to serve as; defaults to the source's.
:operations Merged over the preset.
:widths Builds a srcset over these widths.
sizes The sizes attribute. Only emitted alongside a srcset.

width and height are worked out and added, and left off entirely rather than guessed when the variant cannot be measured without generating it. alt is always emitted so the tag is valid — pass a real one for any image that carries meaning, since an empty alt tells a screen reader the image is decorative. Anything else you put on the tag (class, loading, decoding, fetchpriority, …) passes straight through.

Rename it, or turn it off, if variant is a name your application already uses:

For a URL somewhere that is not an <img> — an og:image, a CSS background, a <source> — use the facade directly, as above.

Twig

If your app renders Twig, register the bundled extension with your Twig environment:

variant_size rather than dimensions, because Twig functions share one global namespace with whatever else the application has registered.

Operations

Operations are named after the methods on Illuminate\Image\Image and can be written either as PHP arrays (in presets and calls) or as query parameters (in the URL). Both forms normalise to the same thing.

Operation Arguments Example
orient — orient=1
rotate angle 0–359, optional background rotate=90,ffffff
flip v or h flip=v
crop width, height, optional x, y crop=300,200,50,25
cover width, height (both required) cover=600,500
contain width, height, optional background contain=600,500,fff
resize width and/or height resize=800,600
scale width and/or height scale=800
grayscale — grayscale=1
blur 0–100, default 5 blur=10
sharpen 0–100, default 10 sharpen=15
quality 1–100, default from config quality=80

A few rules the grammar enforces:

Presets

Presets are named operation sets. The name becomes a path segment, so it is worth keeping them readable.

Anything in $operations is merged over the preset, so a preset can be reused with one value changed. Editing a preset changes the hash of every URL using it, so variants built from the old definition are simply never requested again — they are not served stale, and there is no cache to bust. (They do stay on disk until cleaned up; see below.)

Default quality

quality is the one operation with a configured default, so it does not have to be repeated in every preset:

It sits underneath both layers above — a preset overrides it, and an operation passed alongside overrides that — and it is signed like anything else, so changing it moves every URL that relied on it to a new hash and those variants regenerate rather than being served at the old quality. Set it to null to leave the encoder to its own default.

Being signed does not mean being spelled out: like a preset's operations, the default is merged back in server-side, so it never appears in the URL. Only a quality a caller asked for does.

To keep it out of one particular variant, give that preset an explicit false:

It has to be the preset rather than the call, because the query carries only what the operations normalise to, and a dropped operation normalises to nothing at all: the server would merge the default back in and refuse its own URL. Doing it in $operations throws rather than handing back a URL that 404s.

How the URL works

That last point is why these are all the same variant, and all valid:

The first is what the package generates. The others spell out what thumb already says, which changes nothing — they normalise to the same operations, so they hash the same and resolve to the same file. Say something the preset does not say, though, and the signature no longer covers it: &quality=70 on any of those is a different variant and returns a 404.

The signature is never optional

There is no unsigned path to a variant. VariantFactory::fromRequest() verifies the hash itself and throws rather than returning, so a request that is not signed cannot become a Variant at all — there is no unverified object for a caller to forget to check. That covers every case equally: presets, custom operations, a source in a subdirectory, a renamed file.

Every part of the URL is covered. Swapping thumb for wide, for custom, or for a preset that does not exist, changing the source, adding or altering an operation, renaming the file — each produces a different hash and a 404, and nothing is written to disk on the way.

What is signed is the operations after the preset and the defaults are merged in, which is why a preset URL needs no operations in its query and is generated without them: ?src=uploads/photo.jpg on its own rebuilds the identical variant and validates. That is the preset name in the path earning its keep, not a gap. The merged result is what gets hashed, so the only thing reachable under a given signature is still the one variant that produced it — a shorter query says less, it does not permit more.

Behind that, a Variant cannot even hold a path that would escape. preset and name are single path segments and are refused if they contain a separator, a .., or a null byte; src may name a subdirectory but may not climb or be absolute. So path() stays inside the cache directory however a Variant was built — including one constructed directly in PHP, which goes through neither the route pattern nor the signature.

Sources are then resolved inside the configured disk (and prefix) and rejected if they escape it, and output formats are limited to output_formats.

Dimensions have no upper bound. There is nothing for one to protect against: a URL asking for a 20000px image only exists if this application signed it, so the limit that matters is whichever line of your own code asked for it.

APP_KEY must be set. Without it the digest would be plain SHA-256 over public inputs — computable by anyone, turning the endpoint into an open resize service — so building a URL throws instead.

Guard the input, not just the output

The signature bounds what a URL may ask for. It says nothing about what answering costs, because that is decided by the source rather than by the operations: decoding takes roughly 4 bytes per pixel whatever the file size, so a solid-colour 10000×10000 PNG is ~300KB on disk and ~380MB decoded, and asking it for a 60×40 thumbnail still pays that in full.

max_source_megapixels (default 24) bounds the source itself, read from the image header without decoding. This matters most with the default source disk, which is where uploads land: without it, anyone who can upload an avatar can exhaust a worker on every variant of it. Lower it if memory_limit is tight; set it to 0 to accept anything.

Rotating APP_KEY invalidates every previously generated URL. Existing files stay on disk (until cleaned up) but are never requested again, and new URLs regenerate under a new hash.

Concurrency

Generating a variant is serialised per URL. Without that, a burst of requests arriving for the same uncached image would each decode, resize and encode it at the same time — a page of cold images costs what it should, but one cold URL under load costs N times over.

The first request in takes a lock and generates; the rest wait, then find the file already there and serve it. A request that waits longer than lock.wait gives up and generates anyway, on the grounds that a slow image beats a broken one. Writes go to a temporary file and are renamed into place regardless, so nothing can ever serve a half-written image even if the lock is off.

Every cache store Laravel ships supports locking, including null (as a no-op). A custom store that does not will raise VariantConfigurationException rather than fail obscurely; set lock.enabled to false if that is deliberate.

Cleaning up

Variants accumulate: a preset you edited last year, a page you deleted, a size nobody requests any more. Nothing in the cache is irreplaceable — a deleted variant is regenerated by the next request for it, at the cost of one slow response — so sweeping it is safe.

Only files below the configured cache directory are touched; sources are never read. Directories emptied by the sweep are removed, the cache root itself is kept, and any .tmp files left behind by an interrupted generation get swept up with everything else.

Because generated files are written once and never rewritten, "last written" is effectively "created". If your filesystem records access times (many are mounted relatime or noatime and do not), --accessed ages files by when they were last read instead, which prunes what is genuinely unused rather than what is merely old:

Schedule it in routes/console.php:

The default age lives in the config:

There is deliberately no "clear everything" command, because there is nothing to clear: an edited preset moves its URLs to a new hash rather than leaving stale ones behind, so the old files are simply never asked for again and age out on their own. --days=0 is not that command either — it deletes what is older than now, so anything written in the current second survives. If you really want the cache gone, delete the directory:

Configuration

Key Default What it does
route.prefix storage/variants Where the endpoint is mounted. Must line up with a publicly reachable path to cache.
route.middleware [] Middleware for the endpoint. Deliberately empty — see below.
source.disk public The filesystem disk sources are read from.
source.prefix null An optional directory within that disk to confine sources to.
cache storage_path('app/public/variants') Where generated variants are written. Always a local path — see above.
max_source_megapixels 24 Largest source that will be decoded. 0 disables the check.
hash_length 10 Characters of the HMAC kept in the URL.
source_formats jpg, jpeg, png, gif, webp, avif, bmp What may be read.
output_formats jpg, jpeg, png, gif, webp, avif What may be written.
quality 80 Encoding quality when neither the preset nor the URL sets one. null leaves it to the encoder.
presets [] Named operation sets.
blade.component variant Name the <x-variant> component registers under. null to skip it.
cache_store null Cache store for generation locks and source measurements. null uses the default.
lock.enabled true Serialise generation of the same variant across workers.
lock.ttl 30 Seconds a lock survives a worker that dies holding it.
lock.wait 10 Seconds a queued request waits before generating anyway.
dimensions.ttl 86400 Seconds a source's measurements are remembered. 0 disables caching.
cleanup.days 30 Default age for image-variants:cleanup.

The route is registered outside any middleware group on purpose. Session middleware in particular would attach a Set-Cookie and a private Cache-Control to the one response a real visitor gets from PHP, undoing the long-lived caching the whole scheme exists for.

If you point source.disk at a private disk, note that generated variants still land in cache and are public once generated — the source stays unreachable, the variant does not. Put the endpoint behind route.middleware if that matters.

AI agents

The package ships an Agent Skill for Laravel Boost, at resources/boost/skills/image-variants-development/SKILL.md. If your application uses Boost, it is installed with:

The agent then loads it on demand when a task involves images, which saves it inferring the operation grammar from the source — and, more usefully, stops it hand-assembling variant URLs that will never match the HMAC.

Running the Test Suite

License and attribution

Laravel Image Variants is open-sourced software licensed under the MIT license.


All versions of laravel-image-variants with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
ext-fileinfo Version *
illuminate/console Version ^13.0
illuminate/filesystem Version ^13.0
illuminate/http Version ^13.0
illuminate/image Version ^13.25
illuminate/routing Version ^13.0
illuminate/support Version ^13.24
intervention/image Version ^4.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package fruitcake/laravel-image-variants contains the following files

Loading the files please wait ...