← All posts

What happens to your EXIF

· Julian Lindner

Open any photograph from a phone or a camera in a metadata viewer and you will find rather more than you expected: the make and model, the lens, the shutter speed and aperture, the moment it was taken to the second, and — very often — the latitude and longitude to within a few metres.

That data is genuinely useful. It is how you search your library for everything shot on a particular lens, or everything from one trip. It is also exactly what you do not want attached to a picture you post publicly.

What Miru does

When you upload a file, Miru reads the metadata once and stores it in your library, where it powers search: by camera, by lens, by date, by place.

Then it generates the display copies — thumbnail, medium, large — and those copies carry no metadata at all. Not a reduced set, not everything-but-GPS. None.

Your original is untouched and stays exactly as you uploaded it. When you download it, you get your metadata back, because it is your file.

The bug

This is worth telling because it went wrong for the entire life of the app before launch, and the way it went wrong is instructive.

The code asked libvips to strip metadata on save, using an option called strip. That option was correct when it was written. libvips later replaced it with a different one called keep, and the Elixir binding silently ignores options it does not recognise.

So the code said "strip metadata", the tests passed, nobody had written a test that asked whether the metadata was actually gone — and every image served carried the photographer's full EXIF, GPS included.

Measured after the fact: the supposedly-stripped output had thirteen metadata fields in it.

The fix was one line. The lesson was not. A check that cannot observe the thing it is checking is not a check, and "the code says it does X" is not evidence that it does X. There are now tests that read the served file back and assert the metadata is absent — tests that would have failed loudly the whole time.

The same dead option, it turned out, had also silently broken image editing and one of the RAW conversion paths. All three were found the same afternoon, by a static analyser pointing out that a function could never return successfully.