fomvasss/laravel-imagepresets

Image presets (on-the-fly resize/convert) for Laravel via League Glide

Maintainers

Package info

github.com/fomvasss/laravel-imagepresets

pkg:composer/fomvasss/laravel-imagepresets

Statistics

Installs: 2

Dependents: 0

Suggesters: 1

Stars: 2

Open Issues: 0

1.7.0 2026-05-03 09:02 UTC

This package is auto-updated.

Last update: 2026-05-03 09:19:19 UTC


README

πŸ‡ΊπŸ‡¦ ДокумСнтація ΡƒΠΊΡ€Π°Ρ—Π½ΡΡŒΠΊΠΎΡŽ

On-the-fly image resizing, converting and caching for Laravel, powered by League/Glide.

PHP Laravel Latest Stable Version Build Status Total Downloads License

Features

  • On-the-fly resize, crop and format conversion (WebP, AVIF, JPG, PNG, GIF)
  • Automatic caching of processed images to any Laravel filesystem disk (local, S3, GCS, FTP, etc.)
  • Remote disk support (S3 / GCS / FTP) β€” Glide processes locally, result is uploaded automatically
  • SVG passthrough with optional XSS sanitization
  • Remote image support with SSRF and image-bomb protection
  • Race condition protection via Cache lock (Redis/Memcached)
  • Auto-registered route β€” no manual setup required
  • Facade + global helper + Blade directive
  • Artisan command imagepresets:clear
  • Fully configurable via config/imagepresets.php

Requirements

Dependency Version
PHP ^8.1
Laravel 10 / 11 / 12 / 13
league/glide ^2.0 | ^3.0

Optional:

  • imagick PHP extension β€” required for AVIF output and SVG rasterization
  • enshrined/svg-sanitize β€” full SVG sanitization (recommended)

Installation

composer require fomvasss/laravel-imagepresets

The service provider is auto-discovered via Laravel's package discovery.

Publish configuration

php artisan vendor:publish --tag=imagepresets-config

This creates config/imagepresets.php in your application.

Configuration

Key options in config/imagepresets.php:

// Route
'route' => [
    'prefix'     => env('IMAGEPRESET_ROUTE_PREFIX', 'imagepresets'),
    'name'       => env('IMAGEPRESET_ROUTE_NAME', 'imagepresets'),
    'middleware' => ['throttle:240,1'],
],

// Storage disk and subdirectory for cached presets
'disk' => env('IMAGEPRESET_DISK', 'public'),  // or 's3', 'gcs', etc.
'path' => env('IMAGEPRESET_PATH', 'imagepresets'),

// Processing driver: 'gd' or 'imagick'
'driver' => env('IMAGEPRESET_DRIVER', 'gd'),

// Default output quality and format
'quality' => 80,
'format'  => 'webp',

// HTTP cache lifetime (seconds)
'cache_max_age' => 31536000,

// Allowed dimensions, qualities, fit methods, formats
// Use ['*'] as a wildcard to allow any value (no restriction)
'allowed_widths'    => [100, 200, 300, 400, 600, 800, 1000, 1200, 1600],
'allowed_heights'   => [100, 200, 300, 400, 600, 800],
'allowed_sizes'     => [[300, 200], [600, 400], [1200, 800]],
'allowed_qualities' => [50, 60, 70, 80, 90, 100],
'allowed_fits'      => ['contain', 'crop', 'fill', 'fill-max', 'max', 'stretch'],
'allowed_formats'   => ['webp', 'jpg', 'png', 'gif'],

// SVG optionslaravel-imagepresets
'svg' => [
    'sanitize'                 => true,
    'remove_remote_references' => true,
    'rasterize'                => false, // requires driver=imagick
],

// Remote image protection
'max_download_bytes' => 20 * 1024 * 1024,
'max_image_pixels'   => 150_000_000, // ~150 Mpx image-bomb protection
'allowed_hosts'      => [], // e.g. ['cdn.example.com']

// Local Glide working dir when using a remote disk (S3/GCS/FTP)
'local_cache_dir' => storage_path('app/imagepreset_glide_cache'),

Cache Lock: For correct multi-server behaviour set CACHE_DRIVER=redis in .env. The file driver only locks within a single PHP process.

Remote disk (S3 / GCS / FTP)

Set the disk in .env β€” the package detects it automatically:

IMAGEPRESET_DISK=s3
IMAGEPRESET_PATH=imagepresets

Processing flow for remote disks:

  1. Glide processes the image into local_cache_dir (local)
  2. The result is uploaded to the remote disk via Flysystem
  3. The local file is deleted
  4. The response is streamed directly from the remote disk
# Clear S3 preset cache
php artisan imagepresets:clear --disk=s3

Usage

Endpoint

GET /imagepresets?src=...&w=...&h=...&q=...&fm=...&fit=...

Query parameters

Parameter Type Description
src string Required. Relative path or remote URL of the source image
preset string Named preset defined in config/imagepresets.php (presets section)
w int Output width in pixels (must be in allowed_widths, or any if ['*'])
h int Output height in pixels (must be in allowed_heights, or any if ['*'])
q int Quality 1–100 (must be in allowed_qualities, or any if ['*'])
fm string Output format: webp, jpg, png, gif, avif
fit string Fit method: contain, crop, fill, fill-max, max, stretch
blur int Blur radius 0–100
sharp int Sharpen amount 0–100
or string Orientation: auto (EXIF), 0, 90, 180, 270
crop string Coordinate crop: w,h,x,y β€” e.g. 200,200,10,10
bg string Background fill colour (hex without #): fff, ff5733

When both w and h are passed, the pair must be listed in allowed_sizes (unless allowed_sizes = ['*']).

Fit methods

Value Description
contain Scales the image to fit within wΓ—h, preserving aspect ratio. No cropping. Transparent/empty space is not filled.
max Same as contain but never upscales beyond the original dimensions.
fill Scales the image to fill the entire wΓ—h canvas. Empty space is filled with the bg colour. May upscale small images.
fill-max Same as fill but never upscales β€” if the image is smaller than the canvas it is centred and the remaining space is filled with bg. Equivalent to Spatie MediaLibrary's Fit::FillMax.
crop Scales and crops the image to exactly wΓ—h. No empty space, but edges may be trimmed.
stretch Stretches the image to exactly wΓ—h ignoring the aspect ratio.

fill-max vs crop: use fill-max when the full image must remain visible (e.g. og:image banners, product feeds); use crop when exact pixel dimensions are required and trimming is acceptable.

// The full image is visible; white padding fills the remaining canvas area
$url = imagepreset_url('photo.jpg', ['w' => 1300, 'h' => 650, 'fit' => 'fill-max', 'bg' => 'ffffff', 'fm' => 'jpg']);

Wildcard mode

Set any of the allowed_* config keys to ['*'] to disable the corresponding restriction entirely:

'allowed_widths'    => ['*'], // any width is accepted
'allowed_heights'   => ['*'], // any height is accepted
'allowed_sizes'     => ['*'], // any w+h pair is accepted
'allowed_qualities' => ['*'], // any quality value 1–100 is accepted

Note: Base HTTP validation limits still apply β€” w and h are capped at 20000, q must be an integer.

Helper function

$url = imagepreset_url('storage/images/photo.jpg', ['w' => 800, 'fm' => 'webp']);
// β†’ https://example.com/imagepresets?fm=webp&src=storage%2Fimages%2Fphoto.jpg&w=800
$url = imagepreset_url('https://example.com/storage/images/photo.jpg', ['w' => 800, 'fm' => 'webp']);
// β†’ https://example.com/imagepresets?fm=webp&src=https://example.com/storage%2Fimages%2Fphoto.jpg&w=800

Named Presets

Define reusable named presets in config/imagepresets.php:

'presets' => [
    'thumb'  => ['w' => 300, 'h' => 200, 'fm' => 'webp', 'q' => 80, 'fit' => 'crop'],
    'hero'   => ['w' => 1200, 'fm' => 'webp', 'q' => 85],
    'avatar' => ['w' => 96, 'h' => 96, 'fm' => 'webp', 'fit' => 'crop'],

    // og:image social banner β€” fill-max keeps the full image, fills gaps with bg colour
    'og_banner' => ['w' => 1300, 'h' => 650, 'fit' => 'fill-max', 'fm' => 'jpg', 'q' => 85, 'bg' => 'ffffff'],
],

Use a preset by name:

// Helper β€” shorthand string
$url = imagepreset_url('photo.jpg', 'thumb');

// Helper β€” array key
$url = imagepreset_url('photo.jpg', ['preset' => 'hero']);

// Facade
Imagepreset::url('photo.jpg', 'avatar');

// Blade directive
<img src="@imagepreset('photo.jpg', 'thumb')" alt="Thumbnail">

// HTML endpoint
<img src="/imagepresets?src=photo.jpg&preset=thumb" alt="Thumbnail">

Explicit params passed alongside a preset override the preset defaults:

// Uses thumb preset but overrides format to jpg
$url = imagepreset_url('photo.jpg', ['preset' => 'thumb', 'fm' => 'jpg']);

Security: preset params come from trusted config and bypass allowed_widths / allowed_heights / allowed_sizes / allowed_qualities checks. Explicit override params are still validated against the allowlists.

Facade

use Fomvasss\Imagepresets\Facades\Imagepreset;

$url = Imagepreset::url('storage/images/photo.jpg', ['w' => 400, 'h' => 300]);

Blade directive

<img src="@imagepreset('storage/images/photo.jpg', ['w' => 600, 'fm' => 'webp'])" alt="Photo">

HTML example

<img src="/imagepresets?src=storage/images/photo.jpg&w=800&fm=webp" alt="Photo">

SVG Support

SVG files are passed through without dimension transformations. The cache key is based solely on src to avoid duplicates.

// SVG is cached and served as-is (sanitized by default)
$url = imagepreset_url('storage/icons/logo.svg');

Enable sanitization (recommended) in config:

'svg' => [
    'sanitize' => true,
],

For full sanitization install the optional dependency:

composer require enshrined/svg-sanitize

Without it, a basic regex sanitizer removes <script> tags, on* event attributes, and javascript: URIs.

SVG rasterization

To convert SVG to raster when w, h or fm is passed:

'svg' => [
    'rasterize' => true, // requires driver=imagick
],

Remote Images

Pass any allowed external URL as src:

$url = imagepreset_url('https://cdn.example.com/photo.jpg', ['w' => 400]);

Allowed hosts must be declared in config:

'allowed_hosts' => [
    'cdn.example.com',
],

Security measures:

  • HTTP redirects are blocked (SSRF protection)
  • Private and reserved IP ranges are rejected
  • localhost is rejected
  • Maximum download size: max_download_bytes
  • Maximum pixel area: max_image_pixels (image-bomb protection)

Artisan Commands

Clear preset cache

php artisan imagepresets:clear

Options:

Option Description
--disk= Override disk (default: imagepresets.disk config)
--path= Override path (default: imagepresets.path config)
--temp Also clear source_dir and temp_dir
# Clear cache + temp directories
php artisan imagepresets:clear --temp

# Clear a custom disk/path
php artisan imagepresets:clear --disk=s3 --path=presets

Audit Log (discovering required sizes)

Use audit logging in local / staging environments to discover which image params the frontend actually requests β€” then promote them to explicit allowlists for production.

Workflow

  1. Enable wildcard mode + audit log in .env (non-production only):
IMAGEPRESET_AUDIT_LOG=true
# IMAGEPRESET_AUDIT_LOG_ONLY_NEW=true  # log only cache misses (first generation)

Entries are written to the application default log channel (LOG_CHANNEL in .env).

  1. Let the frontend work freely β€” every new param combination is logged:
{"message":"imagepreset_request","context":{"params":{"src":"products/photo.jpg","w":640,"fm":"webp"},"ip":"127.0.0.1","url":"http://app.test/imagepresets?src=..."}}
  1. Analyse the log to collect unique combinations:
# All unique w values requested
grep -oh '"w":[0-9]*' storage/logs/*.log | sort -u

# All unique [w, h] pairs
grep -oh '"w":[0-9]*,"h":[0-9]*' storage/logs/*.log | sort -u

# All unique quality values
grep -oh '"q":[0-9]*' storage/logs/*.log | sort -u
  1. Promote findings to explicit allowlists in config/imagepresets.php and disable wildcard + audit log before deploying to production:
'allowed_widths'    => [320, 640, 960, 1280],
'allowed_heights'   => [200, 400],
'allowed_sizes'     => [[640, 400], [1280, 800]],
'allowed_qualities' => [80, 90],
# .env (production)
IMAGEPRESET_AUDIT_LOG=false

Config reference

Key Default Description
audit_log.enabled false Enable/disable via IMAGEPRESET_AUDIT_LOG
audit_log.only_new true Log only cache misses β€” skip already-cached combinations

Excluding Preset Cache from Backups

The /imagepresets cache directory contains auto-generated files that can always be recreated on demand. Including it in backups wastes storage and increases backup time.

Recommended: separate disk outside the backup scope

Define a dedicated filesystem disk that lives outside your regular backup directory:

// config/filesystems.php
'imagepresets' => [
    'driver' => 'local',
    'root'   => storage_path('app/imagepresets'), // not inside app/public
    // 'url'    => env('APP_URL').'/imagepresets', // no need for a URL since this is only a temporary working dir for Glide
    'visibility' => 'public',
    'throw'      => false,
],
# .env
IMAGEPRESET_DISK=imagepresets

Now the cache folder is completely outside storage/app/public and will never appear in backups that include only storage_path('app/public').

Note: The cache directory is recreated automatically on the first request for each preset β€” no manual intervention is needed after a restore.

HTTP Caching & CDN / Reverse Proxy

Every response from the /imagepresets endpoint includes headers optimised for aggressive edge caching:

Cache-Control: public, max-age=31536000, s-maxage=31536000, immutable
ETag: "<hash>"
Last-Modified: <date>

Nginx

Cache processed presets directly on the server β€” Laravel is bypassed on subsequent requests:

proxy_cache_path /var/cache/nginx/imagepresets
    levels=1:2
    keys_zone=imagepresets:20m
    max_size=2g
    inactive=365d
    use_temp_path=off;

server {
    # ...

    location /imagepresets {
        proxy_cache            imagepresets;
        proxy_cache_valid      200 365d;
        proxy_cache_use_stale  error timeout updating http_500 http_502 http_503;
        proxy_cache_lock       on;                    # prevents cache stampede
        proxy_cache_key        "$scheme$host$request_uri";
        add_header             X-Cache-Status $upstream_cache_status;

        proxy_pass http://127.0.0.1:9000;             # your PHP-FPM / app
    }
}

Cloudflare

Add a Cache Rule in the Cloudflare dashboard:

  • If β†’ URI Path starts with /imagepresets
  • Then β†’ Cache Level: Cache Everything, Edge Cache TTL: 1 year

Or via Terraform / API:

{
  "description": "Cache imagepresets",
  "expression": "(http.request.uri.path starts_with \"/imagepresets\")",
  "action": "set_cache_settings",
  "action_parameters": {
    "cache": true,
    "edge_ttl": { "mode": "override_origin", "default": 31536000 },
    "browser_ttl": { "mode": "override_origin", "default": 31536000 }
  }
}

Cache invalidation

When you change a preset definition or replace a source image, the cached files must be purged:

# Clear the Laravel-level disk cache (always required)
php artisan imagepresets:clear

# Nginx β€” reload or flush proxy cache directory
find /var/cache/nginx/imagepresets -type f -delete

# Cloudflare β€” purge by prefix via API
curl -X POST "https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache" \
     -H "Authorization: Bearer {TOKEN}" \
     -H "Content-Type: application/json" \
     --data '{"prefixes":["https://example.com/imagepresets"]}'

Tip: Use versioned src paths (e.g. photo_v2.jpg) or append a query param (?v=2) to bust the cache without a full purge.

Response Headers

Every response includes:

Header Value
Content-Type Correct MIME type
Cache-Control public, max-age=N, s-maxage=N, immutable
ETag Based on file mtime + size
Last-Modified File modification time
Content-Disposition inline
X-Content-Type-Options nosniff
Content-Security-Policy SVG only: default-src 'none'; style-src 'unsafe-inline'; sandbox

Security

Threat Protection
Path traversal .. and null bytes rejected in src
SSRF via remote URL Private/reserved IPs + localhost blocked; redirects disabled
Image bomb Pixel area check (max_image_pixels) for both local and remote files
SVG XSS Sanitization via enshrined/svg-sanitize or regex fallback; CSP header
Cache stampede Cache::lock() with double-check after acquiring
Content sniffing X-Content-Type-Options: nosniff

Testing

composer test
# or
vendor/bin/phpunit

License

MIT β€” see LICENSE.