PHP code example of povly / moonshine-intervention-image

1. Go to this page and download the library: Download povly/moonshine-intervention-image library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

povly / moonshine-intervention-image example snippets


// config/moonshine-intervention-image.php
return [
    'default' => [
        'quality' => env('MOONSHINE_INTERVENTION_IMAGE_QUALITY', 85),
        'quality_webp' => env('MOONSHINE_INTERVENTION_IMAGE_QUALITY_WEBP', 80),
        'quality_avif' => env('MOONSHINE_INTERVENTION_IMAGE_QUALITY_AVIF', 65),
        'generate_webp' => env('MOONSHINE_INTERVENTION_IMAGE_WEBP', false),
        'generate_avif' => env('MOONSHINE_INTERVENTION_IMAGE_AVIF', false),
        'strip_metadata' => env('MOONSHINE_INTERVENTION_IMAGE_STRIP_METADATA', false),
        'max_width' => env('MOONSHINE_INTERVENTION_IMAGE_MAX_WIDTH'),
        'max_height' => env('MOONSHINE_INTERVENTION_IMAGE_MAX_HEIGHT'),
        'logging' => env('MOONSHINE_INTERVENTION_IMAGE_LOGGING', false),
    ],
    'png' => [
        'indexed' => env('MOONSHINE_INTERVENTION_IMAGE_PNG_INDEXED', true),
        'colors' => env('MOONSHINE_INTERVENTION_IMAGE_PNG_COLORS', 256),
    ],
    'queue' => [
        'enabled' => env('MOONSHINE_INTERVENTION_IMAGE_QUEUE_ENABLED', false),
        'connection' => env('MOONSHINE_INTERVENTION_IMAGE_QUEUE_CONNECTION'),
        'queue' => env('MOONSHINE_INTERVENTION_IMAGE_QUEUE_NAME', 'images'),
        'delay' => env('MOONSHINE_INTERVENTION_IMAGE_QUEUE_DELAY'),
        'tries' => env('MOONSHINE_INTERVENTION_IMAGE_QUEUE_TRIES', 3),
        'timeout' => env('MOONSHINE_INTERVENTION_IMAGE_QUEUE_TIMEOUT', 120),
    ],
    'presets' => [
        'banner' => [
            'quality' => 85,
            'quality_webp' => 80,
            'quality_avif' => 65,
            'generate_webp' => true,
            'generate_avif' => false,
            'max_width' => 1920,
            'max_height' => 1080,
            'png_indexed' => true,
        ],
    ],
    'watermark' => [
        'enabled' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_ENABLED', false),
        'image' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_IMAGE'),
        'position' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_POSITION', 'bottom-right'),
        'custom_position' => [
            'x' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_CUSTOM_X'),
            'y' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_CUSTOM_Y'),
        ],
        'offset_x' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_OFFSET_X', 10),
        'offset_y' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_OFFSET_Y', 10),
        'opacity' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_OPACITY', 100),
        'width' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_WIDTH'),
        'height' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_HEIGHT'),
    ],
    'watermark_text' => [
        'enabled' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_ENABLED', false),
        'text' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT'),
        'font' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_FONT'),
        'size' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_SIZE', 24),
        'color' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_COLOR', 'ffffff'),
        'position' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_POSITION', 'bottom-right'),
        'custom_position' => [
            'x' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_CUSTOM_X'),
            'y' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_CUSTOM_Y'),
        ],
        'offset_x' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_OFFSET_X', 10),
        'offset_y' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_OFFSET_Y', 10),
        'stroke_color' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_STROKE_COLOR'),
        'stroke_width' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_STROKE_WIDTH', 2),
        'angle' => env('MOONSHINE_INTERVENTION_IMAGE_WATERMARK_TEXT_ANGLE'),
    ],

    'optimize' => [
        'disk' => env('MOONSHINE_INTERVENTION_IMAGE_OPTIMIZE_DISK', 'public'),
        'exclude_paths' => [],
    ],
];

