News

Validate and Convert HEIC Images in Laravel

Published
Validate and Convert HEIC Images in Laravel image

Every iPhone sold in the last several years shoots HEIC by default. The file is roughly half the size of the equivalent JPEG at the same quality, which is why Apple switched, and it is also a format that Chrome and Firefox will not render. An app that accepts photo uploads from phones has to deal with this somewhere, and until recently Laravel's image component was not the place: HEIC files were rejected before they reached the driver.

Laravel 13.24 accepts HEIC, HEIF, and AVIF as inputs, adds toHeic() for output, and teaches the image validation rule about all three. This article walks through accepting a phone photo and serving it in a format browsers understand.

What Your Server Needs

HEIC decoding is not something PHP does on its own. It comes from ImageMagick's HEIF delegate, which is built on libheif, so you need the Imagick extension with that delegate compiled in. The GD driver cannot read HEIC at all, no matter which PHP version you are on.

Many distribution packages ship ImageMagick without the HEIF delegate, so check rather than assume:

php -r "print_r(Imagick::queryFormats('HEI*'));"

An empty array means the extension is installed but cannot touch these files. On Debian and Ubuntu the delegate lives in libheif1, and on macOS the Homebrew imagemagick formula includes it. AVIF is more forgiving, since GD can decode it too when PHP was built against libavif.

Install Intervention Image if you have not already, since it backs both drivers:

composer require intervention/image:^4.0

Validating the Upload

The image rule checks the file against a fixed list of image types, and that list now includes heic, heif, and avif:

$request->validate([
'photo' => ['required', 'image', 'max:12288'],
]);

Nothing to configure. A photo taken on an iPhone and uploaded straight from the camera roll passes where it previously failed with "The photo field must be an image."

If you would rather be explicit about what you accept, the mimes rule takes the same extensions:

'photo' => ['required', 'mimes:jpg,png,webp,heic', 'max:12288'],

Note that HEIC uploads arrive with a variety of MIME types depending on the client, image/heic and image/heif being the common pair. The image and mimes rules resolve the type from the file contents rather than trusting the browser, so both land in the same place.

Converting on Upload

Accepting the file is only half of it. Store a HEIC as-is and most of your visitors get a broken image. The fix is to convert during the upload request, which is a couple of lines with the image API:

use App\Models\Photo;
use Illuminate\Http\Request;
 
public function store(Request $request)
{
$request->validate([
'photo' => ['required', 'image', 'max:12288'],
]);
 
$path = $request->image('photo')
->usingImagick()
->orient()
->scale(width: 2000)
->toWebp()
->quality(80)
->store('photos');
 
return Photo::create(['path' => $path]);
}

Two details worth calling out. usingImagick() is there because the default driver is GD, which will fail on a HEIC input. And orient() reads the orientation metadata and rotates accordingly, which matters more for phone photos than for anything else you will handle, since a portrait shot is usually stored as a landscape frame with a rotation flag.

The stored file gets the right extension automatically. store() builds a hashed filename from the output format, so a HEIC upload converted to WebP lands as photos/{hash}.webp.

Serving AVIF With a WebP Fallback

If you want the smaller file where it is supported, generate both variants off one source. Every transformation returns a new instance, so branching does not leak state between the two:

$source = $request->image('photo')->usingImagick()->orient()->scale(width: 2000);
 
$avif = $source->toAvif()->quality(70)->storeAs('photos', "{$id}.avif");
$webp = $source->toWebp()->quality(80)->storeAs('photos', "{$id}.webp");

Then let the browser pick:

<picture>
<source srcset="{{ Storage::url("photos/{$photo->id}.avif") }}" type="image/avif">
<img src="{{ Storage::url("photos/{$photo->id}.webp") }}" alt="{{ $photo->caption }}">
</picture>

AVIF typically lands 20 to 30 percent smaller than WebP at visually comparable quality, at the cost of slower encoding. If your uploads are synchronous, that encode time shows up in the request, so this is a good candidate for a queued job once you are doing more than one size.

Writing HEIC

Output works too, through toHeic() or optimize('heic'):

Image::fromPath(storage_path('app/photo.jpg'))
->usingImagick()
->toHeic()
->quality(80)
->store('photos');

This is a narrower use case than reading HEIC, but it comes up: an archive or export that will be opened on Apple devices, or a pipeline that keeps the original format on the way through.

The heif alias is normalized to HEIC everywhere, so optimize('heif') produces the same output as optimize('heic'), files are stored with the canonical .heic extension, and mimeType() reports image/heic. Image::extension() also recognizes the image/x-heic and image/x-avif MIME aliases that some clients send, mapping them to heic and avif rather than falling through.

When the Format Is Not Supported

If a file makes it to the driver in a format the component does not handle, you get an ImageException with the offending type in the message:

The image format [image/tiff] is not supported.

Validating first with the image rule catches nearly all of these, but the exception is also what you will see when a HEIC file reaches an Imagick build without the HEIF delegate. That is a deployment problem rather than a user problem, so it is worth checking the delegate on the server as part of a release rather than discovering it from an error report.

For anything older than 13.24, or when you need the conversion outside of Laravel, converting HEIC to JPEG in PHP covers the manual approach.

Further Reading

Paul Redmond photo

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

Filed in

Sponsored

serpapi logo
SerpApi

The Web Search API for Your LLM and AI Applications

Visit SerpApi

The latest

View all →
Saga Lara Flow: Durable Workflows and Compensating Transactions on Laravel Queues image

Saga Lara Flow: Durable Workflows and Compensating Transactions on Laravel Queues

Read article
Reject Unexpected Array Keys with Laravel Validation image

Reject Unexpected Array Keys with Laravel Validation

Read article
Major performance improvements & security patches for Filament v4.12 and v5.7! image

Major performance improvements & security patches for Filament v4.12 and v5.7!

Read article
Laravel Boost Project Rules: Teach Agents Your Conventions image

Laravel Boost Project Rules: Teach Agents Your Conventions

Read article
Extract an Image's Dominant Color in Laravel image

Extract an Image's Dominant Color in Laravel

Read article
Image Dominant Color and HEIC Support in Laravel 13.24 image

Image Dominant Color and HEIC Support in Laravel 13.24

Read article