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

Heif Converter Laravel Package

maestroerror/heif-converter

A Laravel/PHP package for converting HEIF/HEIC images into web-friendly formats like JPEG or PNG. It provides a simple API for handling uploads, transforming Apple-style photos, and generating compatible files for storage or display.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations: this is not a Composer package. maestroerror/heif-converter is a standalone Go CLI (plus a Docker image) that wraps libheif. There is no service provider, no facade, no config/ to publish. You integrate it the same way you'd integrate ffmpeg or pdftoppm: install the binary, shell out to it, parse exit codes.

Minimal steps

  1. Read the repo README for the exact CLI shape and flags (heif-converter source.heic target.jpg, quality/output-format flags). Confirm with heif-converter --help on the machine you'll actually deploy to — don't trust your laptop.
  2. Pick an install path:
    • Docker image — fastest to evaluate, but adds a container to your stack.
    • Release binary — download, chmod +x, drop in /usr/local/bin.
    • Build from source — only if you need a newer libheif than the image ships.
  3. Verify codec support, not just that the binary runs:
    heif-converter --help                 # flags
    heif-converter sample.heic out.jpg   # real decode test
    
    A binary that starts but can't decode HEVC is the single most common failure mode. Run this against a genuinely iPhone-originated file.
  4. In Laravel, add a config entry for the binary path and a thin wrapper:
// config/heif-converter.php
return [
    'binary'  => env('HEIF_CONVERTER_BIN', '/usr/local/bin/heif-converter'),
    'timeout' => env('HEIF_CONVERTER_TIMEOUT', 120),
    'quality' => 85,
];
use Illuminate\Support\Facades\Process;

$result = Process::timeout(config('heif-converter.timeout'))
    ->run([config('heif-converter.binary'), $source, $target]);

if ($result->failed()) {
    throw new RuntimeException('HEIC conversion failed: '.$result->errorOutput());
}

First use case: an upload endpoint that accepts iPhone HEIC photos and produces JPEGs for display. Store the original, generate a JPEG derivative in a queued job, serve the derivative.

Where to look first: the repo's README (CLI flags, Docker tags), then your base image's libheif packaging — that's where 80% of "it doesn't work" comes from.

Implementation Patterns

Wrap it once, inject everywhere. A single HeifConverter service class (constructor takes binary path + timeout, one convert(string $from, string $to, array $options): void method) keeps CLI details out of controllers and jobs. Bind it in a service provider; swap it for a fake in tests so your test suite doesn't need the binary installed.

Always use the array form of Process::run(). Never interpolate paths into a shell string. iPhone filenames routinely contain spaces, unicode, and parentheses — the array form handles all of it and closes an injection hole if filenames ever derive from user input.

Queue the conversion. HEIC decode is CPU-bound and takes noticeably longer than a JPEG resize — hundreds of milliseconds to a few seconds per image, more for 48MP. Put it in a job; never convert inline in a request. Set $timeout on the job and ->timeout() on the Process call, and give the job $tries = 1 unless you know the failure is transient (codec errors retry identically and just burn queue capacity).

class ConvertHeicToJpeg implements ShouldQueue
{
    public int $timeout = 180;
    public int $tries = 1;

    public function __construct(public string $disk, public string $path) {}

    public function handle(HeifConverter $converter): void
    {
        $tmpIn  = tempnam(sys_get_temp_dir(), 'heic_in_');
        $tmpOut = tempnam(sys_get_temp_dir(), 'heic_out_').'.jpg';

        try {
            file_put_contents($tmpIn, Storage::disk($this->disk)->get($this->path));
            $converter->convert($tmpIn, $tmpOut);
            Storage::disk($this->disk)->put(
                $this->derivativePath(), fopen($tmpOut, 'r')
            );
        } finally {
            @unlink($tmpIn);
            @unlink($tmpOut);
        }
    }
}

Round-trip through local temp files, not streams. The CLI is file-in/file-out. With S3 or another remote disk, download to tempnam(), convert, upload, unlink in a finally. Watch /tmp sizing on containerized deployments — tmpfs defaults can be small.

