Skip to main content

← Docs home · Getting started · Commands

How it works

The problem​

Photos taken on iOS 18+ iPhones carry HEIF metadata that older libheif releases cannot parse: a tmap tone-map/gain-map item and additional auxiliary images. Both libheif 1.12.0 (Ubuntu 22.04) and 1.17.6 (Ubuntu 24.04) fail before the image is decoded. Upgrading PHP does not help, because every PHP version on the server shares the same system ImageMagick and libheif.

DistributionlibheifiOS 18 HEIC
Ubuntu 22.041.12.0fails
Ubuntu 24.041.17.6fails
Ubuntu 26.041.21.2fixed natively; install the HEVC decoder plugin
Debian 121.15.1fails
Any build with libheif ≥ 1.18.2≥ 1.18.2works, with an HEVC decoder available

Ubuntu 26.04 does not need this build. Install libheif-plugin-libde265 for HEVC decoding; see Ubuntu 26.04.

Symptoms​

A failed upload commonly produces one of these:

  • PHP Imagick: ImagickException: Failed to read the file
  • heif-convert 1.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 partial fix, with PHP GD installed: undefined symbol: heif_has_compatible_filetype (or another undefined symbol: heif_...)
  • Or the old library is silently used and decoding still fails.

The undefined symbol error is the GD trap described below.

Prefix layout​

/opt/heic-ios18-fix/
├── heif/ libde265 + libheif, private SONAMEs
│ ├── bin/ heif-dec, heif-info, heif-enc, ...
│ └── lib/
│ ├── libde265-heicfix.so.0
│ ├── libheif-heicfix.so.1
│ └── pkgconfig/
├── heif-static/ PIC archives linked into MagickCore
│ └── lib/
│ ├── libde265.a
│ ├── libheif.a
│ └── pkgconfig/
├── imagemagick/ ImageMagick 7.1.x, Q16-HDRI
│ ├── bin/magick
│ └── lib/libMagickCore-7.Q16HDRI.so
├── php/
│ └── 8.3/imagick.so per PHP version, linked to the prefix
├── src/ downloaded sources and build trees
├── logs/ one log per build step
├── tmp/
└── installed-versions.txt

heif/ is the shared build used by the diagnostic tools. heif-static/ holds the archives that are linked into ImageMagick, so the two roles stay separate. Nothing here shadows or replaces /usr/lib. The system ImageMagick and libheif remain available to other software.

Per-PHP-version extension​

Imagick is a thin PHP extension over ImageMagick's MagickWand C API, so it must be compiled for each PHP ABI. The script builds the same Imagick tag once per requested PHP version and records the result:

/etc/php/8.3/mods-available/imagick-heic.ini
; priority=20
extension=/opt/heic-ios18-fix/php/8.3/imagick.so

The stock imagick.ini is disabled with phpdismod -v 8.3 imagick, and phpenmod -v 8.3 imagick-heic creates the CLI/FPM symlinks. FPM is reloaded only after confirmation.

The GD symbol-capture trap​

PHP's GD extension (gd.so) is linked against the system libheif.so.1. On Debian/Ubuntu, gd.so is loaded before imagick.so, so the dynamic linker already has libheif.so.1 mapped and has exported its heif_* functions globally.

Private SONAMEs alone do not fix that on every PHP build. A SONAME stops the linker from reusing the system object; it does not rename the exported functions. On Ubuntu 22.04 with PHP 8.5, a prefix that used only private SONAMEs loaded both libheifs and produced this binding trace:

libMagickCore-7.Q16HDRI.so.10 → /lib/x86_64-linux-gnu/libheif.so.1 heif_context_alloc
libMagickCore-7.Q16HDRI.so.10 → /opt/heic-ios18-fix/heif/lib/libheif-heicfix.so.1 heif_context_get_security_limits
libMagickCore-7.Q16HDRI.so.10 → /lib/x86_64-linux-gnu/libheif.so.1 heif_context_read_from_file
libMagickCore-7.Q16HDRI.so.10 → /lib/x86_64-linux-gnu/libheif.so.1 heif_context_free

The private ImageMagick decoded the sample, and Imagick decoded it when loaded alone. With GD loaded first, libheif 1.12.0 parsed the file and failed with Too many auxiliary image references. One function handed a context allocated by the other library version, which is unsafe whether or not it happens to decode.

RTLD_DEEPBIND is not a guarantee here: the trace above comes from a real PHP 8.5 process that uses it.

How the build isolates the symbols​

build compiles a second, position-independent static copy of libde265 and libheif into heif-static/, then links those archives into libMagickCore-7 with:

-Wl,--exclude-libs,ALL
-Wl,-Bsymbolic-functions

The heif_* and de265_* definitions become local to libMagickCore, so every ImageMagick call binds to the private code regardless of what GD loaded. GD keeps its own system libheif.so.1; the two implementations coexist in one PHP process.

The resulting library has no shared libheif dependency at all:

$ readelf -d /opt/heic-ios18-fix/imagemagick/lib/libMagickCore-7.Q16HDRI.so.10 | grep -i heif
$ ldd /opt/heic-ios18-fix/php/8.3/imagick.so | grep -i heif

Both commands print nothing. check, verify and the pre-flight report libheif linkage=static-hidden only when that layout is proven.

The shared heif/ build keeps its private SONAMEs so heif-dec, heif-info and the diagnostic paths keep working next to the system libraries.

Realistic pre-flight​

Before enabling anything, php builds a temporary PHP_INI_SCAN_DIR from the site's real FPM conf.d (or CLI conf.d if FPM is absent), removes stock imagick entries, adds the new extension, and runs PHP with that scan directory. GD is therefore loaded exactly as it is in production.

php -n is deliberately not used: it skips GD and hides the symbol-capture bug.

The pre-flight must report:

  • gd=yes (or a warning if GD is genuinely not installed),
  • Imagick loaded,
  • ImageMagick 7,
  • HEIC listed by Imagick::queryFormats("HEI*"),
  • libheif link : static-hidden,
  • a successful sample decode when a sample is present.

If any check fails, the extension is not enabled.

needrestart​

A plain apt install once triggered needrestart on a production GridPane server; it restarted nginx, MariaDB and every php-fpm pool. Every apt call in the script runs with:

NEEDRESTART_MODE=l NEEDRESTART_SUSPEND=1 DEBIAN_FRONTEND=noninteractive apt-get ...

Why not just upgrade PHP?​

Every PHP version on the server uses the same system ImageMagick and libheif. Changing PHP does not change the HEIC decoder. The long-term fix is a distribution with libheif ≥ 1.18.2 (Ubuntu 26.04) or a private build like this one.

See Commands for version overrides and GridPane for platform-specific notes.