A HEIC photo is nine pictures and an 8-byte note on how to staple them together

HEIC gets two complaints, and they are usually made in the same breath: the files are impressively small, and almost nothing will open them. Both are true, both have concrete causes, and the causes are unrelated to each other.

The small part is a codec decision. The opening part is a container decision. This article measures the first and takes apart a real file to show the second.

How much smaller, actually

"Half the size of JPEG" is the usual claim. It is worth testing, because the quality numbers in the two formats are not comparable — a HEIC at quality 50 and a JPEG at quality 50 have nothing in common except the digits.

So: one source image, saved losslessly as PNG, fed to both encoders, and the results scored by peak signal-to-noise ratio against that same source. Higher PSNR is closer to the original.

  HEIC        bytes    PSNR      JPEG        bytes    PSNR
  q30        15,212   38.29      q30        42,006   36.47
  q50        27,996   40.03      q50        50,805   38.38
  q70        61,528   41.05      q70        63,788   40.14
  q85        83,450   41.37      q85       106,758   42.87
  q95       218,831   42.11      q95       182,723   48.30

Read it by finding equal quality rather than equal settings.

At about 38.3 dB, HEIC needs 15,212 bytes. JPEG reaches the same quality at just under its quality 50, which costs about 50,000. That is roughly a third of the size for the same picture.

At about 40 dB, HEIC needs 27,996 bytes and JPEG needs 63,788. Less than half.

So the claim holds, and at the lower end it is understated. But look at the last row.

Where HEIC stops

Between quality 70 and 95, HEIC's file size more than triples — from 61,528 to 218,831 bytes — and the quality improves by one decibel. JPEG at quality 95 is smaller and six decibels better.

HEIC appears to hit a ceiling at around 42 dB that more bytes will not break through. Splitting the error by channel says where the ceiling is:

                 bytes    Y (brightness)     Cb      Cr
  HEIC q50      27,996        46.40        42.58   43.32
  HEIC q95     218,831        51.17        43.80   44.89
  JPEG q50      50,805        42.87        42.02   42.44
  JPEG q95     182,723        54.79        50.87   51.28

HEIC's brightness channel behaves exactly as expected: 46.4 dB at quality 50, 51.2 at quality 95, and it beats JPEG's luma at quality 50 by three and a half decibels while using half the bytes. The codec is doing its job.

The colour channels are the ones that stall. They move from about 43 dB to about 44 dB while the file triples. JPEG's colour channels, meanwhile, climb from 42 to 51 dB, because above quality 95 that encoder stops throwing colour resolution away.

The combined score is dragged down by whichever channel is worst, so HEIC's overall figure is pinned by colour no matter how many bytes go into brightness. That is an encoder policy, not a limit of the codec, and it means the headline is narrower than it is usually stated: HEIC is dramatically smaller at the qualities phones and messaging apps actually use, and the wrong tool for an archival master.

What the codec does that JPEG cannot

The byte counts say HEVC wins; they do not say how. One of the differences is visible with the same grid test used on JPEG: measure the average brightness step across every eighth column and divide it by the average step elsewhere. A value of 1 means there is no grid.

The source here had already been a JPEG once, so it arrives carrying a grid of its own — 1.482 before either encoder touches it. That makes it a useful test, because now we can watch what each encoder does to a grid that already exists.

                      bytes     every 8px
source (lossless copy)    -         1.482
JPEG  q50            50,805         2.088
HEIC  q50            27,996         1.247

JPEG takes the existing grid and deepens it, as the previous article measured in detail. HEIC, using 55% of the bytes, leaves the picture with less of a grid than it started with.

That is not a rounding artefact. HEVC runs a deblocking filter inside the coding loop: after each block is reconstructed, the edges between blocks are smoothed, and the smoothed result is what the next block is predicted from. JPEG has nothing equivalent — its decoder hands back exactly what the inverse transform produces, boundaries and all. So an encoder that re-encodes an already-blocky picture through HEVC partly cleans it up on the way through, while JPEG can only add.

The other large difference does not show up in this test. JPEG transforms every block independently and never predicts: each 8x8 square is described from scratch. HEVC predicts each block from its already-decoded neighbours first and only codes the difference, over block sizes it chooses per region rather than a fixed 8x8. A flat sky costs almost nothing because the prediction is already right. That is where most of the 2-to-3x comes from.

Now the file itself

A JPEG begins with two bytes, FF D8, and from there a decoder is essentially already reading the picture. HEIC does not work like that at all. It is built on the same container as MP4 — a tree of boxes, each with a length and a four-character name.

Here is the entire structure of the 88,010-byte file produced above:

ftyp   @0             36 B   major=heic  compatible=mif1 MiPr miaf MiHB heic
meta   @36           915 B
  hdlr   @48            33 B
  dinf   @81            36 B
    dref   @89            28 B
  pitm   @117           14 B
  iinf   @131          245 B
  iref   @376           56 B
  iprp   @432          311 B
    ipco   @440          216 B
      colr   @448           19 B
      clli   @467           12 B
      ispe   @479           20 B
      ispe   @499           20 B
      irot   @519            9 B
      pixi   @528           16 B
      hvcC   @544          112 B
    ipma   @656           87 B
  idat   @743            16 B
  iloc   @759           192 B