use Povly\MoonshineInterventionImage\Fields\InterventionImage;
// Basic usage
InterventionImage::make('Image', 'image')
// Using preset from config
InterventionImage::make('Image', 'image')
    ->preset('banner')
// With WebP generation
InterventionImage::make('Image', 'image')
    ->generateWebp()
// With AVIF generation
InterventionImage::make('Image', 'image')
    ->generateAvif()
// With both formats
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->generateAvif()
// With quality control (1-100)
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->generateAvif()
    ->quality(85)           // Original image
    ->qualityWebp(80)       // WebP version
    ->qualityAvif(65)       // AVIF version
// PNG optimization with indexed colors (reduces file size significantly)
InterventionImage::make('Image', 'image')
    ->pngIndexed()                    // Uses default 256 colors
    ->pngIndexed(true, 128)           // Custom color count (2-256)
// Strip metadata (EXIF, IPTC, etc.)
InterventionImage::make('Image', 'image')
    ->stripMetadata()
// Resize images (keeps aspect ratio)
InterventionImage::make('Image', 'image')
    ->maxDimensions(1920, 1080)
// Multiple images
InterventionImage::make('Gallery', 'gallery')
    ->multiple()
    ->generateWebp()
    ->generateAvif()
// Enable logging (disabled by default)
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->logging()
// Queue processing (for large images)
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->queue()
// Queue with custom settings
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->queue(true, 'redis', 'images', 10) // enabled, connection, queue, delay(seconds)

use Povly\MoonshineInterventionImage\Enums\WatermarkPosition;
// Basic image watermark
InterventionImage::make('Image', 'image')
    ->watermarkImage('/path/to/watermark.png')
// With position
InterventionImage::make('Image', 'image')
    ->watermarkImage(
        '/path/to/watermark.png',
        WatermarkPosition::TopLeft
    )
// Full control
InterventionImage::make('Image', 'image')
    ->watermarkImage(
        path: '/path/to/watermark.png',
        position: WatermarkPosition::BottomRight,
        offsetX: 20,         // Offset from position (pixels)
        offsetY: 20,
        opacity: 80,         // 0-100 (default: 100)
        width: 200,          // Resize watermark width (optional)
        height: null,        // Resize watermark height (optional)
        customX: null,       // Custom X position (when position is 'custom')
        customY: null        // Custom Y position (when position is 'custom')
    )
// Custom position
InterventionImage::make('Image', 'image')
    ->watermarkImage(
        path: '/path/to/watermark.png',
        position: WatermarkPosition::Custom,
        customX: 150,
        customY: 100
    )
// Using string position
InterventionImage::make('Image', 'image')
    ->watermarkImage('/path/to/watermark.png', 'top-left')

use Povly\MoonshineInterventionImage\Enums\WatermarkPosition;
// Basic text watermark
InterventionImage::make('Image', 'image')
    ->watermarkText('© My Site 2025')
// With position
InterventionImage::make('Image', 'image')
    ->watermarkText(
        '© My Site 2025',
        WatermarkPosition::BottomRight
    )
// Full control
InterventionImage::make('Image', 'image')
    ->watermarkText(
        text: '© My Site 2025',
        position: WatermarkPosition::BottomRight,
        offsetX: 20,         // Offset from position (pixels)
        offsetY: 20,
        size: 32,            // Font size (default: 24)
        color: 'ffffff',     // Hex color without # (default: 'ffffff')
        customX: null,       // Custom X position (when position is 'custom')
        customY: null        // Custom Y position (when position is 'custom')
    )
// With custom font
InterventionImage::make('Image', 'image')
    ->watermarkText('© My Site', WatermarkPosition::BottomRight)
    ->watermarkTextFont('/path/to/font.ttf')
// With stroke (text outline)
InterventionImage::make('Image', 'image')
    ->watermarkText('© My Site', WatermarkPosition::BottomRight)
    ->watermarkTextStroke('000000', 2)  // color, width
