Search by

zoker / responsive-images

zoker

There is no license information available for the latest version (1.4) of this package.

Responsive images with webp conversion for Laravel

Package info

github.com/zoker68/responsive-images

pkg:composer/zoker/responsive-images

Statistics

Installs: 264

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.4 2026-10-06 11:12 UTC

This package is auto-updated.

Last update: 2026-10-06 11:13:06 UTC


README

Laravel package for automatic image conversion to WebP and responsive sizes generation.

Installation

composer require zoker/responsive-images

Configuration

Publish the configuration file:

php artisan vendor:publish --tag="responsive-images-config"

Driver

// config/responsive-images.php
'driver' => env('RESPONSIVE_IMAGES_DRIVER', extension_loaded('imagick') ? 'imagick' : 'gd'),

Imagick is the default when ext-imagick is loaded; otherwise the package falls back to GD, so a server without the extension keeps working. Set RESPONSIVE_IMAGES_DRIVER to pin one.

imagick (default) gd (fallback)
PHP extension ext-imagick ext-gd
Reads jpg/jpeg, png, webp, gif, avif, bmp, tif/tiff, heic/heif and whatever the ImageMagick build supports jpg/jpeg, png, webp, gif, avif, bmp
Animated GIF animated WebP (every frame is resized for every breakpoint, so slower and heavier) static WebP (first frame only)
Photos faster (≈1.7× on a 4000×3000 JPEG) slower
Memory ≈8 bytes per pixel (Q16), allocated outside PHP; capped by imagick.* (see Memory) ≈5 bytes per pixel, allocated outside PHP by libgd on most distro builds; no cap

Run php artisan responsive-images:clear after switching the driver: generated file names do not depend on it, so the old files would keep being served.

Memory

Both drivers must be planned against the RAM of the server, not against memory_limit. Imagick allocates in ImageMagick, outside the PHP heap. GD is counted in memory_limit only with the libgd bundled into PHP (docker-php-ext-install gd, Herd); Debian/Ubuntu packages (php8.x-gd) are built with --with-external-gd, where the system libgd allocates with malloc and memory_limit does not see it. queue:work --memory counts PHP memory only and does not help either. When the kernel kills the worker, nothing is logged by PHP: the job stays reserved until retry_after, is picked up again with attempts above tries and fails as MaxAttemptsExceededException without a cause. Look in journalctl -k | grep -i oom.

generate() decodes the original once, reduces it to the target size once and cuts every breakpoint from that base, so with a target width the peak is about one decoded copy of the original (two for a moment while its full-size WebP is encoded) plus the small base:

Original Pixels Imagick (8 B/px) GD (5 B/px)
4000×3000 12 Mpx ≈ 96 MB ≈ 60 MB
4000×4000 16 Mpx ≈ 128 MB ≈ 80 MB
8000×4500 36 Mpx ≈ 288 MB ≈ 180 MB

Without a target width (make('a.png') with no dimensions) the base is the original itself, so the peak stays around two copies. What keeps such a job alive is the Imagick resource limits, applied with Imagick::setResourceLimit() when the driver is imagick, once per process (queue worker and php-fpm alike):

'imagick' => [
    'memory' => env('RESPONSIVE_IMAGES_IMAGICK_MEMORY', '256M'),
    'map' => env('RESPONSIVE_IMAGES_IMAGICK_MAP', '512M'),
    'area' => env('RESPONSIVE_IMAGES_IMAGICK_AREA'),   // pixels, e.g. 16000000; null = default
    'disk' => env('RESPONSIVE_IMAGES_IMAGICK_DISK'),   // null = default (unlimited)
],

Above memory ImageMagick moves the pixel cache to memory-mapped files, above map to plain disk files: slower, but the worker survives. Generation happens once per image and size set, so that slow pass is a one-off. Values are bytes or a size with a K/M/G suffix; null leaves ImageMagick's own default. These are upper bounds: a lower value in the server's policy.xml wins. The defaults are conservative on purpose, since the package is also installed on small VPSes; raise them on servers with more RAM. GD has no such switch: on a GD host the server's RAM is the only limit, so keep uploads reasonably sized.