Docker integration, two viable shapes:

  • Multi-stage copy into your app image — COPY --from=maestroerror/heif-converter:latest /usr/local/bin/heif-converter /usr/local/bin/ (adjust the source path per the image). One container, simplest ops, but inherits the image's codec set.
  • Sidecar with a shared volume — better if you want to isolate CPU-heavy work or the image targets a different libc than your app container.

MIME handling belongs before conversion. Laravel's image validation rule and mimes:jpg,png do not accept HEIC. Detect with mimes:heic,heif plus a mimetypes:image/heic,image/heif fallback, and be aware File::mimeType() sometimes returns application/octet-stream for HEIC depending on the fileinfo magic database version — so validate with an explicit extension and MIME allowance, and treat unparseable MIME as "attempt conversion, fail gracefully."

Batch/backfill. The CLI converts one file per invocation. For migrations, loop in a queued job that dispatches per-file jobs (Bus::batch), and cap queue worker concurrency on the conversion queue — running 8 workers on a 2-core box just makes everything slower.

Keep a fallback path. If you already depend on ImageMagick/Imagick compiled against libheif, treat that as the backup implementation behind the same HeifConverter interface. Switching implementations then costs one binding change.

Gotchas and Tips

The codec trap. libheif itself doesn't decode HEVC (HEIC) — it needs libde265, and AVIF needs libaom/dav1d. Distro packages split these out; Alpine's libheif in particular has had HEVC omitted for licensing reasons. Symptoms: exit code non-zero with "unsupported compression" / "cannot open" / "No decoding plugin". Always test a decode in CI against a committed sample HEIC, not just --help.

HEIC encoding is a different problem. The tool also goes JPEG→HEIC, but that path needs an HEVC encoder (x265) and carries patent/licensing considerations you probably don't want in a SaaS. Most teams should treat this package as decode-only.

Last release: June 2023. The pinned libheif in the Docker image is old relative to current upstream. That's not fatal — libheif is stable and HEIC hasn't changed — but pin the image by digest, and if you hit a decode bug on a specific iPhone model or a newer AVIF, assume you need to build the binary against a current libheif rather than expecting an upstream fix. Have a maintenance plan; 8 stars and 0 dependents means you are the maintainer.

EXIF orientation is the #1 visual bug. iPhone photos store rotation in EXIF. Verify output orientation on portrait shots; if it's wrong, chain a fix (ImageMagick -auto-orient after conversion, or the tool's own rotate flag if it has one). Test with a portrait photo, not the landscape one you'll reflexively grab.

Colour shifts. HEIC is typically Display P3; naive conversion can drop the ICC profile to sRGB-less output and produce washed-out or oversaturated JPEGs. Compare a converted image against the iOS render before shipping, especially for skin tones. Keeping the original file is your insurance.

Exit code 0 isn't a full success signal. Some builds print per-image errors to stdout/stderr while exiting 0. For anything user-facing, verify filesize($target) > 0 after the call — it's one line and catches the whole class.

Symfony Process defaults to a 60s timeout. Large HEICs on a loaded box can exceed that and you'll get a timeout exception that looks like a codec failure. Set it explicitly per call.

Clean up temp files in finally. Failed conversions leave both the input and (often) a zero-byte output behind. On a container with a small tmpfs this becomes a slow-motion outage.

Log errorOutput(), not just "failed". The useful message (Unsupported codec, Invalid input: not a HEIF file) is almost always in stderr, and it's what distinguishes "user uploaded a corrupt file" from "our deployment lost libde265."

Path with a leading dash. A filename like -foo.heic will be read as a flag. Either prefix with ./ or pass an explicit -- if the tool supports it — easy to miss because real filenames rarely look like that until one does.

Don't convert on the read path. Resist the urge to lazily convert HEIC when a missing derivative is requested. You'll end up with conversions happening inside web requests under load, timeouts cascading into 500s, and no retry semantics. Convert on write, serve what exists, and return a placeholder for missing derivatives.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager