Search by

onlinedigital / laravel-perfect-image

andreitelteu

Automatic responsive image optimization for Laravel

Package info

github.com/OnlineDigital/laravel-perfect-image

pkg:composer/onlinedigital/laravel-perfect-image

Statistics

Installs: 16

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.0.2 2026-10-11 12:12 UTC

This package is auto-updated.

Last update: 2026-10-12 09:58:45 UTC


README

Automatic responsive image optimization for Laravel. Detects rendered image dimensions, caches the most common resolutions, and generates srcset attributes automatically.

Features

  • Automatic detection of rendered image dimensions via JavaScript
  • Caches the most common resolutions per image
  • Driver-based URL generation (supports multiple image services)
  • Blade directive for easy usage
  • Lazy resize tracking

Installation

composer require onlinedigital/laravel-perfect-image

Add Middleware (Optional - for automatic JS injection)

Add to your bootstrap/app.php:

use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'perfect-image' => \OnlineDigital\PerfectImage\Http\Middleware\InjectPerfectImageJs::class,
        ]);
    })
    ->create();

Or apply to specific routes:

Route::get('/home', [HomeController::class, 'index'])->middleware('perfect-image');

Publish Config

php artisan vendor:publish --provider="OnlineDigital\\PerfectImage\\PerfectImageServiceProvider" --tag="perfect-image-config"

Config Options

// config/perfect-image.php
return [
    // Cache driver to store resolution data
    'cache_driver' => env('PERFECT_IMAGE_CACHE_DRIVER', 'file'),
    
    // Enable the collection of rendered image dimensions (tracking attributes,
    // JS injection and the observer endpoint). When disabled, the endpoint
    // answers with HTTP 403 {"code": "collect_dimensions_disabled"}.
    'collect_dimensions' => env('PERFECT_IMAGE_COLLECT_DIMENSIONS', false),

    // How many of the most common resolutions to keep
    'max_resolutions' => 5,
    
    // Minimum width difference to count as a new resolution
    'min_width_diff' => 50,
    
    // The route name for the observer endpoint
    'route_name' => '_perfect_image_observer',
    
    // Default image driver (url, cloudinary, etc.)
    'driver' => env('PERFECT_IMAGE_DRIVER', 'url'),
    
    // Driver-specific configuration
    'drivers' => [
        'url' => [
            // For simple URL manipulation (e.g., adding query params)
            'base_url' => env('APP_URL'),
            'param_format' => '{url}?w={width}&h={height}', // or {url}/{width}x{height}
        ],
        'cloudinary' => [
            'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
            'base_url' => 'https://res.cloudinary.com/{cloud_name}/image/upload',
        ],
        // Add more drivers as needed
    ],
];

Usage

Basic Usage

In your Blade templates:

<img @perfect_image_srcset('unique-key', $imagepath) />

Example:

<img @perfect_image_srcset('hero-image', asset('images/hero.jpg')) />

This generates:

<!-- If resolutions are cached -->
<img src="https://example.com/images/hero.jpg?w=320"
     srcset="https://example.com/images/hero.jpg?w=320 320w, https://example.com/images/hero.jpg?w=640 640w, ..."
     sizes="(max-width: 800px) 100vw, 640px">

<!-- If no cached resolutions and collect_dimensions is enabled -->
<img 
    src="https://example.com/images/hero.jpg"
    data-perfect-image-id="hero-image" 
    data-perfect-image-src="https://example.com/images/hero.jpg"
    onload="window.PerfectImage&&window.PerfectImage.observe(this)">

<!-- If no cached resolutions and collect_dimensions is disabled -->
<img src="https://example.com/images/hero.jpg">

sizes is learned from how the rendered width changes with the viewport width (100vw until something has been learned).

Custom Sizes

The third argument is a list of widths that overrides the learned resolutions:

<img {{ perfect_image_srcset('product-image', $product->image, ['640', '1024', '1280']) }}>

The directive accepts the same arguments:

<img @perfect_image_srcset('product-image', $product->image, ['640', '1024', '1280']) />

JavaScript

While collect_dimensions is enabled, the JavaScript is injected automatically (before </body>) on pages that contain tracked images. It does not need jQuery.

Creating Custom Drivers

Create a class that implements OnlineDigital\PerfectImage\Contracts\Driver:

<?php

namespace App\Drivers;

use OnlineDigital\PerfectImage\Contracts\Driver;

class MyCustomDriver implements Driver
{
    public function generateUrl(string $url, int $width, ?int $height = null): string
    {
        // Generate the URL for the resized image
        return "https://my-cdn.com/{$width}x{$height}/{$url}";
    }
}

Register it in a service provider:

$this->app->bind(\OnlineDigital\PerfectImage\Contracts\Driver::class, MyCustomDriver::class);

How It Works

  1. First Visit: If collection is enabled and no resolutions are cached for an image, the Blade directive adds src, data attributes and an onload handler
  2. JavaScript Tracking: On page load and whenever an image or the window is resized (throttled, with a trailing call), JS measures every tracked image
  3. Dimension Collection: Once enough samples were gathered, all images are sent in a single batch to the _perfect_image_observer endpoint
  4. Caching: The server saves the most common resolutions (at most max_resolutions) and a sizes value
  5. Applying: The response contains src, srcset and sizes, which JS applies without reloading the page
  6. Subsequent Visits: srcset is generated from cached resolutions

Collecting all resolutions automatically (Playwright)

Instead of waiting for someone to resize a browser window, a script can open the pages, sweep the viewport from the smallest phone (320px) to a 2K desktop (2560px) and trigger the collection. This is also the workflow meant for coding agents: after adding a page or a component, run the collector for those URLs and commit the result.

Setup (once)

npm install -D playwright
npx playwright install chromium

Set the JSON file store (PERFECT_IMAGE_CACHE_DRIVER=storage_file). Collected data is written to resources/perfect-image.json, so it can be committed and deployed with the code.

Run

The application must be running (for example php artisan serve) on the same machine/filesystem.

# Specific pages (new page, new component): all of them are visited
php artisan perfect-image:collect --urls=/,/about,/product/12

# Discover pages: robots.txt -> Sitemap:, fallback /sitemap.xml, then a few pages per template
php artisan perfect-image:collect --sitemap

# Pages built from the application routes (static routes + routes bound to Eloquent models)
php artisan perfect-image:collect --from-routes

# Discard the previously collected data of the visited images first
php artisan perfect-image:collect --urls=/product/12 --fresh

What the command does:

  1. Turns the collection on with a temporary flag file (storage/app/perfect_image_collect.flag). The flag is ignored in production, so there is no per request cost there. It is removed afterwards, and the previous state is restored even if the command is interrupted.
  2. Runs scripts/collect.mjs with Playwright. For every page it loads the page, forces lazy images to load, resizes the viewport step by step (and exactly at the usual breakpoints) and measures every tracked image.
  3. Calls the collect API once per page and prints a JSON report (--json for JSON only).

Useful options:

Option Default
--base-url app.url Running application
--max-per-template 3 Discovered pages visited per route (/product/{slug}) or URL directory
--all Visit every discovered URL
--min-width, --max-width, --step 320, 2560, 40 Viewport sweep
--concurrency 2 Pages in parallel (1 with --fresh)
--header="Name: value" Extra request headers
--storage-state=file.json Playwright storage state for pages behind a login

Notes:

  • Only images rendered with @perfect_image_srcset / perfect_image_srcset() are measured.
  • Images inside carousels, tabs, modals or hover menus have no size until they are visible. Visit the page state in a custom Playwright script, or measure them with real users (collect_dimensions).
  • --from-routes visits static GET routes. Do not use it if one of them has side effects.
  • Models are resolved for routes with exactly one parameter that is type-hinted with an Eloquent model.

Without artisan

The script can be used directly (collection must already be enabled, see collect_dimensions):

node vendor/onlinedigital/laravel-perfect-image/scripts/collect.mjs \
    --base-url=http://localhost:8000 --urls=/,/about

Exit codes: 0 ok, 1 errors, 2 nothing measured or images left unmeasured, 3 collection disabled.

Coding agents

See AGENTS.md for the short workflow to give to an agent.

Route

The package automatically registers the observer route:

POST _perfect_image_observer

Request body (resolutions items are [width, height] or [width, height, viewportWidth]):

{
    "id": "unique-image-key",
    "src": "https://example.com/image.jpg",
    "resolutions": [[320, 240], [640, 480], [1024, 768]]
}

Several images can be sent at once (this is what the JavaScript does):

{
    "images": [
        {"id": "hero-image", "src": "https://example.com/hero.jpg", "resolutions": [[320, 240, 360]]}
    ]
}

Responses: 200 with the resulting src/srcset/sizes per image id, 422 for an invalid payload, 403 (collect_dimensions_disabled) when collect_dimensions is off.

License

MIT