queue => false or a sync queue connection moves all of this into php-fpm: on a small server a large original will then kill the web request, not a worker. Keep an asynchronous queue on such hosts.

Supported Formats

Only files whose extension is listed in extensions are converted (comparison is case-insensitive, so PHOTO.JPG counts as jpg):

// config/responsive-images.php
'extensions' => ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif', 'bmp', 'tif', 'tiff', 'heic', 'heif'],

A listed extension is converted only if the driver on this server can read it: GD is checked with imagetypes() (AVIF needs GD built with libavif), Imagick with Imagick::queryFormats() (HEIC needs ImageMagick built with libheif). With gd, tif/tiff/heic/heif are therefore served as-is.

Never add vector or document formats (svg, pdf, ps, eps) when using Imagick: ImageMagick hands them to external delegates, which is a known attack surface. SVG is best served as-is anyway.

Animated GIF with the gd driver becomes a static WebP. Use imagick, or remove gif from extensions.

Formats browsers cannot display (tif/tiff, heic/heif). Before the job has run, the untouched original would be useless as a fallback, so the first make() converts the full-size WebP synchronously (one conversion; the breakpoint sizes are still built by the job). If that conversion fails, the error is reported and the original URL is returned.

Any other file (svg, ...) is served as-is:

  • no GenerateResponsiveImages job is dispatched and nothing is written to the output disk;
  • make() returns a ResponsiveImage with src = URL of the original on the source disk, empty generatedImages, width and height = 0 (the passed values are ignored: the file is served untouched, so they would not describe it) and format = the file extension;
  • generate() returns null;
  • the rendered <picture> contains only the <img>, without <source> and without width/height attributes.

A <source type="image/webp"> is rendered only once generated images in the output format exist. Until the job has finished, the original (or its WebP copy) is rendered as a plain <img>.

Usage

Facade

use Zoker\ResponsiveImages\Facades\ResponsiveImages;

// With specific dimensions
$image = ResponsiveImages::make(
    'uploads/products/image.jpg',
    width: 1200,
    height: 800,
    disk: 'public',
);

// Or use original image dimensions
$image = ResponsiveImages::make(
    'uploads/products/image.jpg',
    disk: 'public',
);

// Default: lazy loading
echo $image->toHtml('Product image');

// For hero images: eager loading
echo $image->toHtml('Hero image', 'eager');

Blade Component

{{-- With specific dimensions --}}
<x-responsive-image
    path="uploads/products/image.jpg"
    :width="1200"
    :height="800"
    alt="Product image"
    disk="public"
/>

{{-- Or use original dimensions --}}
<x-responsive-image
    path="uploads/products/image.jpg"
    alt="Product image"
    disk="public"
/>

{{-- Hero image with eager loading --}}
<x-responsive-image
    path="uploads/hero.jpg"
    :width="1920"
    :height="1080"
    alt="Hero image"
    loading="eager"
    disk="public"
/>
{{-- Image in a one-third column: let the browser pick a size for 33vw, not 100vw --}}
<x-responsive-image
    path="uploads/products/image.jpg"
    :width="800"
    alt="Product image"
    sizes="(min-width: 1024px) 33vw, 100vw"
/>

Blade Directive

{{-- Default: lazy loading --}}
@responsiveImage(
    'uploads/products/image.jpg',
    width: 1200,
    height: 800,
    alt: 'Product image',
    disk: 'public'
)

{{-- Hero image with eager loading --}}
@responsiveImage(
    'uploads/hero.jpg',
    width: 1920,
    height: 1080,
    alt: 'Hero image',
    loading: 'eager',
    disk: 'public'
)
{{-- Custom sizes attribute --}}
@responsiveImage(
    'uploads/products/image.jpg',
    width: 800,
    alt: 'Product image',
    sizes: '(min-width: 1024px) 33vw, 100vw'
)

Working with the ResponsiveImage Object

ResponsiveImages::make() returns a ResponsiveImage object with the following methods:

$image = ResponsiveImages::make('uploads/products/image.jpg', width: 1200, height: 800);

// Get all generated images as [width => url] array
$image->getImages();
// e.g. [320 => 'https://...', 640 => 'https://...', 1200 => 'https://...']

// Get the URL of the closest generated image to the given width
// Prefers equal or larger sizes; falls back to largest available if all are smaller.
// Returns $src when nothing has been generated (e.g. SVG or before the job has run)
$image->getImage(500);
// returns URL of the nearest generated size (e.g. 640px version)

// Get srcset string
$image->getSrcset();
// e.g. "https://.../image-320-abc.webp 320w, https://.../image-640-def.webp 640w, ..."

// Whether a <source> is rendered (generated images in the output format exist)
$image->hasSource();

// Render as HTML <picture> element
$image->toHtml('Alt text');
$image->toHtml('Alt text', 'eager');
$image->toHtml('Alt text', 'lazy', ['class' => 'my-image']);
// Override the sizes attribute (the image itself is not affected, so it is not part of the cache key)
$image->toHtml('Alt text', 'lazy', [], '(min-width: 1024px) 33vw, 100vw');

Available public properties:

Property Type Description
$src string URL of the largest generated image (or the original / fallback)
$generatedImages array All generated images as [width => url]
$srcset string The srcset attribute value (same as getSrcset(); handy where methods cannot be called, e.g. sandboxed Twig)
$sizes string The default sizes attribute value (100vw)
$width int Target width (0 for unsupported files)
$height int Target height (0 for unsupported files)
$format string Output format (e.g. webp), or the source extension for unsupported files / fallback

Artisan Commands

Clear Generated Images

# Clear all
php artisan responsive-images:clear

# Clear for specific file
php artisan responsive-images:clear uploads/products/image.jpg

Note: Images are automatically regenerated when the original file changes or config parameters are updated. No manual regeneration needed!

Dimensions

width and height of the result (and of the rendered <img>) are the same whether make() builds it from freshly generated files or from files already on the output disk:

Arguments Target size Generated widths
width and height as passed, the original is not read breakpoints ≤ width, plus width
width only height = width × original ratio breakpoints ≤ width, plus width
height only width = original width, height as passed breakpoints ≤ original width, plus it
none original width and height breakpoints ≤ original width, plus it

When the original has to be consulted, only its header is read (getimagesize(), no decoding). A JPEG/TIFF with an EXIF orientation of 5–8 is auto-rotated on conversion, so its width and height are swapped here too. If the header cannot be read (e.g. HEIC, which getimagesize() does not understand, or a corrupt file), the result carries the passed values (0 when omitted) and the missing sizes are requested on every cache refresh, as before.

Cache

The result of make() is cached with Cache::flexible() in cache_store; cache_ttl is [stale, expire] in seconds, [86400, 604800] by default. A cache hit touches neither disk nor image. The entry is recomputed in the background after stale, and synchronously after expire. It only changes when the original file, breakpoints, quality or format change, so after replacing an image under the same name run php artisan responsive-images:clear (or lower the TTL).

Paths

A leading / in the source path is ignored: make('/uploads/a.jpg') is the same image, cache entry and job as make('uploads/a.jpg'). make('/') and make('') return null, and responsive-images:clear / clears everything.

How It Works

  1. Package accepts image path, optional target width and height (uses original dimensions if not specified), Alt attribute, optional disk (uses default disk if not specified)
  2. Automatically calculates responsive sizes based on breakpoints from config
  3. Converts image to WebP
  4. Generates versions for each size with hash-based filenames
  5. Caches results using hash of (file modification time + dimensions + quality + format)
  6. Automatically invalidates cache when original file or parameters change
  7. Returns HTML with <picture> and srcset

Queue

Missing sizes are built by the GenerateResponsiveImages job, and until it has run make() returns the fallback. Generation happens inside the make() call instead, and the first call already returns every size, when:

  • queue is false (RESPONSIVE_IMAGES_QUEUE=false), or
  • the default queue connection uses the sync driver.

If that in-request generation fails, the error is reported and the fallback is returned, so the page still renders. Decoding then happens inside the web request: see the memory note under Configuration before enabling this on a small server.