PHPackages                             laraextend/media-toolkit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. laraextend/media-toolkit

ActiveLibrary[Templating &amp; Views](/categories/templating)

laraextend/media-toolkit
========================

A comprehensive Laravel media toolkit for processing images, animated images, vector graphics, audio and video — with automatic optimization, responsive variants, next-gen formats (WebP, AVIF), smart caching and Artisan commands.

v1.0.0(2mo ago)0285MITPHPPHP ^8.2

Since Feb 19Pushed 2mo agoCompare

[ Source](https://github.com/laraextend/media-toolkit)[ Packagist](https://packagist.org/packages/laraextend/media-toolkit)[ Docs](https://github.com/laraextend/media-toolkit)[ RSS](/packages/laraextend-media-toolkit/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (6)Dependencies (7)Versions (8)Used By (0)

 [![Latest Version on Packagist](https://camo.githubusercontent.com/abe212f1dd8675fe57a7281845d200d6791f2f57d0912206bc4c16b9dd0c0f4c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6c617261657874656e642f6d656469612d746f6f6c6b69742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laraextend/media-toolkit) [![Total Downloads](https://camo.githubusercontent.com/07fdfd151c1af0704a7b7e61a11fa74aefa88efdd8ef30f3fffb1dda3c059efc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c617261657874656e642f6d656469612d746f6f6c6b69742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laraextend/media-toolkit) [![PHP Version](https://camo.githubusercontent.com/ff64763a3fc8c390f04d5c89e52d314890843b4c98818143b6d5ab9108501d98/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6c617261657874656e642f6d656469612d746f6f6c6b69742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laraextend/media-toolkit) [![License](https://camo.githubusercontent.com/55c0218c8f8009f06ad4ddae837ddd05301481fcf0dff8e0ed9dadda8780713e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

Laravel Media Toolkit
=====================

[](#laravel-media-toolkit)

**A comprehensive Laravel media toolkit for automatic image optimization, transformations, responsive variants, next-gen formats and more — ready to use directly in Blade.**

`laraextend/media-toolkit` handles the heavy lifting for you: images are resized, cropped, filtered, compressed, converted to modern formats (WebP, AVIF) and rendered as responsive `` or `` tags via a clean fluent API.

> **Roadmap:** Future releases will extend the toolkit to cover animated images (GIF/APNG/WebP animated), vector graphics (SVG), audio and video processing — all behind the same `Media::` facade.

---

✨ Features
----------

[](#-features)

- **🔗 Fluent Builder API** — `Media::image($path)->resize(800)->grayscale()->html(alt: 'Hero')`
- **🧩 Four Blade Components** — ``, ``, ``, ``
- **🖼️ Four Blade Helpers** — `img()`, `responsive_img()`, `picture()` and `img_url()` (deprecated, still available)
- **📐 Image Transformations** — resize, fit, stretch, crop with automatic proportional scaling
- **🎨 Image Filters** — grayscale, sepia, negate, brightness, contrast, colorize, blur, smooth, rotate, flip, watermark
- **📱 Automatic Responsive Variants** — Generates 5 breakpoint sizes (0.5×, 0.75×, 1×, 1.5×, 2×) with `srcset`
- **🌐 Next-Gen Formats** — WebP, AVIF, JPEG, PNG with automatic fallback
- **⚡ Smart Caching** — Manifest-based cache with automatic invalidation and filter-aware cache keys
- **🔧 Artisan Commands** — `media:cache-clear` and `media:cache-warm` with optional `--type=` flag
- **⚙️ Configurable** — Publish `config/media-toolkit.php` to customize quality, formats, breakpoints and more
- **🏎️ Performance-Optimized** — Lazy loading, `fetchpriority`, `decoding="async"` by default
- **🛡️ Memory-Safe Fallback** — Automatically serves original images when GD memory would be exceeded
- **📦 Zero Config** — Works immediately after installation
- **🔄 GD &amp; Imagick** — Automatic driver detection
- **⚡ Livewire &amp; Alpine.js Ready** — Blade components forward `wire:*`, `x-*` and `data-*` attributes automatically

---

📋 Requirements
--------------

[](#-requirements)

- **PHP** &gt;= 8.2
- **Laravel** &gt;= 10.x
- **Intervention Image** &gt;= 3.0 (`intervention/image`)
- **GD** or **Imagick** PHP extension
- Optional: AVIF support in GD (`imageavif`) or Imagick

---

🚀 Installation
--------------

[](#-installation)

```
composer require laraextend/media-toolkit
```

> The ServiceProvider and `Media` Facade alias are registered automatically via Laravel's Auto-Discovery.

### Optional: Publish Configuration

[](#optional-publish-configuration)

```
php artisan vendor:publish --tag=media-toolkit-config
```

---

🔗 Fluent API — `Media::image()`
-------------------------------

[](#-fluent-api--mediaimage)

The primary API is a fluent builder accessed through the `Media` facade.

```
use Laraextend\MediaToolkit\Facades\Media;

// Simple optimized URL
$url = Media::image('resources/images/hero.jpg')
    ->resize(width: 800)
    ->format('webp')
    ->url();

// Full responsive  with filters
echo Media::image('resources/images/hero.jpg')
    ->resize(width: 1200)
    ->grayscale()
    ->picture(formats: ['avif', 'webp'], fallback: 'jpg')
    ->fetchpriority('high')
    ->html(alt: 'Hero', class: 'w-full');
```

---

📐 Transformations
-----------------

[](#-transformations)

### `resize(?int $width, ?int $height)` — Proportional Resize

[](#resizeint-width-int-height--proportional-resize)

Scale the image while preserving aspect ratio. Provide width, height, or both (contain-box).

```
Media::image('photo.jpg')->resize(width: 800)              // → 800px wide, height proportional
Media::image('photo.jpg')->resize(height: 600)             // → 600px tall, width proportional
Media::image('photo.jpg')->resize(width: 800, height: 600) // → fit inside 800×600, no crop
```

Chain `->upscale()` to allow resizing beyond the original dimensions (capped by default):

```
Media::image('small.jpg')->resize(width: 1200)->upscale()->url();
```

### `fit(int $width, int $height)` — Cover + Crop

[](#fitint-width-int-height--cover--crop)

Scale so the image fills the frame completely, cropping the overflow from center.

```
Media::image('photo.jpg')->fit(400, 400)->url();  // Always exactly 400×400
```

### `stretch(int $width, int $height)` — Exact Dimensions, No Aspect Ratio

[](#stretchint-width-int-height--exact-dimensions-no-aspect-ratio)

Resize to exact dimensions, ignoring aspect ratio.

```
Media::image('photo.jpg')->stretch(200, 200)->url();
```

### `crop(int $width, int $height, int|string $x = 0, int|string $y = 0)` — Region Extract

[](#cropint-width-int-height-intstring-x--0-intstring-y--0--region-extract)

Extract a region from the original image without scaling. String offsets: `'left'`, `'center'`, `'right'`, `'top'`, `'bottom'`.

```
Media::image('photo.jpg')->crop(400, 200, 'center', 'center')->url();
Media::image('photo.jpg')->crop(400, 200, 100, 50)->url();  // pixel offsets
```

> **Note:** Only one size/crop operation can be used per chain. Combining `resize()` + `fit()` etc. throws a `MediaBuilderException`.

### `original()` — No Processing

[](#original--no-processing)

Serve the original file without any transformation or optimization.

```
Media::image('resources/images/photo.jpg')->original()->url();
```

> `original()` locks the chain — calling `resize()`, `format()`, `quality()`, filters or `watermark()` afterwards throws a `MediaBuilderException`.

---

🎨 Filters
---------

[](#-filters)

Filters are stackable and can be combined in any order.

```
Media::image('photo.jpg')
    ->resize(width: 800)
    ->grayscale()
    ->blur(3)
    ->brightness(20)
    ->html(alt: 'Photo');
```

MethodDescription`->grayscale()`Convert to black &amp; white`->sepia()`Apply a warm sepia tone`->negate()`Invert all colors`->brightness(int $level)`Adjust brightness: −255 (darkest) to +255 (brightest)`->contrast(int $level)`Adjust contrast: −100 to +100`->colorize(int $r, int $g, int $b)`Tint with RGB offset: −255 to +255 per channel`->blur(int $amount = 1)`Apply blur (amount = number of passes)`->smooth(int $level)`Smooth/sharpen: −10 (max sharpen) to +10 (max smooth)`->rotate(int|string $angle)`Rotate degrees CCW, or `'auto'` for EXIF-based rotation`->flipHorizontal()`Mirror left-right`->flipVertical()`Mirror top-bottom`->flipBoth()`Mirror both axes### `watermark(string $source, string $position, int $padding, int $opacity)` — Overlay

[](#watermarkstring-source-string-position-int-padding-int-opacity--overlay)

```
Media::image('photo.jpg')
    ->resize(width: 1200)
    ->watermark(
        source:   'resources/images/watermark.png',
        position: 'bottom-right',
        padding:  20,
        opacity:  80,
    )
    ->html(alt: 'Photo');
```

Position values: `'top-left'` `'top-center'` `'top-right'` `'center-left'` `'center'` `'center-right'` `'bottom-left'` `'bottom-center'` `'bottom-right'`

**The `source` parameter accepts multiple formats:**

```
// Relative path (resolved via base_path())
->watermark('resources/images/logo.png', 'bottom-right')

// Web path — as returned by ->url() (resolved via public_path())
$logoUrl = Media::image('resources/images/logo.png')->resize(width: 200)->url();
->watermark($logoUrl, 'bottom-right', 10, 80)

// Absolute filesystem path
->watermark('/var/www/html/storage/watermarks/logo.png', 'center')
```

---

🖼️ Output Methods
-----------------

[](#️-output-methods)

### `->url()` — URL String

[](#-url--url-string)

```
$url = Media::image('resources/images/og.jpg')->resize(width: 1200)->format('jpg')->url();
```

### `->html(string $alt, string $class, ?string $id, array $attributes)` — HTML Tag

[](#-htmlstring-alt-string-class-string-id-array-attributes--html-tag)

Output depends on the active output mode set via `->responsive()` or `->picture()`:

```
// Simple
echo Media::image('logo.png')->resize(width: 200)->html(alt: 'Logo', class: 'h-8');

//  with srcset
echo Media::image('hero.jpg')->resize(width: 800)->responsive('(max-width: 768px) 100vw, 800px')->html(alt: 'Hero');

//  with  elements
echo Media::image('hero.jpg')->resize(width: 800)->picture()->html(alt: 'Hero');
```

### `->responsive(?string $sizes)` — Switch to srcset Mode

[](#-responsivestring-sizes--switch-to-srcset-mode)

```
Media::image('hero.jpg')
    ->resize(width: 800)
    ->responsive('(max-width: 768px) 100vw, 800px')
    ->fetchpriority('high')
    ->html(alt: 'Hero Banner', class: 'w-full');
```

### `->picture(?array $formats, ?string $fallback, string $imgClass, string $sourceClass)` — Switch to `` Mode

[](#-picturearray-formats-string-fallback-string-imgclass-string-sourceclass--switch-to-picture-mode)

```
Media::image('hero.jpg')
    ->resize(width: 1200)
    ->picture(formats: ['avif', 'webp'], fallback: 'jpg', imgClass: 'w-full')
    ->fetchpriority('high')
    ->html(alt: 'Hero', class: 'hero-picture');
```

---

⚙️ Output Modifiers
-------------------

[](#️-output-modifiers)

These can be chained anywhere before `->url()` or `->html()`:

MethodDescription`->format(string $format)`Output format: `webp`, `avif`, `jpg`, `jpeg`, `png``->quality(int $quality)`Quality 1–100 (overrides config)`->loading(string $loading)``'lazy'` or `'eager'``->fetchpriority(string $priority)``'auto'`, `'high'`, `'low'` (high → forces eager)`->noCache()`Skip the manifest cache, always regenerate---

🧩 Blade Components
------------------

[](#-blade-components)

All components use the `media` namespace and map to the `Media::image()` builder.

> Attributes placed directly on the component tag (`wire:key`, `data-*`, `x-*`) are forwarded automatically. Use `:extra-attributes="[...]"` for programmatic attribute arrays.

### `` — Single Optimized Image

[](#x-mediaimg--single-optimized-image)

```

```

**Props:** `src`, `alt`, `width`, `height`, `class`, `format`, `loading`, `fetchpriority`, `id`, `original`, `extra-attributes`

---

### `` — Responsive with srcset

[](#x-mediaresponsive-img--responsive-with-srcset)

```

```

**Additional prop:** `sizes`

---

### `` — Multi-Format with Fallback

[](#x-mediapicture--multi-format-with-fallback)

```

```

**Additional props:** `formats`, `fallback-format`, `img-class`, `source-class`

> The Blade attribute bag (`wire:key`, `x-*`, `@*` etc.) is applied to the outer `` element. Use `extra-attributes` for the inner ``.

---

### `` — URL Only

[](#x-mediaimg-url--url-only)

```

```

**Props:** `src`, `width`, `format`, `original`

---

🖼️ Legacy Blade Helpers
-----------------------

[](#️-legacy-blade-helpers)

The four global helper functions are still available but marked `@deprecated`. They are now thin wrappers around `Media::image()`.

```
{{-- Still works: --}}
{!! img(src: 'resources/images/logo.jpg', alt: 'Logo', width: 200, format: 'webp') !!}
{!! responsive_img(src: 'resources/images/hero.jpg', alt: 'Hero', width: 800) !!}
{!! picture(src: 'resources/images/hero.jpg', alt: 'Hero', width: 800) !!}
{{ img_url(src: 'resources/images/og.jpg', width: 1200, format: 'jpg') }}

{{-- Preferred (v2): --}}
{!! Media::image('resources/images/hero.jpg')->resize(width: 800)->html(alt: 'Hero') !!}
```

---

⚙️ Configuration
----------------

[](#️-configuration)

```
php artisan vendor:publish --tag=media-toolkit-config
```

**`config/media-toolkit.php`:**

```
return [
    // Image processing driver: 'auto' (recommended), 'gd', or 'imagick'
    'driver'     => env('MEDIA_TOOLKIT_DRIVER', 'auto'),

    // Output directory relative to public/
    'output_dir' => env('MEDIA_TOOLKIT_OUTPUT_DIR', 'media/optimized'),

    'image' => [

        // Image quality per format (1–100)
        'quality' => [
            'webp'  => 80,
            'avif'  => 65,
            'jpg'   => 82,
            'jpeg'  => 82,
            'png'   => 85,
        ],

        // Responsive breakpoints
        'responsive' => [
            'size_factors' => [0.5, 0.75, 1.0, 1.5, 2.0], // multipliers of the requested width
            'min_width'    => 100,                           // skip variants narrower than this
        ],

        // Default HTML attribute values and format choices
        'defaults' => [
            'format'          => 'webp',
            'picture_formats' => ['avif', 'webp'],
            'fallback_format' => 'jpg',
            'loading'         => 'lazy',
            'fetchpriority'   => 'auto',
            'sizes'           => '100vw',
        ],

    ],
];
```

Example `.env` overrides:

```
MEDIA_TOOLKIT_DRIVER=imagick
MEDIA_TOOLKIT_OUTPUT_DIR=media/optimized
```

---

📐 Responsive Variants — How It Works
------------------------------------

[](#-responsive-variants--how-it-works)

When you specify `width: 800`, the following variants are generated:

FactorCalculationResult0.5×800 × 0.5**400w**0.75×800 × 0.75**600w**1.0×800 × 1.0**800w**1.5×800 × 1.5**1200w**2.0×800 × 2.0**1600w****Automatic constraints:**

- Variants smaller than `min_width` (default 100px) are skipped
- Variants wider than the original image are skipped (no artificial upscaling)
- If the original width is ≤ 2× the target, the original width is added as an additional variant
- Duplicates are automatically removed

---

⚡ Performance
-------------

[](#-performance)

### Loading Behavior

[](#loading-behavior)

```
{{-- Default: Lazy Loading (below the fold) --}}
{!! Media::image('photo.jpg')->resize(width: 600)->html(alt: 'Photo') !!}
{{-- → loading="lazy" decoding="async" fetchpriority="auto" --}}

{{-- Above the Fold: High Priority --}}
{!! Media::image('hero.jpg')->resize(width: 1200)->fetchpriority('high')->html(alt: 'Hero') !!}
{{-- → loading="eager" decoding="async" fetchpriority="high" --}}
```

> Setting `fetchpriority('high')` automatically forces `loading="eager"`, even if `lazy` was set explicitly.

---

🔧 Artisan Commands
------------------

[](#-artisan-commands)

### Clear Cache

[](#clear-cache)

Deletes all optimized media variants from `public//`:

```
php artisan media:cache-clear
```

```
✓ 42 cache entries deleted.

```

### Warm Cache

[](#warm-cache)

Regenerates any variants whose source file has changed since they were last generated:

```
php artisan media:cache-warm
```

```
Checking cache for outdated media variants...
✓ 3 regenerated, 39 up to date.
⚠ Source file not found: resources/images/deleted-image.jpg

```

Both commands accept `--type=` for future multi-type support (image, video, audio).

### In Your Deployment Pipeline

[](#in-your-deployment-pipeline)

```
php artisan media:cache-clear   # Optional: rebuild everything from scratch
php artisan media:cache-warm    # Recommended: only regenerate what changed
```

---

💾 Caching — How It Works
------------------------

[](#-caching--how-it-works)

Each unique combination of source file, dimensions, format, operations and filters gets its own **cache directory** in `public//` (default: `public/media/optimized/`):

```
public/media/optimized/
├── a1b2c3d4e5f6/          ← Hash of source + options + filter fingerprint
│   ├── manifest.json       ← Metadata + modification timestamp
│   ├── hero-400w.webp
│   ├── hero-600w.webp
│   └── hero-800w.webp
├── f6e5d4c3b2a1/          ← Same image, different filter chain = different cache
│   ├── manifest.json
│   ├── hero-400w.webp     ← With grayscale applied
│   └── hero-800w.webp
└── originals/              ← Unmodified originals (when original: true)
    └── a1b2c3d4-photo.jpg

```

The `manifest.json` stores the **last-modified timestamp** of the source file. On every request:

1. Does the cache directory with manifest exist? → **Yes**: Check timestamp
2. Has the source changed? → **No**: Serve from cache ✓
3. Has the source changed? → **Yes**: Delete old cache, regenerate variants

**You never need to clear the cache manually when replacing images** — changes are detected automatically.

### Concurrent Request Safety

[](#concurrent-request-safety)

Manifest files are written **atomically** using a temp-file-then-rename pattern:

1. Image variants are processed and saved to disk
2. The manifest is written to a temporary file (`manifest.tmp.`)
3. The temp file is **renamed** to `manifest.json` — an atomic operation on POSIX systems (Linux, macOS)

This guarantees that concurrent readers never encounter partially-written JSON, even when multiple requests try to generate the same image simultaneously.

### Memory Check Order — Cache Always Wins

[](#memory-check-order--cache-always-wins)

The memory-bypass check (`on_memory_limit`) happens **after** the cache check, not before:

1. **Cache exists?** → Serve immediately — no processing, no memory check needed
2. **Cache miss** → Check if GD can process the image within the PHP memory limit
3. **Memory too low** → Show placeholder / fallback per `on_memory_limit` setting
4. **Memory ok** → Process, cache, and serve

**Result:** A cached image is **always served from disk** regardless of current PHP memory availability. The `on_memory_limit` fallback only activates when generating a variant for the first time.

---

🛠️ Practical Examples
---------------------

[](#️-practical-examples)

### Hero Banner (Above the Fold)

[](#hero-banner-above-the-fold)

```
{{-- Fluent API --}}
{!! Media::image('resources/images/hero.jpg')
    ->resize(width: 1200)
    ->picture(formats: ['avif', 'webp'], fallback: 'jpg')
    ->fetchpriority('high')
    ->html(alt: 'Welcome', class: 'w-full', attributes: ['id' => 'hero']) !!}

{{-- Blade Component --}}

```

### Logo (Fixed Size, Eager)

[](#logo-fixed-size-eager)

```

```

### Product Gallery with Lightbox

[](#product-gallery-with-lightbox)

```
@foreach ($images as $image)
    {!! Media::image($image->path)
        ->resize(width: 600)
        ->responsive('(max-width: 768px) 100vw, 33vw')
        ->html(
            alt:        $image->caption,
            class:      'gallery-thumb cursor-pointer',
            attributes: [
                'data-lightbox' => 'gallery',
                'data-full'     => Media::image($image->path)->resize(width: 1800)->format('jpg')->url(),
            ],
        ) !!}
@endforeach
```

### Open Graph Meta Tags

[](#open-graph-meta-tags)

```

```

### CSS Background

[](#css-background)

```

    Welcome

```

### Grayscale Thumbnail Grid

[](#grayscale-thumbnail-grid)

```
@foreach ($products as $product)
    {!! Media::image($product->image)
        ->fit(300, 300)
        ->grayscale()
        ->html(alt: $product->name, class: 'product-thumb') !!}
@endforeach
```

### Livewire Repeater

[](#livewire-repeater)

```
{{-- wire:key on  --}}
@foreach ($items as $item)

@endforeach

{{-- wire:key on  (outermost element) --}}
@foreach ($items as $item)

@endforeach
```

---

⚠️ Error Handling
-----------------

[](#️-error-handling)

Error handling behaviour is configurable per error type in `config/media-toolkit.php`:

```
'image' => [
    'errors' => [
        'on_not_found'    => env('MEDIA_ON_NOT_FOUND',    'placeholder'),  // file does not exist
        'on_error'        => env('MEDIA_ON_ERROR',        'placeholder'),  // processing failed
        'on_memory_limit' => env('MEDIA_ON_MEMORY_LIMIT', 'placeholder'),  // GD memory bypass

        // Placeholder label text per error type
        'not_found_text'     => 'Media could not be found.',
        'error_text'         => 'Media could not be displayed!',
        'memory_limit_text'  => 'Media will be displayed shortly.',

        // Placeholder background colour per error type
        'not_found_color'    => '#f87171',   // red-400
        'error_color'        => '#f87171',   // red-400
        'memory_limit_color' => '#9ca3af',   // gray-400
    ],
],
```

### Modes

[](#modes)

Mode`html()` returns`url()` returns`'placeholder'`Inline SVG `` with label text`''` (empty string)`'broken'``` — browser shows broken-image icon`''` (empty string)`'exception'`Throws `MediaBuilderException`Throws `MediaBuilderException``'original'` *(memory-limit only)*Serve the unprocessed source fileURL of source file copyDefault values: `on_not_found=placeholder`, `on_error=placeholder`, `on_memory_limit=placeholder`.

**Override via `.env`:**

```
MEDIA_ON_NOT_FOUND=placeholder        # placeholder | broken | exception
MEDIA_ON_ERROR=placeholder            # placeholder | broken | exception
MEDIA_ON_MEMORY_LIMIT=placeholder     # placeholder | original | broken | exception
```

### Memory-Safe Fallback (GD)

[](#memory-safe-fallback-gd)

When the GD driver detects that processing a large image would exceed the PHP memory limit, the behaviour is controlled by `on_memory_limit` (default: `'placeholder'`).

Setting `MEDIA_ON_MEMORY_LIMIT=original` serves the raw source file unchanged with `data-media-toolkit-status="original-fallback"` and `data-media-toolkit-reason="memory-limit"` attributes on the ``.

---

📋 Logging &amp; Failure Registry
--------------------------------

[](#-logging--failure-registry)

Every error (not found, processing failure, memory bypass) is written to the Laravel application log and recorded in a local failure registry for offline retry.

### Log Configuration

[](#log-configuration)

```
'image' => [
    'logging' => [
        'enabled' => env('MEDIA_LOGGING_ENABLED', true),

        // null = Laravel's default log channel (LOG_CHANNEL in .env)
        // Set to 'single', 'daily', 'stack', etc. to use a dedicated channel
        'channel' => env('MEDIA_LOG_CHANNEL', null),

        'level' => [
            'not_found'    => 'warning',
            'error'        => 'error',
            'memory_limit' => 'notice',
        ],
    ],
],
```

**Override via `.env`:**

```
MEDIA_LOGGING_ENABLED=true
MEDIA_LOG_CHANNEL=daily     # optional dedicated channel
```

### Failure Registry

[](#failure-registry)

Failed images are persisted to `storage/media-toolkit/failures.json` so you can retry them later without re-deploying:

```
{
  "resources/images/hero.jpg": {
    "reason": "memory_limit",
    "count": 3,
    "first_occurred": "2026-02-22T12:00:00+00:00",
    "last_occurred":  "2026-02-22T12:05:00+00:00",
    "params": {
      "display_width": 800,
      "format": "webp",
      "quality": 80,
      "operations_fingerprint": "d41d8cd9...",
      "single_only": true
    }
  }
}
```

### `media:process-pending` Command

[](#mediaprocess-pending-command)

Retry all registered failures with an unlimited memory limit (useful for processing large images that were bypassed at request time due to GD memory constraints):

```
# List all pending failures
php artisan media:process-pending --list

# Attempt offline generation (unlimited memory by default)
php artisan media:process-pending

# Use a custom memory limit
php artisan media:process-pending --memory=512M

# Clear the registry
php artisan media:process-pending --clear
```

> **Note:** The command regenerates the base resize/format variant. Operations (filters, watermarks) cannot be reproduced from the fingerprint alone — a warning is shown for entries with non-trivial operation chains.

---

🔀 GD vs. Imagick
----------------

[](#-gd-vs-imagick)

FeatureGDImagickJPEG✅✅PNG✅✅WebP✅ (if `imagewebp` available)✅AVIF✅ (if `imageavif` available, PHP 8.1+)✅ (if compiled with AVIF)> Imagick is automatically preferred when available and generally offers better quality and performance for large images.

---

🆙 Upgrading from v1
-------------------

[](#-upgrading-from-v1)

**Breaking Changes in v2:**

Areav1v2Blade namespace````Output directory`public/img/optimized/``public/media/optimized/`Config structureflat keysnested under `image.*`Config key `quality.webp``config('media-toolkit.quality.webp')``config('media-toolkit.image.quality.webp')`Config key `responsive.*``config('media-toolkit.responsive.*')``config('media-toolkit.image.responsive.*')`Config key `defaults.*``config('media-toolkit.defaults.*')``config('media-toolkit.image.defaults.*')`Artisan: clear cache`media:img-clear``media:cache-clear`Artisan: warm cache`media:img-warm``media:cache-warm`Error behaviorenv-based (empty string / HTML comment)configurable `placeholder` / `broken` / `exception`**Migration steps:**

1. Update `config/media-toolkit.php` to the new nested structure (or re-publish it):

    ```
    php artisan vendor:publish --tag=media-toolkit-config --force
    ```
2. Clear the old cache directory:

    ```
    php artisan media:cache-clear
    rm -rf public/img/optimized   # remove old directory manually if needed
    ```
3. Update `.gitignore`:

    ```
    # Remove:
    /public/img/optimized/
    # Add:
    /public/media/optimized/
    ```
4. Update Blade templates — replace `` with ``. The helper functions (`img()`, `responsive_img()`, `picture()`, `img_url()`) are still available but now go through the new builder and are marked deprecated.

---

📂 .gitignore
------------

[](#-gitignore)

```
/public/media/optimized/
```

---

🤝 Contributing
--------------

[](#-contributing)

Contributions are welcome! Please fork the repository, create your feature branch and submit a pull request.

```
git clone https://github.com/laraextend/media-toolkit.git
cd media-toolkit
composer install
./vendor/bin/pest
```

---

📄 Changelog
-----------

[](#-changelog)

All notable changes are documented in the [CHANGELOG](CHANGELOG.md).

---

🔒 Security
----------

[](#-security)

### Built-in Input Validation

[](#built-in-input-validation)

Every source path and watermark path is validated before any filesystem access occurs. The following are rejected with a `MediaBuilderException`:

ThreatExampleCheckDirectory traversal`../../etc/passwd``..` in any path segmentNull byte injection`image.jpg\0.php``\x00` in pathLog injection (CRLF)`image.jpg\nFAKE_LOG``\r` / `\n` in pathDisallowed file type`config/database.php`Extension whitelist**Allowed image extensions:** `jpg`, `jpeg`, `png`, `gif`, `webp`, `avif`, `bmp`, `tiff`, `tif`

```
// All of these throw MediaBuilderException:
Media::image('../../etc/passwd')->html();            // traversal
Media::image("logo.jpg\nINJECTED")->html();          // CRLF
Media::image('config/database.php')->html();          // disallowed extension
Media::image('resources/img.svg')->html();            // SVG excluded (scripting risk)

Media::image($src)->watermark('/../../etc/shadow')->html();   // traversal in watermark
Media::image($src)->watermark('http://x.com/../../etc')->html(); // traversal in URL
```

### Watermark Path Confinement

[](#watermark-path-confinement)

Watermark sources are validated against their respective root directories:

- **Relative paths** → must resolve within `base_path()`
- **Web paths** (`/...`) → must resolve within `public_path()`
- **HTTP(S) URLs** → extracted URL path must resolve within `public_path()`

### Memory Safety (GD)

[](#memory-safety-gd)

When the GD driver estimates that processing would exceed the PHP memory limit, the image is never loaded — preventing fatal OOM errors. Image dimensions from EXIF metadata are capped at 65 535 px per axis to prevent integer overflow through adversarially crafted image headers.

### Developer Responsibility

[](#developer-responsibility)

The package validates paths structurally. It does **not** enforce which directories developers may use as image sources. If you accept image paths from user input, ensure the input is validated by your application before passing it to `Media::image()`.

### Reporting Vulnerabilities

[](#reporting-vulnerabilities)

If you discover a security issue, please send an email to  instead of creating a public issue.

---

📜 License
---------

[](#-license)

MIT License. See [LICENSE](LICENSE.md) for details.

---

 Made with ❤️ by [LaraExtend](https://github.com/laraextend)

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance83

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

6

Last Release

85d ago

Major Versions

v0.5.0 → v1.0.02026-02-22

### Community

Maintainers

![](https://www.gravatar.com/avatar/9e2b5350dbcb74d6fd831b8150e27fd15979b6d792249a341b25434b00591c7f?d=identicon)[arashsaffari](/maintainers/arashsaffari)

---

Top Contributors

[![arashsaffari](https://avatars.githubusercontent.com/u/737229?v=4)](https://github.com/arashsaffari "arashsaffari (28 commits)")

---

Tags

laravelimageperformancecompressionaudiovideosvgbladecachemediaresponsivepicturetoolkitoptimizeranimatedinterventionsrcsetWebpavif

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/laraextend-media-toolkit/health.svg)

```
[![Health](https://phpackages.com/badges/laraextend-media-toolkit/health.svg)](https://phpackages.com/packages/laraextend-media-toolkit)
```

###  Alternatives

[blade-ui-kit/blade-icons

A package to easily make use of icons in your Laravel Blade views.

2.5k34.2M309](/packages/blade-ui-kit-blade-icons)[tightenco/jigsaw

Simple static sites with Laravel's Blade.

2.2k438.5k29](/packages/tightenco-jigsaw)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9682.1M97](/packages/roots-acorn)[angus-mcritchie/blade-boost-directive

Lightning-Fast Blade Components with `@boost` Directive

3910.0k](/packages/angus-mcritchie-blade-boost-directive)[somehow-digital/typo3-media-processing

Media Processing

101.1k](/packages/somehow-digital-typo3-media-processing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
