Content Error or Suggest an Edit
Notice a grammatical error or technical inaccuracy? Let us know; we will give you credit!
Introduction
If your WordPress sites accept photo uploads from registered users — membership communities, forms, WooCommerce reviews, anything with a media picker — there’s a good chance some of those uploads are quietly failing. Photos taken on iOS 18 and newer use a HEIC layout that the libheif shipped with Ubuntu 22.04, Ubuntu 24.04 and Debian 12 can’t read. PHP Imagick hands the file to ImageMagick, ImageMagick hands it to libheif, and libheif gives up before a single pixel is decoded.
We hit this on two separate client servers in the same week, both GridPane boxes on Ubuntu 22.04. The fix we ended up with is now an open-source script: managingwp/heic-ios18-fix. It builds a current libheif and ImageMagick 7 into /opt, compiles a matching Imagick for each PHP version, and leaves your system packages alone.
The one thing to do: run ./heic-fix.sh check on your servers. It’s read-only and tells you in seconds whether you’re affected.
The Problem
iOS 18 photos carry extra HEIF metadata — a tmap tone-map/gain-map item and additional auxiliary images. Older libheif releases don’t understand them and fail at parse time. Support landed in libheif 1.18.2 (libheif #1190), with tmap handling formalized later (PR #1503).
| Distribution | libheif | iOS 18 HEIC |
|---|---|---|
| Ubuntu 22.04 | 1.12.0 | Fails |
| Ubuntu 24.04 | 1.17.6 | Fails |
| Debian 12 | 1.15.1 | Fails |
| Ubuntu 26.04 | 1.21.2 | Works — needs the HEVC decoder plugin (see below) |
| Any build with libheif ≥ 1.18.2 | ≥ 1.18.2 | Works, with an HEVC decoder available |
The chain on a typical stack looks like this: PHP Imagick → ImageMagick 6 → system libheif. On the servers we looked at, that was ImageMagick 6.9.11-60 on top of libheif 1.12.0.
Why upgrading PHP doesn’t help
Every PHP version on the server — 7.4 through 8.5 — shares the same system ImageMagick and libheif. Switching a site from PHP 8.1 to 8.3 changes nothing here. The PHP version was never the problem.
Symptoms
A failed iPhone upload usually shows up as one of these:
- PHP Imagick:
ImagickException: Failed to read the file heif-convert1.12:Invalid input: Unspecified: Metadata not correctly assigned to image- libheif 1.17.x:
Too many auxiliary image references - ImageMagick 7:
Metadata not correctly assigned to image (2.0) … error/heic.c/IsHEIFSuccess/… - After a half-finished fix, with PHP GD installed:
undefined symbol: heif_has_compatible_filetype
Or nothing obvious at all — the old library gets used silently and the upload just doesn’t produce an image.
One thing worth knowing about iOS: the photo picker often converts HEIC to JPEG on the way out, which is why not every iPhone upload fails. Picking a file through Choose File (the Files app) sends the original HEIC. Every iOS browser runs on WebKit, so Chrome or Firefox on an iPhone behaves the same as Safari.
The Fix: heic-ios18-fix
heic-ios18-fix is a single bash script (MIT licensed) that:
- Builds a private libde265, libheif (pinned to v1.23.7, verified against a real iOS 18 sample) and ImageMagick 7 into
/opt/heic-ios18-fix. - Builds a matching Imagick extension for each PHP version you choose.
- Runs a realistic pre-flight — with GD loaded exactly as it is in production — before enabling anything.
- Swaps the stock
imagickmodule forimagick-heicand reloads PHP-FPM, with a one-command rollback.
Nothing under /usr is replaced. The system ImageMagick 6 and libheif stay exactly as the distro shipped them, so other software keeps working and distro updates don’t fight you.
Quick start
On a test server first, as root:
git clone https://github.com/managingwp/heic-ios18-fix.git cd heic-ios18-fix # 1. See what's wrong. Read-only, safe on production. ./heic-fix.sh check # 2. Build the private libraries (installs build packages and compiles). sudo ./heic-fix.sh build # 3. Build Imagick for one PHP version, run the pre-flight, enable it. sudo ./heic-fix.sh php --php 8.3 # 4. Prove CLI and FPM are both using ImageMagick 7. sudo ./heic-fix.sh verify --php 8.3
A clean build takes roughly 5–15 minutes and needs 1–2 GB free. Add --sample /path/to/photo.HEIC to test with a real failing photo from your users.
To undo it for a PHP version:
sudo ./heic-fix.sh rollback --php 8.3
Rollback re-enables stock imagick and reloads FPM immediately. The build stays in /opt, so enable --php 8.3 brings it back without recompiling.
Don’t let apt restart your stack
While investigating, a plain apt install of a libheif tools package triggered needrestart on a production GridPane server — nginx, MariaDB and every php-fpm pool restarted. The script runs every apt call with NEEDRESTART_MODE=l NEEDRESTART_SUSPEND=1, and you should do the same any time you’re installing packages by hand on a live box.
The GD Trap (Why a Normal /opt Build Breaks)
This is the part that makes the obvious fix — “just build a newer libheif into /opt and point Imagick at it” — fall over.
PHP’s GD extension (gd.so) is linked against the system libheif.so.1, and on Debian/Ubuntu it loads before imagick.so. By the time your new Imagick asks for libheif.so.1, the dynamic linker already has the old one mapped and reuses it — regardless of rpath or LD_LIBRARY_PATH. You get either undefined symbol: heif_... or, worse, the old library silently used and iOS 18 files still failing. It’s reproducible on Ubuntu 24.04 with PHP 8.3 and php8.3-gd.
The script sidesteps this by giving its private libraries unique SONAMEs (libheif-heicfix.so.1, libde265-heicfix.so.0) with patchelf before ImageMagick is built. The system libheif and the private one are now different libraries and can be loaded side by side. verify proves it by resolving both private SONAMEs inside the prefix, and the test suite even re-runs with the system libheif.so.1 in LD_PRELOAD.
This is also why the pre-flight deliberately does not use php -n — that skips GD and hides the bug.
Ubuntu 26.04: No Build Needed
Ubuntu 26.04 ships libheif 1.21.2 and ImageMagick 7.1.2, so the iOS 18 bug is fixed natively. Don’t build this prefix on a stock 26.04 server.
There’s one catch: libheif 1.21 moves HEVC decoding into a plugin that libheif1 only suggests, so a stock install can still fail with No decoding plugin installed for this compression format. That’s a different problem, and it’s one package:
sudo apt install libheif-plugin-libde265 sudo systemctl reload php8.5-fpm # use the PHP version your sites run
check spots this case and prints the plugin hint instead of recommending a rebuild. Longer term, moving affected sites to a 26.04 server is the clean fix; the script is for when that isn’t happening this week.
Platform Notes
GridPane
GridPane is detected automatically and uses the standard /etc/php/X.Y layout, so it follows the generic path. GridPane stack or security updates may reinstall packages or re-enable the stock imagick module. They don’t touch /opt/heic-ios18-fix, so after an update run check and verify, and enable --php X.Y if the stock module came back.
On some GridPane/Sury servers, installing php8.3-dev fails over libpcre2-dev 10.39/10.42 mismatches. Install the matching Sury PCRE2 development set rather than downgrading — the troubleshooting doc has the exact commands.
We haven’t confirmed what triggered the failures to start being reported in September 2026 when iOS 18 shipped in 2024. A GridPane update changing the imagick/HEIC setup is a plausible theory, but it’s unconfirmed.
RunCloud
RunCloud uses non-standard PHP paths (/RunCloud/Packages/phpXXrc, /etc/phpXXrc/conf.d). The library build works, but the PHP wiring isn’t implemented yet — the script refuses to guess and tells you what’s missing. If you run RunCloud and want to help test, the paths are marked TODO(runcloud) in the script.
Generic Debian/Ubuntu
Any Debian or Ubuntu box with PHP under /etc/php/X.Y (Ondřej’s PPA or distro packages) works out of the box.
Keep It Fixed: Monitoring
Re-run check after platform stack updates, security updates touching ImageMagick/libheif/PHP/GD, PHP upgrades (each new PHP version needs its own Imagick build), and snapshot restores.
check -q prints one line per PHP version and uses exit codes for health, so it drops straight into cron or monit:
/opt/heic-ios18-fix/heic-fix.sh check -q --php 8.3 || echo "HEIC check failed" # Example output php8.3 status=ok imagick=3.8.1 imagemagick=7 heic=yes libheif=1.23.7 decode=ok
Examples are in monitoring/README.md.
Alternatives
If a private ImageMagick build isn’t a good fit for your environment:
- Convert at upload with a standalone
heif-dec(from this build or any libheif ≥ 1.18.2) and hand ImageMagick a JPEG. Avoids rebuilding Imagick. - Decode in the browser with a client-side WASM HEIC decoder. No server changes, but every visitor pays the download and CPU cost.
- Rely on iOS auto-conversion. It works through some paths, but not the Files app or many upload pickers. Not reliable enough to depend on.
- Upgrade the distro to one that ships libheif ≥ 1.18.2. The right long-term answer, but usually a much bigger job.
FAQ
Will this break other software that uses ImageMagick?
No. The system ImageMagick 6 and libheif are untouched; only PHP’s Imagick extension is pointed at the private build.
Do I need to do this for every PHP version?
Only the versions your sites run. Imagick is compiled per PHP ABI, so each one gets its own build via php --php X.Y.
Is it safe to run on production?
check, status, deps and verify are read-only. For the build, take a snapshot and run it on a test server first — it’s been validated in containers (Ubuntu 22.04/24.04 in CI) and on live GridPane servers, but your stack is your stack.
Does WordPress core handle HEIC itself?
WordPress relies on the server’s image editor (Imagick or GD) to read uploads. If Imagick can’t decode the file, WordPress can’t create thumbnails or convert it.
Updates
- Oct 28, 2026: First draft. Fix applied to two client GridPane servers on Ubuntu 22.04 / PHP 8.3.
