Reading an iPhone HEIC Photo's Hidden Location by Hand

Most iPhone photos are HEIC files, and most of them carry the exact GPS location where they were taken. When I built ToolOnTap’s EXIF viewer, I wanted it to show that location for HEIC photos in any browser. That sounds hard, because only Safari can decode HEIC images. But you don’t need to decode the picture to read its metadata. You only need to understand how the file is put together.

This is how I did it, starting from a real photo taken on an iPhone 16.

A HEIC file is a set of boxes

HEIC is the HEIF image format (ISO/IEC 23008-12), which is built on the ISO base media file format, the same family as MP4. The whole file is a sequence of boxes. Each box starts with its size in 4 bytes, then a 4-letter type, then its contents. Some boxes contain other boxes.

There are two special sizes: 1 means a 64-bit size follows the type, and 0 means the box runs to the end of the file. That’s all you need to walk through any box list:

function boxes(b: Uint8Array, view: DataView, from: number, to: number): Box[] {
  const list: Box[] = []
  let i = from
  while (i + 8 <= to) {
    let size = view.getUint32(i)
    let header = 8
    if (size === 1) { if (i + 16 > to) break; size = Number(view.getBigUint64(i + 8)); header = 16 }
    else if (size === 0) size = to - i
    if (size < header || i + size > to) break
    list.push({ type: ascii(b, i + 4, 4), start: i, body: i + header, end: i + size })
    i += size
  }
  return list
}

The last check matters. A box that claims to be bigger than the file means the file is damaged or cut off, and the loop stops instead of reading past the end.

Running this over the iPhone 16 photo gives three top-level boxes:

Box Size What it holds
ftyp 52 bytes The file type: which brands of HEIF it follows
meta about 36 KB The description of every item in the file
mdat about 3.4 MB The actual data: compressed image tiles, EXIF and the rest

The surprise: one photo is 117 images

Inside meta, the iinf box lists the file’s items. I expected one image. The iPhone 16 photo has 117 HEVC-coded images, most of them 512 × 512 tiles, plus 3 grid items that assemble tiles into full images: the photo itself at 5712 × 4284 and smaller ones. Next to them are a few other items, and one of type Exif.

So an iPhone doesn’t store a photo as one big picture. It cuts it into tiles that can be decoded in parallel, and a grid item says how they fit together. For reading metadata, that’s good news: I can ignore all the tiles and look for the single Exif item.

Finding the Exif item: iinf and iloc

Two boxes inside meta lead to the EXIF bytes.

iinf (item info) has one infe entry per item, with the item’s ID and its 4-letter type. Loop over them and remember the ID of the one typed Exif:

const iinfVersion = b[iinf.body]!
let exifId = -1
for (const infe of boxes(b, view, iinf.body + 4 + (iinfVersion === 0 ? 2 : 4), iinf.end)) {
  const v = b[infe.body]!
  if (infe.type !== 'infe' || v < 2) continue
  const idSize = v === 2 ? 2 : 4
  const id = idSize === 2 ? view.getUint16(infe.body + 4) : view.getUint32(infe.body + 4)
  if (ascii(b, infe.body + 4 + idSize + 2, 4) === 'Exif') { exifId = id; break }
}

The + 4 after each box start skips its version and flags. iinf and infe are “full boxes”, which start with 1 byte of version and 3 bytes of flags. The version changes the layout: a version 0 iinf stores its item count in 2 bytes and later versions in 4, and a version 2 infe has 2-byte IDs where version 3 has 4-byte ones. Most bugs in a parser like this are a field read with the wrong width.

iloc (item location) says where each item’s bytes are in the file. It’s compact and a little unusual. Its header gives the size of its own fields in 4-bit nibbles: how many bytes an offset takes, how many a length takes, and so on. The iPhone file uses 4-byte offsets and lengths. Then, for each item, it lists one or more extents, each an offset and a length into the file. Find the entry with the Exif item’s ID, and its extent points into mdat.

From the Exif item to the GPS coordinates

The Exif item starts with 4 bytes that say how far ahead the TIFF header is. That’s usually 6, to skip the text Exif\0\0:

const skip = new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0)
return data.subarray(4 + skip)

From there it’s the same EXIF as in a JPEG, and EXIF is a small TIFF file:

  • 2 bytes for the byte order: II (Intel, little-endian) or MM (Motorola, big-endian);
  • the number 42, then the offset of the first directory (IFD0);
  • directories made of 12-byte entries: a tag, a type, a count, and either the value or an offset to it.

IFD0 has the camera make and model. Tag 0x8769 points to the Exif directory with the shutter speed, aperture and the date taken. Tag 0x8825 points to the GPS directory. There the latitude and longitude are each three rational numbers (degrees, minutes, seconds, each a fraction), with N/S and E/W in separate tags. The viewer turns them into decimal coordinates and also reads the altitude and the direction the camera was facing.

Making it safe

Files from the internet are untrusted, and a parser that crashes or hangs on a bad file is a bug. A few rules kept this one safe:

  • Every read checks that it’s inside the file, and a box that claims to be bigger than its parent stops the walk.
  • Directory pointers are remembered, so a damaged file whose IFD points back to itself can’t make the reader loop forever.
  • Anything unexpected returns “no EXIF found” instead of throwing.

The tests build a small HEIC file in memory, with ftyp, meta, iinf, iloc and mdat laid out by hand, and check that the viewer finds the location in it, and that a truncated copy fails cleanly. I also ran it on real photos from an iPhone 11, 13 Pro and 16.

Why not just use a library

ToolOnTap already loads heic2any to convert HEIC to JPG, and it’s about 1.3 MB because it includes a full HEVC decoder. Reading the metadata takes about 75 lines and no download at all, because it never touches the pixels. The EXIF viewer shows what a photo reveals, and Remove EXIF takes it out before you share it.

Tools in this article

  • EXIF viewer

    Open a JPG, PNG, WebP or iPhone HEIC photo to see its hidden EXIF data: GPS location, date and time zone, camera, lens and settings. No upload.

  • Remove EXIF data from photos

    See and remove the hidden GPS location, camera details and dates in your photos before you share them. Lossless, in your browser, no upload.

  • Convert HEIC to JPG

    Convert iPhone HEIC photos to JPG in your browser, one at a time or a whole batch with a ZIP download. Nothing is uploaded.

More articles