mdat   @951        87,059 B   <- the picture data

915 bytes of bookkeeping wrapped around 87 kilobytes of picture. That ratio is fine. The problem is what the bookkeeping says.

The primary item is not a picture

The pitm box names the primary item — the thing you get when you open the file. In this file it says item 10.

The iinf box lists what the items are:

1=hvc1  2=hvc1  3=hvc1  4=hvc1  5=hvc1
6=hvc1  7=hvc1  8=hvc1  9=hvc1  10=grid  11=Exif

Items 1 through 9 are HEVC-coded pictures. Item 11 is the Exif metadata. Item 10 — the primary one, the one that is the photograph — has the type grid.

A grid is not an image. It is an instruction. Reading its bytes requires the iloc box, which gives each item's location, and for this item the location is not in mdat at all but in the small idat box. All eight bytes of it:

00 00 02 02 04 b0 04 b0

version 0, flags 0
rows    = 2 + 1 = 3
columns = 2 + 1 = 3
width   = 0x04b0 = 1200
height  = 0x04b0 = 1200

Three rows by three columns, composing to 1200 by 1200. Which nine items? That is in yet another box, iref, under the sub-type dimg:

iref/dimg: item 10 is built from 9 items -> [1, 2, 3, 4, 5, 6, 7, 8, 9]

So this single photograph is stored as nine entirely separate HEVC bitstreams. The two ispe properties give their size and the output size: the tiles are 512 by 512, and three of them across is 1536, so the composed image is cropped back to the declared 1200.

The arithmetic closes, which is the check that the parse is right: the nine tile payloads come to 86,965 bytes, the Exif item is 78, and together that is 87,043 — exactly the size of the mdat payload. Nothing unaccounted for.

What opening one actually takes

Putting that in order, here is what a program must do to show a HEIC photograph, before it decodes a single pixel:

read pitm           -> the primary item is 10
read iinf           -> item 10 is a "grid", not a picture
read iloc           -> its bytes are in idat, not mdat
parse those 8 bytes -> 3x3 tiles composing to 1200x1200
read iref/dimg      -> the tiles are items 1..9
read iprp/ipco/hvcC -> the HEVC decoder configuration
decode 9 separate HEVC bitstreams and compose them

Seven steps across six box types, and only then does an HEVC decoder get to start — nine times.

Compare that with the format it replaced. To display a JPEG: find the start-of-frame marker for the dimensions, read the Huffman and quantisation tables, decode. One image, one pass, no indirection.

This is the real answer to "why doesn't my software open HEIC". It is not that the format is secret or that nobody has got round to it. It is that a conforming reader has to implement a general-purpose item-and-property database, tile composition, and a full HEVC intra decoder — and HEVC's patent licensing has historically made bundling that decoder a commercial decision rather than a technical one. A library that handles JPEG in a few hundred lines needs something far larger here.

Why the tiles exist at all

The tiling is not arbitrary. HEVC was designed for video, and hardware decoders in phones are built around a maximum frame size and benefit from work that can be split across cores. Cutting a large still into 512-pixel tiles lets the same silicon that decodes video decode a photograph, in parallel, without a special path.

That is a sound engineering decision on a phone. It is also exactly the decision that makes the file awkward everywhere else, and it is worth noticing that the awkwardness is not a side effect — it is the same choice, seen from the other end.

When these numbers do not apply

This is one encoder. The file was produced by Apple's, which is the one that matters for photos from an iPhone but is not the only one. Other HEIF encoders make different choices, including about tiling and about the colour handling that caused the ceiling above.

The source had been a JPEG before. Both encoders were fed the same lossless copy of it, so the comparison between them is fair, but neither was working from a pristine camera original. The absolute PSNR figures would be higher with a true original; the relative picture is what this measures.

PSNR is not perception. It is a reproducible proxy, which is why it is used here, but it does not fully agree with how the result looks. A six-decibel gap is real; a half-decibel one is not worth arguing about.

Not every HEIC is a grid. Small images are often stored as a single hvc1 item with no tiling at all, in which case the walk above is shorter. The machinery still has to be there, because the reader cannot know which kind it has until it has read the item table.

Nothing here is about AVIF. AVIF uses the same container and the same item structure, so the opening problem is identical, but the codec underneath is AV1 rather than HEVC, with a different licensing position and different behaviour at high quality.

Checking it yourself

Every claim above is a few bytes into the file. Boxes are a 4-byte big-endian length followed by a 4-byte name, so the first 8 bytes of any HEIC read as a length and then ftyp. Skip that many bytes and the next name appears.

One trap worth knowing: a length of 1 does not mean a 1-byte box. It means the real 64-bit length follows the name, which is how a box larger than 4 GB is expressed. A parser that misses this reads the file as 1-byte boxes and gives up — the first version of the parser used for this article did exactly that, and reported the picture as 8 bytes long.

If you want the short version: search a HEIC for the four ASCII characters grid. If they are there, the photograph you are looking at is several pictures in a trench coat.

Sources

Tools mentioned

Advertisements

← All articles · corrections: editorial policy