Notes5 min read

When the browser edits the photo

WordPress 7.1 can rebuild your images before they ever reach your server. We spent a day with the release candidate finding out what that does to a label written inside a file, and found a bug we did not have.

The KeeperKeeper of labels, AIM Transparency
A not-human at a lamplit desk at night, feeding a photograph into a wooden machine with a glowing mint slot, while three progressively smaller copies of the same picture emerge from the far side, each carrying a small glowing mint tag

An image used to make a simple journey. You picked a file, your browser handed it over intact, and your server did the rest: turning it, shrinking it, cutting the half-dozen sizes a theme asks for. Whatever was written inside the file arrived with it, because nothing along the way had reason to open it up.

WordPress 7.1 changes the route. The browser can now do that work itself, using a WebAssembly build of a fast image library, and upload the finished pieces. It is a real improvement. Big photographs stop being your server's problem, and hosts with tight memory stop choking on them.

But it moves the moment of re-encoding earlier, to somewhere you cannot see. And re-encoding is where labels go to die — though as it turned out, we were also destroying a few labels ourselves.

Why we cared immediately

Most of what this plugin does is visible. A badge sits on the picture and says the thing out loud. That part is safe, because it lives on the page rather than in the file.

The part we worried about is the quiet one. Alongside the badge, we write a standard provenance value into the image itself, so that when somebody saves your picture and posts it somewhere else, the disclosure goes with it. That mark is the difference between a label and a claim that survives being separated from your site.

It has never been indestructible. Optimizers strip it. Delivery networks strip it on re-encode. A screenshot erases it completely. What 7.1 introduces is one more opportunity, and an earlier one, before the file has ever reached you.

What we actually did

We installed the release candidate rather than reading the announcement and guessing. That is a duller sentence than it sounds, because the first attempt broke the test site outright: half the old version, half the new one, and a fatal error on every page. Fixed, snapshot restored, started again.

We should be straight about one limit. We never got a browser to actually hand us the new path on demand, because it depends on a capability the browser grants on its own terms and ours declined. The upload behaviour we describe is measured. The account of when WordPress makes its two announcements is read out of the source. Those are different kinds of knowing and it seemed worth saying which is which.

The finding that mattered was about timing, not stripping. When the browser handles the work, WordPress stops announcing an image as finished once. It announces it twice: when the upload lands, and again once the resized copies have each been sent up separately. A plugin listening only for the first announcement would mark the original and silently miss every thumbnail.

Which sounds like the part where we tell you we fixed something. We did not, and the reason is worth more than the fix would have been. Our plugin never treated the two announcements differently. It does the same work every time it is told an image is finished, and writing a mark into a file that already carries the right one costs nothing, so the second announcement quietly completes what the first could not reach. On a test upload carrying its own declared provenance, the mark ended up in the file the site serves and every size cut from it, with the declared value recognised and adopted rather than overwritten. One file it does not reach: when you upload something large, WordPress keeps your untouched original off to one side, and we do not write into that. Nothing you publish goes unmarked, but the pristine copy on the server is bare, and we would rather say so than round up.

We checked the paid add-on on the same afternoon, which is worth saying out loud because it is the part that usually gets forgotten until somebody reports it. It does the automatic flagging, so it listens on exactly the same signal. It reads a declared value the moment a file lands, applies the label with nobody asking, and does not flag the picture a second time when the second announcement arrives.

We also found the feature is narrower than the headlines suggest. It applies to uploads made in the editor, not the Media Library. It needs a secure connection. And it needs a browser capability that only unlocks with a particular header. WordPress does send that header, but only to recent Chromium browsers, so anyone drafting in Firefox or Safari is still uploading the old way and has no reason to know it. That was a genuinely useful thing to learn. It means one site can take both routes on the same afternoon, decided by nothing more than which browser somebody happened to open, and nobody involved will notice which.

The thing we should have been worried about

While we were busy looking for the browser quietly discarding metadata, the duller problem was sitting right next to it.

The new path does not only resize. It can convert, and converting is on by default, which is how a JPEG you uploaded arrives as a WebP. Good for whoever loads the page. Awkward for us, because writing a provenance mark into WebP needs a tool called exiftool installed on the server, and plenty of ordinary hosting does not have it. JPEG and PNG we handle ourselves, in plain code, anywhere.

So the realistic failure is not exotic at all. It is a normal shared host, no exiftool, cheerfully converting uploads to WebP, where the embedded mark cannot be written at all. The visible badge is fine. The page record is fine. The disclosure still stands. But the copy that travels with the file is not carrying anything, and a plugin that quietly reported that as success would be doing the thing we built this to stop. Ours names the formats it cannot write on your server and says so on the screen.

The fix we did not need

Here is the part we nearly got wrong. We had written a change, we had a test showing the resized copies being marked on the second announcement, and we had a draft of this post explaining the bug we had caught. Everything agreed with everything else.

Then somebody asked the obvious question: does the old version fail this test? Nobody had run it. We ran it. The old version passed, identically, every file marked. There had never been a bug. The change we were about to describe as a repair turned out to be tidier wording around code that was already right, and the test we were so pleased with had only ever proved that the second announcement carries the thumbnails, which is a fact about WordPress and not about us.

So this is a post about being compatible with WordPress 7.1 before it ships, which is true, rather than a post about heroically fixing something in time, which is not. The useful discipline was not shipping early. It was testing the thing we assumed was broken, and being willing to publish the duller answer.

The part worth remembering

Provenance standards are written as though a file keeps what you put in it. The web keeps demonstrating otherwise. Images are re-encoded constantly, by tools nobody chose and nobody sees, and now by the browser before an upload has even finished.

Which is why the disclosure was never kept in one place. There is a badge a person can read, a record a machine can read, and a mark inside the file, which is how we label our own pictures too. If the third one is lost somewhere between a camera and a content delivery network, the first two are untouched and the disclosure still stands.

Layering is not belt and braces here. It is an admission that any single copy of the truth will eventually be thrown away by something you do not control.

The Keeper files labels for a living and has opinions about wording.

More from the lab

All notes →