News

Image Dominant Color and HEIC Support in Laravel 13.24

Published
Image Dominant Color and HEIC Support in Laravel 13.24 image

Laravel 13.24 extends the framework's image API with dominant color detection and HEIC input and output, adds a modelKeys() method to the Eloquent query builder, introduces an array_keys validation rule, and fixes wildcard validation that could stall a request for over a minute on large arrays. The Laravel team released v13.24.0 on August 4, 2026.

  • dominantColor() on images, plus a dominant background for contain() and rotate()
  • AVIF and HEIC input, toHeic() output, and both formats in the image validation rule
  • modelKeys() on the Eloquent query builder
  • New array_keys validation rule with an :unexpected placeholder
  • A fix for quadratic rule expansion in foo.*.bar validation

What's New

Dominant Color Detection in the Image API

The first-party image API can now tell you the average color of an image. dominantColor() downsamples the image to a single pixel and returns that pixel as a hex string:

use Illuminate\Support\Facades\Image;
 
$color = Image::fromPath(storage_path('app/photo.jpg'))->dominantColor(); // "#8a6f4c"

That is the value you would use for a placeholder background while a photo loads, or to tint a card behind its own artwork. The result is memoized per image instance, and if the pipeline has pending transformations, they run before the color is sampled, so you get the color of the image you are about to store rather than the original.

The same sampling is available as a background value. Passing dominant to contain() or rotate() fills the letterbox bars or the rotation corners with the image's own dominant color instead of a color you picked in advance:

Image::fromUpload($request->file('photo'))
->contain(1200, 800, background: 'dominant')
->store('photos');

Contributed by @dr-codswallop in #60932.

AVIF and HEIC Input, HEIC Output

The image API exposed AVIF output through toAvif(), but rejected AVIF files on the way in, and HEIC and HEIF were rejected in both directions even though Intervention Image ships an encoder for them. Photos coming straight off an iPhone are HEIC, so uploads had to be converted before they reached the framework.

AVIF, HEIC, and HEIF are now accepted as inputs, toHeic() and optimize('heic') are available as outputs, and all three formats are recognized by the image validation rule:

Image::fromPath(storage_path('app/photo.heic'))
->usingImagick()
->cover(1200, 800)
->toAvif()
->quality(80)
->store('photos');

Both changes build on the image pipeline covered in our practical guide to Laravel's image processing. optimize('heif') and the image/heif MIME type are normalized to HEIC, so files are stored with the canonical .heic extension. Image::extension() also learned the image/x-avif, image/x-heic, and image/heif aliases. HEIC processing requires an Imagick build with the HEIC codec compiled in. Contributed by @riasvdv in #60922.

modelKeys() on the Eloquent Query Builder

Eloquent collections have had modelKeys() for a long time, but getting the same array straight off a query meant naming the primary key yourself with pluck('id'). The query builder now has the method too:

$ids = Post::query()->where('published', true)->modelKeys(); // [1, 2, 3]

It plucks the qualified key name from the model, so a custom $primaryKey and joined queries both resolve correctly without hardcoding a column. Contributed by @ziadoz in #60924.

array_keys Validation Rule

Rule::array(['sort', 'direction']) already rejects arrays containing unexpected keys, but it reports the failure with the generic message "The options field must be an array," which does not tell the reader which of the two checks failed. The new array_keys validation rule answers only the key question and says so:

$request->validate([
'options' => Rule::arrayKeys(['sort', 'direction']),
]);
 
// or as a string
$request->validate([
'options' => 'array_keys:sort,direction',
]);

Given ['sort' => 'name', 'colour' => 'red'], the failure message is "The options field must only contain the following keys: sort, direction." The keys are permitted, not required, so this constrains what may appear rather than demanding everything be present. required_array_keys still covers the other half.

Two placeholders are available for custom messages: :values for the accepted keys, and :unexpected for the keys that actually caused the failure, which is usually the more useful one in an API response:

$request->validate(
['options' => Rule::arrayKeys(['sort', 'direction'])],
['options.array_keys' => 'The :attribute field may not contain :unexpected.'],
);
 
// The options field may not contain colour.

Because the rule reports as ArrayKeys in $validator->failed(), it composes with array when you want both checks reported independently. Rule::array() and its message are untouched. Contributed by @nebarg in #60918.

Wildcard Validation No Longer Stalls on Large Arrays

Expanding foo.*.bar rules was quadratic in the number of expanded attributes. ValidationRuleParser::explodeWildcardRules() accumulated results by reassigning the return value of mergeRules(), which took the accumulated set by value, so copy-on-write cloned the entire rule set once per expanded attribute. All of the time was spent in Validator::make(), before a single rule ran.

The numbers from the pull request, using 17 rules under one wildcard:

Items Before After
1,000 0.98s 0.11s
4,000 18.59s 0.47s
8,000 85.13s 0.98s

At 8,000 items the payload is only about 1.1 MB, so post_max_size and web server body limits never fire. A max:500 rule on the array did not help either, since expansion happened before it was evaluated.

