Summary
HeifFileParser.canHandle() rejects any file whose ftyp box is larger than 50 bytes. iOS 18 writes a 52-byte ftyp on adaptive-HDR HEIC photos (e.g. from iPhone 15 Pro), so exifr no longer recognizes them as HEIC even though the EXIF payload is completely intact.
Repro
- Take a photo on an iPhone 15 Pro (or any iOS 18 device with HDR on).
const result = await exifr.parse(file, { gps: true })
- Expected:
{ latitude, longitude, DateTimeOriginal, ... }
- Actual: no
HeifFileParser.canHandle() returns true, so exifr falls through all parsers and the caller ends up with "Unknown file format" / empty metadata.
Root cause
In src/file-parsers/heif.mjs:
let ftypLength = file.getUint16(2)
if (ftypLength > 50) return false
iOS 17 and earlier HEIC files have a ftyp of around 28 bytes, so the heuristic was safe. iOS 18 adds compatible brands to signal adaptive-HDR tone-mapping (notably tmap, plus heic hevc unif on top of the usual heic mif1 miaf MiHB), which pushes the ftyp box to 52 bytes.
Example byte layout
iPhone 14 / iOS 17: ftyp size=28 brands: heic mif1 miaf MiHB
iPhone 15 Pro / 18: ftyp size=52 brands: heic mif1 miaf MiHB
tmap heic hevc unif
Proposed fix
Three options, in increasing invasiveness:
- Raise the ceiling to something that still rejects obviously-bogus files but tolerates current and near-future Apple output — e.g. 128 or 256 bytes.
- Remove the length cap entirely and let the
major_brand / compatible-brand check in the following step do the work.
- Derive the cap from the number of brands observed rather than a fixed byte count.
Option (1) is the smallest change and would unblock iOS 18 without touching the rest of the canHandle logic. Happy to send a PR if you have a preference.
Impact
Every iOS 18 HEIC photo from iPhones with adaptive HDR on (iPhone 15 Pro and later shoot adaptive HDR by default). The rejection is silent from the caller's perspective — exifr just returns nothing — so this is hard to diagnose without reading the parser source.
Workaround I'm currently using
In TrailPaint I keep exifr as the primary parser and dynamically import exifreader as a fallback only when exifr returns no GPS and no metadata on a HEIC file. Short write-up. Upstream fix would let me drop the ~34 KB fallback for most users.
https://jacobmei.com/blog/2026/0421-2yh1pu/
https://jacobmei.com/blog/2026/0421-49ofw6/
Thanks for exifr — it's been the default EXIF reader in my projects for years.
Summary
HeifFileParser.canHandle()rejects any file whoseftypbox is larger than 50 bytes. iOS 18 writes a 52-byteftypon adaptive-HDR HEIC photos (e.g. from iPhone 15 Pro), so exifr no longer recognizes them as HEIC even though the EXIF payload is completely intact.Repro
const result = await exifr.parse(file, { gps: true }){ latitude, longitude, DateTimeOriginal, ... }HeifFileParser.canHandle()returnstrue, so exifr falls through all parsers and the caller ends up with"Unknown file format"/ empty metadata.Root cause
In
src/file-parsers/heif.mjs:iOS 17 and earlier HEIC files have a
ftypof around 28 bytes, so the heuristic was safe. iOS 18 adds compatible brands to signal adaptive-HDR tone-mapping (notablytmap, plusheic hevc unifon top of the usualheic mif1 miaf MiHB), which pushes theftypbox to 52 bytes.Example byte layout
Proposed fix
Three options, in increasing invasiveness:
major_brand/ compatible-brand check in the following step do the work.Option (1) is the smallest change and would unblock iOS 18 without touching the rest of the
canHandlelogic. Happy to send a PR if you have a preference.Impact
Every iOS 18 HEIC photo from iPhones with adaptive HDR on (iPhone 15 Pro and later shoot adaptive HDR by default). The rejection is silent from the caller's perspective — exifr just returns nothing — so this is hard to diagnose without reading the parser source.
Workaround I'm currently using
In TrailPaint I keep exifr as the primary parser and dynamically import
exifreaderas a fallback only when exifr returns no GPS and no metadata on a HEIC file. Short write-up. Upstream fix would let me drop the ~34 KB fallback for most users.https://jacobmei.com/blog/2026/0421-2yh1pu/
https://jacobmei.com/blog/2026/0421-49ofw6/
Thanks for exifr — it's been the default EXIF reader in my projects for years.