Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Advanced Image Field Laravel Package

ctessier/nova-advanced-image-field

A Laravel Nova field that extends the default image field with cropping, resizing, and multi-image support. Crop images to predefined ratios, generate multiple versions, and manage responsive images directly from your Nova admin panel.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the constraint before you write a line of field config.

composer require ctessier/nova-advanced-image-field

The service provider is auto-discovered; there is nothing to register. Pin the minor version — this is a single-maintainer package whose last tag is v2.1.0 (Aug 2023), and Nova majors move faster than the package does:

"ctessier/nova-advanced-image-field": "2.1.*"

Then immediately check the declared Nova constraint against what you actually run. This is the single most common reason the field "doesn't work":

composer show ctessier/nova-advanced-image-field --all
composer show laravel/nova

Swap the field, not the column. AdvancedImage extends Nova's stock Image, so it's a drop-in replacement inside fields(). No migration, no model cast, no accessor change:

use Ctessier\NovaAdvancedImageField\AdvancedImage;

// before
Image::make('Cover', 'cover')->disk('public')->path('covers'),

// after
AdvancedImage::make('Cover', 'cover')
    ->disk('public')
    ->path('covers')
    ->croppable()
    ->aspectRatio(16 / 9),

Everything you already chain — disk, path, storeAs, prunable, rules, nullable, hideFromIndex — still works, because it's inherited from Laravel\Nova\Fields\Image.

Where to look first, in order:

  1. vendor/ctessier/nova-advanced-image-field/src/AdvancedImage.php. It's short. It is the definitive API surface for your installed version — more reliable than the README, which drifts.
  2. The package's Vue component. That's where crop coordinates and rotation are computed client-side and where you'll learn exactly what gets posted alongside the file.
  3. composer show --all (above). Do this before building anything on top.

First use case: one hero image on a low-traffic resource. Chain ->croppable()->aspectRatio(16/9), upload something, and verify four things: the file exists on disk, the stored MIME type, the URL Nova returns, and how it renders in the browser. That end-to-end pass surfaces environment problems (driver, disk, limits) that reading the README never will — cropping is client-side, but the resize/encode step runs server-side on the upload request.

Implementation Patterns

The standard content-editor chain. This is what most real fields end up looking like:

AdvancedImage::make('Hero', 'hero')
    ->disk(config('media.disk'))
    ->path('heroes')
    ->croppable()
    ->aspectRatio(16 / 9)
    ->resize(1920, 1080)
    ->quality(80)
    ->rules('required', 'image', 'max:4096')
    ->prunable(),

The distinction that matters: aspectRatio() constrains the editor (the crop box users can drag), resize() bounds the output (the pixels you actually store). Set both. An unconstrained crop from a 4000px phone photo is how you end up with 8MB hero images on a marketing page.

Format conversion (v2.1.0+). The release adds a method for specifying an output format, so emitting WebP becomes field configuration instead of a bespoke field class. Check the exact method name in src/AdvancedImage.php on your installed version, then:

AdvancedImage::make('Hero', 'hero')
    ->croppable()
    ->aspectRatio(16 / 9)
    ->resize(1920, 1080)
    ->quality(80)
    ->format('webp') // confirm the setter name in your installed source
    ->rules('required', 'image', 'max:4096'),

Pilot on one resource first. The value is deleting a backlog item; the risk is that this field now owns output correctness on that column. And pick one owner for derived assets — if you also run a queued intervention/image job over the same path, two systems will fight over filenames.

Stop repeating the chain after the second resource. A small factory is cheaper than a copy-paste and doubles as your escape hatch if a Nova upgrade breaks the field:

final class Fields
{
    public static function hero(string $name = 'Hero', string $attribute = 'hero'): AdvancedImage
    {
        return AdvancedImage::make($name, $attribute)
            ->disk(config('media.disk'))
            ->path('heroes')
            ->croppable()
            ->aspectRatio(16 / 9)
            ->resize(1920, 1080)
            ->quality(80)
            ->prunable();
    }
}

If the package goes stale, you edit one file instead of forty resource classes — or swap the return type back to Image and lose only the crop UI.

Filenames and disks. Use ->storeAs() when you need deterministic paths (per-tenant prefixes, per-record directories) instead of Nova's hash naming:

