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.
Installs: 233
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/laraextend/media-toolkit
Requires
- php: ^8.2
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/filesystem: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- intervention/image: ^3.0
Requires (Dev)
- orchestra/testbench: ^8.5|^9.0|^10.0
- pestphp/pest: ^3.0
- phpunit/phpunit: ^10.1|^11.0
README
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 <img> or <picture> 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
- 🔗 Fluent Builder API —
Media::image($path)->resize(800)->grayscale()->html(alt: 'Hero') - 🧩 Four Blade Components —
<x-media::img>,<x-media::responsive-img>,<x-media::picture>,<x-media::img-url> - 🖼️ Four Blade Helpers —
img(),responsive_img(),picture()andimg_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-clearandmedia:cache-warmwith optional--type=flag - ⚙️ Configurable — Publish
config/media-toolkit.phpto 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 & Imagick — Automatic driver detection
- ⚡ Livewire & Alpine.js Ready — Blade components forward
wire:*,x-*anddata-*attributes automatically
📋 Requirements
- PHP >= 8.2
- Laravel >= 10.x
- Intervention Image >= 3.0 (
intervention/image) - GD or Imagick PHP extension
- Optional: AVIF support in GD (
imageavif) or Imagick
🚀 Installation
composer require laraextend/media-toolkit
The ServiceProvider and
MediaFacade alias are registered automatically via Laravel's Auto-Discovery.
Optional: Publish Configuration
php artisan vendor:publish --tag=media-toolkit-config
🔗 Fluent API — Media::image()
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 <picture> 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
resize(?int $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
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
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
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 aMediaBuilderException.
original() — No Processing
Serve the original file without any transformation or optimization.
Media::image('resources/images/photo.jpg')->original()->url();
original()locks the chain — callingresize(),format(),quality(), filters orwatermark()afterwards throws aMediaBuilderException.
🎨 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');
| Method | Description |
|---|---|
->grayscale() |
Convert to black & 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
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
->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
Output depends on the active output mode set via ->responsive() or ->picture():
// Simple <img> echo Media::image('logo.png')->resize(width: 200)->html(alt: 'Logo', class: 'h-8'); // <img> with srcset echo Media::image('hero.jpg')->resize(width: 800)->responsive('(max-width: 768px) 100vw, 800px')->html(alt: 'Hero'); // <picture> with <source> elements echo Media::image('hero.jpg')->resize(width: 800)->picture()->html(alt: 'Hero');
->responsive(?string $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 <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
These can be chained anywhere before ->url() or ->html():
| Method | Description |
|---|---|
->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
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.
<x-media::img> — Single Optimized Image
<x-media::img src="resources/images/logo.png" alt="Company Logo" :width="200" format="webp" loading="eager" />
Props: src, alt, width, height, class, format, loading, fetchpriority, id, original, extra-attributes
<x-media::responsive-img> — Responsive with srcset
<x-media::responsive-img src="resources/images/hero.jpg" alt="Hero Banner" :width="800" fetchpriority="high" sizes="(max-width: 768px) 100vw, 800px" />
Additional prop: sizes
<x-media::picture> — Multi-Format with Fallback
<x-media::picture src="resources/images/hero.jpg" alt="Hero Banner" :width="800" :formats="['avif', 'webp']" fallback-format="jpg" fetchpriority="high" sizes="(max-width: 768px) 100vw, 800px" class="hero-picture" img-class="hero-img" />
Additional props: formats, fallback-format, img-class, source-class
The Blade attribute bag (
wire:key,x-*,@*etc.) is applied to the outer<picture>element. Useextra-attributesfor the inner<img>.
<x-media::img-url> — URL Only
<div style="background-image: url('<x-media::img-url src="resources/images/bg.jpg" :width="1920" />')">
Props: src, width, format, original
🖼️ 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
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
When you specify width: 800, the following variants are generated:
| Factor | Calculation | Result |
|---|---|---|
| 0.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
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 forcesloading="eager", even iflazywas set explicitly.
🔧 Artisan Commands
Clear Cache
Deletes all optimized media variants from public/<output_dir>/:
php artisan media:cache-clear
✓ 42 cache entries deleted.
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
php artisan media:cache-clear # Optional: rebuild everything from scratch php artisan media:cache-warm # Recommended: only regenerate what changed
💾 Caching — How It Works
Each unique combination of source file, dimensions, format, operations and filters gets its own cache directory in public/<output_dir>/ (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:
- Does the cache directory with manifest exist? → Yes: Check timestamp
- Has the source changed? → No: Serve from cache ✓
- 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
Manifest files are written atomically using a temp-file-then-rename pattern:
- Image variants are processed and saved to disk
- The manifest is written to a temporary file (
manifest.tmp.<pid>) - 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
The memory-bypass check (on_memory_limit) happens after the cache check, not before:
- Cache exists? → Serve immediately — no processing, no memory check needed
- Cache miss → Check if GD can process the image within the PHP memory limit
- Memory too low → Show placeholder / fallback per
on_memory_limitsetting - 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
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 --}} <x-media::picture src="resources/images/hero.jpg" alt="Welcome" :width="1200" :formats="['avif', 'webp']" fallback-format="jpg" fetchpriority="high" class="w-full" img-class="w-full h-auto object-cover" />
Logo (Fixed Size, Eager)
<x-media::img src="resources/images/logo.png" alt="Company Logo" :width="180" loading="eager" />
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
<meta property="og:image" content="{{ url(Media::image('resources/images/og.jpg')->resize(width: 1200)->format('jpg')->url()) }}">
CSS Background
<section style="background-image: url('{{ Media::image('resources/images/bg.jpg')->resize(width: 1920)->url() }}')"> <h1>Welcome</h1> </section>
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
{{-- wire:key on <img> --}} @foreach ($items as $item) <x-media::img :src="$item->image" :alt="$item->name" :width="300" wire:key="item-{{ $item->id }}" class="rounded" /> @endforeach {{-- wire:key on <picture> (outermost element) --}} @foreach ($items as $item) <x-media::picture :src="$item->image" :alt="$item->name" :width="300" wire:key="item-{{ $item->id }}" /> @endforeach
⚠️ 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
| Mode | html() returns |
url() returns |
|---|---|---|
'placeholder' |
Inline SVG <img> with label text |
'' (empty string) |
'broken' |
<img src="original-path"> — browser shows broken-image icon |
'' (empty string) |
'exception' |
Throws MediaBuilderException |
Throws MediaBuilderException |
'original' (memory-limit only) |
Serve the unprocessed source file | URL of source file copy |
Default 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)
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 <img>.
📋 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
'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
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
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
| Feature | GD | Imagick |
|---|---|---|
| JPEG | ✅ | ✅ |
| 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
Breaking Changes in v2:
| Area | v1 | v2 |
|---|---|---|
| Blade namespace | <x-laraextend::img> |
<x-media::img> |
| Output directory | public/img/optimized/ |
public/media/optimized/ |
| Config structure | flat keys | nested 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 behavior | env-based (empty string / HTML comment) | configurable placeholder / broken / exception |
Migration steps:
-
Update
config/media-toolkit.phpto the new nested structure (or re-publish it):php artisan vendor:publish --tag=media-toolkit-config --force
-
Clear the old cache directory:
php artisan media:cache-clear rm -rf public/img/optimized # remove old directory manually if needed -
Update
.gitignore:# Remove: /public/img/optimized/ # Add: /public/media/optimized/
-
Update Blade templates — replace
<x-laraextend::*>with<x-media::*>. The helper functions (img(),responsive_img(),picture(),img_url()) are still available but now go through the new builder and are marked deprecated.
📂 .gitignore
/public/media/optimized/
🤝 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
All notable changes are documented in the CHANGELOG.
🔒 Security
Built-in Input Validation
Every source path and watermark path is validated before any filesystem access occurs. The following are rejected with a MediaBuilderException:
| Threat | Example | Check |
|---|---|---|
| Directory traversal | ../../etc/passwd |
.. in any path segment |
| Null byte injection | image.jpg\0.php |
\x00 in path |
| Log injection (CRLF) | image.jpg\nFAKE_LOG |
\r / \n in path |
| Disallowed 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 sources are validated against their respective root directories:
- Relative paths → must resolve within
base_path() - Web paths (
/...) → must resolve withinpublic_path() - HTTP(S) URLs → extracted URL path must resolve within
public_path()
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
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
If you discover a security issue, please send an email to security@laraextend.dev instead of creating a public issue.
📜 License
MIT License. See LICENSE for details.
Made with ❤️ by LaraExtend