The merge body moved into a private method that takes the accumulator by reference. mergeRules() and mergeRulesForAttribute() keep their signatures, but explodeWildcardRules() no longer routes through the latter, so an override there will no longer affect wildcard expansion. Contributed by @mariomka in #60908.

Arr::forget() Removing the Wrong Element

Arr::forget() reset its internal reference back to the top-level array only after handling an exact top-level key match. After processing a dotted key, the reference was left pointing at a nested array, so the next key in the list resolved against that nested array:

$array = ['users' => ['name' => 'Joe', 'id' => 1], 'id' => 99];
 
Arr::forget($array, ['users.name', 'id']);
 
// before: ['users' => [], 'id' => 99]
// after: ['users' => ['id' => 1]]

The wrong element was removed and the requested one kept, with no error. Moving the reset to the top of the loop fixes it for everything that delegates to the helper, including Arr::except(), Collection::except(), data_forget(), and Uri::withoutQuery(). Contributed by @AlessioGiacobbe in #61023.

Other Fixes and Improvements

  • CompiledRouteCollection rebuilt every Route object on each get(), getByAction(), and getRoutesByMethod() call, which made 404s expensive on cached routes because checkForAlternateVerbs() calls get() once per HTTP verb. Lookup indexes built from the raw route attributes bring a 404 against 2,000 cached routes from 71ms to 2.5ms (#60909)
  • An accessor for an existing attribute that was also listed in $appends received null instead of the stored value, so $model->price and $model->toArray()['price'] disagreed (#60921)
  • Str::replace() and Str::remove() delegate to str_ireplace() for case-insensitive matching, which only folds ASCII, so Str::replace('ž', 'X', 'Žltý kôň', false) matched nothing. Multibyte search terms now use a /iu pattern; ASCII terms take the original path (#60882)
  • Resolving a relationship attribute inside a Relation::noConstraints() closure skipped the where clause on the primary key, so a BelongsTo could return the first row in the table instead of the right one. A new Relation::withConstraints() wraps attribute resolution (#60931)
  • PostgresGrammar::wrapJsonPathAttributes() interpolated JSON path attributes into quoted SQL literals without escaping single quotes, unlike every other driver (#60923)
  • Two aliases pointing at each other made Container::getAlias() recurse until memory exhaustion; it now throws a LogicException (#60974)
  • NumberPrompt was the only prompt type without a fallback, so number() threw on Windows, in non-interactive contexts, and in tests instead of degrading to a Symfony question (#60959)
  • ShouldBeUniqueUntilProcessing jobs now track the cache lock owner and release through restoreLock(), so a retry cannot release a lock acquired by a newer dispatch while middleware-released jobs still get their lock back (#60906)
  • Guarded attribute resolution was skipped on Pivot, which overrides $guarded with [] and so failed the ['*'] default check (#60952)
  • Job delays are respected when bulk pushing to QueueFake (#60916) and FailoverQueue (#60950), and batch testing fakes use immutable timestamps (#60912)
  • LengthAwarePaginator guards against division by zero when perPage is 0, and CursorPaginator reindexes items consistently on both directions (#60968)
  • Most Artisan commands moved from the $name plus getArguments()/getOptions() style to a single $signature string, backed by characterization tests that record every command's name, aliases, arguments, and options. Four commands stayed on the array style because they have options with required values, which the signature DSL cannot express (#60926)
  • URL validation logic was adjusted (#61009), and several PHP 8.5 null array offset deprecations were resolved (#60964, #60954, #60987)

Upgrade Notes

No breaking changes are expected for typical applications. Two changes are worth checking if you extend framework internals: explodeWildcardRules() no longer calls mergeRulesForAttribute(), so an override of that method will not affect wildcard expansion, and Container::getAlias() now throws a LogicException on circular aliases that previously exhausted memory. HEIC support depends on your Imagick build including the HEIC codec.

Two pull requests merged during this cycle were reverted before release, the if-related rector rules (#61027) and a Str::uuid()->toString() conversion (#61028). Review the changelog for PR-by-PR details when upgrading.

References

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Sponsored

masteringlaravel logo
Laravel Code Review

Get expert guidance in a few days with a Laravel code review

Visit Laravel Code Review

The latest

View all →
Official Laravel Zed Extension: LSP for PHP & Blade image

Official Laravel Zed Extension: LSP for PHP & Blade

Read article
Laravel Head: Manage Meta Tags, Open Graph, and JSON-LD image

Laravel Head: Manage Meta Tags, Open Graph, and JSON-LD

Read article
PhpStorm 2026.2 Released image

PhpStorm 2026.2 Released

Read article
Laravel Doctor: Diagnose Your App With One Artisan Command image

Laravel Doctor: Diagnose Your App With One Artisan Command

Read article
CPX: The Composer Package Executor for PHP image

CPX: The Composer Package Executor for PHP

Read article
Laravel AI SDK Adds Human-in-the-Loop Tool Approval image

Laravel AI SDK Adds Human-in-the-Loop Tool Approval

Read article