// With rotation
InterventionImage::make('Image', 'image')
    ->watermarkText('© My Site', WatermarkPosition::Center)
    ->watermarkTextAngle(45)  // degrees
// Using string position
InterventionImage::make('Image', 'image')
    ->watermarkText('© My Site', 'top-left')

// Disable watermark for specific field (even if enabled in config)
InterventionImage::make('Image', 'image')
    ->disableWatermark()

// config/moonshine-intervention-image.php
'watermark' => [
    'enabled' => true,
    'image' => public_path('images/watermark.png'),
    'position' => 'bottom-right',
    'offset_x' => 20,
    'offset_y' => 20,
    'opacity' => 80,
    'width' => 200,
    'height' => null,
],
'watermark_text' => [
    'enabled' => true,
    'text' => '© My Site 2025',
    'font' => public_path('fonts/arial.ttf'),
    'size' => 24,
    'color' => 'ffffff',
    'position' => 'bottom-right',
    'offset_x' => 20,
    'offset_y' => 20,
    'stroke_color' => '000000',
    'stroke_width' => 2,
    'angle' => null,
],

// Move 50px left and 30px up from bottom-right corner
InterventionImage::make('Image', 'image')
    ->watermarkText(
        '© My Site',
        WatermarkPosition::BottomRight,
        offsetX: 50,
        offsetY: 30
    )

// config/moonshine-intervention-image.php
'queue' => [
    'enabled' => true,
    'connection' => null,     // null = default connection
    'queue' => 'images',
    'delay' => null,          // seconds or Carbon instance
    'tries' => 3,
    'timeout' => 120,
],

// Enable queue with default config settings
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->queue()
// Override queue settings per field
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->queue(true, 'redis', 'high-priority', 5)
// Set only delay
InterventionImage::make('Image', 'image')
    ->generateWebp()
    ->queue()
    ->queueDelay(now()->addMinutes(5))

// config/moonshine-intervention-image.php
'optimize' => [
    'disk' => env('MOONSHINE_INTERVENTION_IMAGE_OPTIMIZE_DISK', 'public'),
    'exclude_paths' => [],  // Paths to skip (relative to disk root)
],

'exclude_paths' => [
    'assets/icons',
    'avatars/raw',
],

InterventionImage::make('Image', 'image')
    ->pngIndexed()           // 256 colors (default)
    ->pngIndexed(true, 128)  // 128 colors - smaller file
    ->pngIndexed(true, 64)   // 64 colors - even smaller

InterventionImage::make('Image', 'image')
    ->generateAvif()
    ->qualityAvif(65)  // Lower values = smaller files, lower quality

// Via field method
InterventionImage::make('Image', 'image')
    ->logging()
// Via config
'default' => [
    'logging' => true,
],


namespace App\MoonShine\Layouts;
use MoonShine\AssetManager\InlineJs;
use MoonShine\Laravel\Layouts\AppLayout;
final class MoonShineLayout extends AppLayout
{
    protected function assets(): array
    {
        return [
            ...parent::assets(),
            InlineJs::make(<<<'JS'
                window.removeImage = function(event, name) {
                    let button = event.currentTarget;
                    let imageIndex = button.closest('.dropzone-item')?.dataset.id;
                    if (imageIndex === undefined) {
                        return;
                    }
                    fetch(`${button.dataset.asyncUrl}&imageIndex=${imageIndex}&name=${name}`)
                        .then(() => button.closest('.x-removeable')?.remove());
                };
            JS),
        ];
    }
}


namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Banner extends Model
{
    protected $fillable = [
        'title',
        'image',
        'images',
    ];
    protected function casts(): array
    {
        return [
            'images' => 'array',
        ];
    }
}


namespace App\MoonShine\Resources\Banner;
use App\Models\Banner;
use App\MoonShine\Resources\Banner\Pages\BannerIndexPage;
use App\MoonShine\Resources\Banner\Pages\BannerFormPage;
use App\MoonShine\Resources\Banner\Pages\BannerDetailPage;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use MoonShine\Contracts\Core\DependencyInjection\CrudRequestContract;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\Support\Attributes\AsyncMethod;
class BannerResource extends ModelResource
{
    protected string $model = Banner::class;
    protected string $title = 'Banners';
    protected function pages(): array
    {
        return [
            BannerIndexPage::class,
            BannerFormPage::class,
            BannerDetailPage::class,
        ];
    }
    #[AsyncMethod]
    public function removeImageData(CrudRequestContract $request): void
    {
        $item = $request->getResource()?->getItem();
        $imageIndex = $request->integer('imageIndex');
        $name = $request->string('name');
        if (is_null($item) || $imageIndex < 0) {
            return;
        }
        $images = $item->{$name} ?? [];
        if (isset($images[$imageIndex])) {
            $this->deleteImageWithConversions($images[$imageIndex]);
            Arr::forget($images, $imageIndex);
            $item->{$name} = array_values($images);
            $item->save();
        }
    }
    protected function deleteImageWithConversions(string $filePath, string $disk = 'public'): void
    {
        if (! Storage::disk($disk)->exists($filePath)) {
            return;
        }
        Storage::disk($disk)->delete($filePath);
        $info = pathinfo($filePath);
        $basePath = $info['dirname'].'/'.$info['filename'];
        foreach (['webp', 'avif'] as $format) {
            $conversionPath = $basePath.'.'.$format;
            if (Storage::disk($disk)->exists($conversionPath)) {
                Storage::disk($disk)->delete($conversionPath);
            }
        }
    }
}


namespace App\MoonShine\Resources\Banner\Pages;
use MoonShine\Laravel\Pages\Crud\FormPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use MoonShine\UI\Components\Layout\Box;
use Povly\MoonshineInterventionImage\Fields\InterventionImage;
class BannerFormPage extends FormPage
{
    protected function fields(): iterable
    {
        return [
            Box::make([
                ID::make(),
                Text::make('Title', 'title')
                    ->1080)
                    ->removable(),
                    
                // Multiple images with AJAX deletion
                InterventionImage::make('Gallery', 'images')
                    ->dir('banners/gallery')
                    ->multiple()
                    ->removable(attributes: $this->getRemovableImageAttributes('images'))
                    ->allowedExtensions(['jpg', 'jpeg', 'png', 'webp'])
                    ->generateWebp()
                    ->generateAvif()
                    ->quality(85)
                    ->qualityWebp(80)
                    ->qualityAvif(65)
                    ->pngIndexed()
                    ->maxDimensions(1920, 1080),
            ]),
        ];
    }
    public function getRemovableImageAttributes(string $name): array
    {
        return [
            'data-async-url' => $this->getResource()
                ? $this->getRouter()->getEndpoints()->method(
                    'removeImageData',
                    params: ['resourceItem' => $this->getResource()->getItemID()]
                )
                : null,
            '@click.prevent' => "removeImage(\$event, '{$name}')",
        ];
    }
}


namespace App\MoonShine\Layouts;
use MoonShine\AssetManager\InlineJs;
use MoonShine\Laravel\Layouts\AppLayout;
final class MoonShineLayout extends AppLayout
{
    protected function assets(): array
    {
        return [
            ...parent::assets(),
            InlineJs::make(<<<'JS'
                window.removeImage = function(event, name) {
                    let button = event.currentTarget;
                    let imageIndex = button.closest('.dropzone-item')?.dataset.id;
                    if (imageIndex === undefined) {
                        return;
                    }
                    fetch(`${button.dataset.asyncUrl}&imageIndex=${imageIndex}&name=${name}`)
                        .then(() => button.closest('.x-removeable')?.remove());
                };
                window.removeLayoutImage = function(event, name) {
                    let button = event.currentTarget;
                    let accordion = button.closest('.accordion');
                    let layoutIndex = parseInt(accordion.querySelector('[data-r-index]')?.dataset.rIndex ?? 0);
                    let imageIndex = button.closest('.dropzone-item')?.dataset.id ?? 0;
                    
                    fetch(`${button.dataset.asyncUrl}&imageIndex=${imageIndex}&layoutIndex=${layoutIndex}&name=${name}`)
                        .then(() => button.closest('.x-removeable')?.remove());
                };
            JS),
        ];
    }
}


namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use MoonShine\Layouts\Casts\LayoutsCast;
class Banner extends Model
{
    protected $fillable = [
        'title',
        'image',
        'images',
        'content',
    ];
    protected function casts(): array
    {
        return [
            'images' => 'array',
            'content' => LayoutsCast::class,
        ];
    }
}

Schema::table('banners', function (Blueprint $table) {
    $table->json('content')->nullable();
});


namespace App\MoonShine\Resources\Banner;
use App\Models\Banner;
use App\MoonShine\Resources\Banner\Pages\BannerIndexPage;
use App\MoonShine\Resources\Banner\Pages\BannerFormPage;
use App\MoonShine\Resources\Banner\Pages\BannerDetailPage;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use MoonShine\Contracts\Core\DependencyInjection\CrudRequestContract;
use MoonShine\Layouts\Casts\LayoutItem;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\Support\Attributes\AsyncMethod;
class BannerResource extends ModelResource
{
    protected string $model = Banner::class;
    protected string $title = 'Banners';
    protected function pages(): array
    {
        return [
            BannerIndexPage::class,
            BannerFormPage::class,
            BannerDetailPage::class,
        ];
    }
    #[AsyncMethod]
    public function removeImageData(CrudRequestContract $request): void
    {
        $item = $request->getResource()?->getItem();
        $imageIndex = $request->integer('imageIndex');
        $name = $request->string('name');
        if (is_null($item) || $imageIndex < 0) {
            return;
        }
        $images = $item->{$name} ?? [];
        if (isset($images[$imageIndex])) {
            $this->deleteImageWithConversions($images[$imageIndex]);
            Arr::forget($images, $imageIndex);
            $item->{$name} = array_values($images);
            $item->save();
        }
    }
    #[AsyncMethod]
    public function removeLayoutImageData(CrudRequestContract $request): void
    {
        $item = $request->getResource()?->getItem();
        $imageIndex = $request->integer('imageIndex');
        $layoutIndex = $request->integer('layoutIndex');
        $name = $request->string('name');
        if (is_null($item) || $imageIndex < 0 || $layoutIndex < 0) {
            return;
        }
        $content = $item->content;
        if (! isset($content[$layoutIndex])) {
            return;
        }
        $values = $content[$layoutIndex] instanceof LayoutItem
            ? $content[$layoutIndex]->getValues()
            : $content[$layoutIndex]['values'];
        $images = data_get($values, $name);
        if (is_array($images) && isset($images[$imageIndex])) {
            $this->deleteImageWithConversions($images[$imageIndex]);
            Arr::forget($images, $imageIndex);
            data_set($values, $name, array_values($images));
        } elseif (is_string($images)) {
            $this->deleteImageWithConversions($images);
            data_set($values, $name, null);
        }
        if ($content[$layoutIndex] instanceof LayoutItem) {
            $content[$layoutIndex] = new LayoutItem(
                $content[$layoutIndex]->getName(),
                $content[$layoutIndex]->getKey(),
                $values
            );
        } else {
            $content[$layoutIndex]['values'] = $values;
        }
        $item->content = $content;
        $item->save();
    }
    protected function deleteImageWithConversions(string $filePath, string $disk = 'public'): void
    {
        if (! Storage::disk($disk)->exists($filePath)) {
            return;
        }
        Storage::disk($disk)->delete($filePath);
        $info = pathinfo($filePath);
        $basePath = $info['dirname'].'/'.$info['filename'];
        foreach (['webp', 'avif'] as $format) {
            $conversionPath = $basePath.'.'.$format;
            if (Storage::disk($disk)->exists($conversionPath)) {
                Storage::disk($disk)->delete($conversionPath);
            }
        }
    }
}


namespace App\MoonShine\Resources\Banner\Pages;
use MoonShine\Laravel\Pages\Crud\FormPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use MoonShine\UI\Components\Layout\Box;
use MoonShine\Layouts\Fields\Layouts;
use Povly\MoonshineInterventionImage\Fields\InterventionImage;
class BannerFormPage extends FormPage
{
    protected function fields(): iterable
    {
        return [
            Box::make([
                ID::make(),
                Text::make('Title', 'title')
                    ->      ->removable(),
                    
                // Multiple images (outside layouts)
                InterventionImage::make('Gallery', 'images')
                    ->dir('banners/gallery')
                    ->multiple()
                    ->removable(attributes: $this->getRemovableImageAttributes('images'))
                    ->allowedExtensions(['jpg', 'jpeg', 'png', 'webp'])
                    ->generateWebp()
                    ->generateAvif()
                    ->quality(85)
                    ->qualityWebp(80)
                    ->qualityAvif(65)
                    ->pngIndexed()
                    ->maxDimensions(1920, 1080),
                    
                // Layouts field with images inside
                Layouts::make('Content', 'content')
                    ->addLayout('Image Block', 'image_block', [
                        Text::make('Title', 'title'),
                        InterventionImage::make('Image', 'image')
                            ->dir('banners/blocks')
                            ->allowedExtensions(['jpg', 'jpeg', 'png', 'webp'])
                            ->generateWebp()
                            ->generateAvif()
                            ->quality(85)
                            ->qualityWebp(80)
                            ->qualityAvif(65)
                            ->pngIndexed()
                            ->maxDimensions(1920, 1080)
                            ->removable(attributes: $this->getRemovableLayoutImageAttributes('image')),
                    ])
                    ->addLayout('Gallery Block', 'gallery', [
                        Text::make('Title', 'title'),
                        InterventionImage::make('Images', 'images')
                            ->dir('banners/gallery-blocks')
                            ->multiple()
                            ->allowedExtensions(['jpg', 'jpeg', 'png', 'webp'])
                            ->generateWebp()
                            ->generateAvif()
                            ->quality(85)
                            ->qualityWebp(80)
                            ->qualityAvif(65)
                            ->pngIndexed()
                            ->maxDimensions(1920, 1080)
                            ->removable(attributes: $this->getRemovableLayoutImageAttributes('images')),
                    ]),
            ]),
        ];
    }
    public function getRemovableImageAttributes(string $name): array
    {
        return [
            'data-async-url' => $this->getResource()
                ? $this->getRouter()->getEndpoints()->method(
                    'removeImageData',
                    params: ['resourceItem' => $this->getResource()->getItemID()]
                )
                : null,
            '@click.prevent' => "removeImage(\$event, '{$name}')",
        ];
    }
    public function getRemovableLayoutImageAttributes(string $name): array
    {
        return [
            'data-async-url' => $this->getResource()
                ? $this->getRouter()->getEndpoints()->method(
                    'removeLayoutImageData',
                    params: ['resourceItem' => $this->getResource()->getItemID()]
                )
                : null,
            '@click.prevent' => "removeLayoutImage(\$event, '{$name}')",
        ];
    }
}
bash
php artisan vendor:publish --tag=moonshine-intervention-image-config
bash
# Force queue processing
php artisan moonshine:image:optimize --queue

# Force synchronous (direct) processing
php artisan moonshine:image:optimize --sync
bash
php artisan moonshine:image:optimize --preset=gallery
bash
php artisan moonshine:image:optimize banners/ uploads/ \
    --disk=public \
    --preset=gallery \
    --queue \
    --quality=80 \
    --strip-metadata