->storeAs(fn (Request $request) => $request->user()->tenant_id.'/'.Str::uuid().'.webp')

Keep the disk in config/media.php, never hardcoded — you want local in tests, S3 in staging, and probably a CDN-backed disk in production. And confirm non-local disks return a publicly resolvable URL in your setup before rollout; temporary vs. public URLs is the classic failure here.

Galleries and multi-image. The package advertises multiple-image support, but multi-image has historically been the roughest edge in Nova image packages, and Nova's native Image has no array mode to inherit. Before you build a gallery on it, wire up a throwaway resource and confirm the actual API on your Nova version. If it feels fragile, the boring fallback — one AdvancedImage per column, or a JSON column plus a small repeatable field — is more maintainable than a clever gallery that breaks on the next Nova minor.

Testing the contract, not the crop math. Don't try to assert pixel coordinates. Assert what your application depends on:

Storage::fake('public');

// ... update through Nova or invoke the field's fill logic

Storage::disk('public')->assertExists($model->hero);
$this->assertSame('image/webp', Storage::disk('public')->mimeType($model->hero));

That gives you a regression net for the two things that actually break in production: the disk and the encoder.

Gotchas and Tips

The encoder is the #1 real-world failure. Format conversion requires an image driver that can encode the target. Lean container images routinely ship GD without WebP, or no Imagick at all. Verify on the runtime image, not your laptop:

php -r 'var_dump(gd_info());'   # look for "WebP Support => true"
php -m | grep -i imagick

The dangerous behavior is a silent fallback — a missing encoder producing an unconverted file your code believes is WebP. Test it explicitly: request a format your driver can't produce and confirm you get a hard error rather than a quiet JPEG.

Conversion runs synchronously on the upload request. Resize and re-encode happen while the editor watches a spinner. Large sources hit memory_limit and max_execution_time long before they hit anything in the package. Two mitigations: cap upload size with max: rules, or move transformation to a queue and let the field handle cropping only. Decide this before broad rollout, or "upload failed on a large image" becomes your most common support ticket.

Extension and MIME drift breaks downstream consumers. The moment one field starts emitting .webp, anything assuming .jpg is a latent bug. Audit Blade string manipulation on filenames, API resources, sitemaps, transactional email templates, PDF exports, and any external system ingesting image URLs. This audit is independent of the package and can run in parallel with your environment work — front-load it.

Transparency and JPEG don't mix. Converting a PNG with alpha to JPEG yields black or white boxes where the transparency was. If editors upload logos or transparent PNGs, keep the field on PNG/WebP or constrain accepted types in rules().

Format changes are not retroactive. Conversion happens at upload time. Existing records keep their old files and old extensions; changing the field's format does not reprocess them. If a CDN cached the old asset at the same path, you get silent format drift. Treat backfill as its own workstream with its own rollback — never fold it into the field rollout.

Dimension validation can fight your resize. A dimensions:min_width=1920 rule validates the uploaded file, not the resized output. Use min-dimension rules to reject upscaled garbage, and let resize() handle the maximum.

The field cannot save you from bad source material. It won't stop an editor uploading a 300px image and cropping it to a 1920px hero. That's a content/validation problem — dimensions: rules are your only guardrail.

Crop coordinates are client-side. Treat the posted crop values as untrusted input and keep server-side validation (image, max, dimensions) in place. Never assume the browser sent a sane crop box.

Nova upgrades are the breakage vector. This is a Nova package, not a Laravel one — expect to revisit it on every Nova major. The blast radius is small because it only ever appears inside fields() arrays; reverting to Image::make() is a one-line diff per resource. Keep that cheap: don't subclass deeply, and avoid overriding fillUsing/resolveUsing, or you'll collide with future upstream behavior.

Debugging checklist, in order: file on disk? → stored MIME correct? → Storage::disk(...)->url() publicly resolvable? → driver capable of the encoding? → memory/time limits hit? That sequence localizes ~95% of issues to either the driver or the disk, and neither is a package bug.

Extension points worth knowing: a thin subclass of AdvancedImage for project-wide defaults; ->storeAs() for path control; disk and conversion policy in config/ so staging and production can diverge without code changes. Those three cover most customization needs without coupling you to internals.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager