DEV Community

AsyncMonk
AsyncMonk

Posted on

Pillow dropped GPS from my test photos, sips kept it, and one PNG had it twice

Most images on the small sites I build for clients start as phone photos, and before deploy they go through whatever resize step the project has. On a budget that tends to be a short Pillow script, or macOS sips in a shell loop because it's already installed and costs nothing. I had always assumed that shrinking a photo produces a new file and the location goes away with the old one. I didn't want to find out otherwise on a live site, so I set up a throwaway folder and checked. The short version: it depends entirely on the tool, and the same folder can end up half clean and half not.

The test folder

The source photos are two. One I generated with a script: a 4032×3024 JPEG with iPhone-style EXIF, Orientation 6 and GPS set to a made-up point on the sea at 30°N 123°E. The other is a real photo, "Avions à l'aéroport de Madrid - 2015" from Wikimedia Commons, released under CC0 and taken with an iPhone 6, which has GPS at 40°29′27.76″N. I ran both through the operations a build step typically does: Pillow's default save to JPEG and WebP, Pillow thumbnail(), Pillow saving with the original EXIF passed back, sips converting to JPEG, PNG and HEIC, sips -Z resizing, and piexif.remove(). That gave 11 files in a folder called public/, laid out like a static site's image directory.

A scanner that fails the build

The scanner walks a directory, reads EXIF through Pillow so it covers JPEG, PNG and WebP with one code path, and returns exit code 1 if anything has GPS or can't be read. For each file it looks at the GPS block in the EXIF and also searches the XMP packet for a GPSLatitude entry, printing the coordinates when it finds them. The XMP check is there because of what the first version missed, which I'll get to.

Output of my scan script on the throwaway test folder: 11 files, 5 with GPS, 1 unreadable. Synthetic sample plus a CC0 iPhone 6 photo from Wikimedia Commons; Python 3.9, Pillow 11.3, macOS sips

Five of 11 files still had coordinates, and the pattern is clean: every sips output that Pillow could read kept GPS, including both resizes, one of them the real iPhone 6 photo at 1200×900. Every Pillow output without exif= lost it. The one Pillow file that kept it is the one where I passed the old EXIF back on purpose, which is exactly the line someone adds after a user complains that their photos lost the camera info. The HEIC can't be opened by Pillow on my machine, so the scanner counts it as unreadable and fails rather than calling it clean. I'd rather have a false alarm there than a file nobody actually looked at.

The PNG line with +XMP is what the first version of the scanner missed. When sips converts to PNG it writes the GPS twice: once in the EXIF chunk and once as exif:GPSLatitude inside the XMP packet. My first scanner only read the EXIF GPS block. Had I stripped that block and rescanned with the same scanner, the PNG would have reported clean with a full copy of the coordinates still sitting in XMP.

What each way of stripping costs

Once you know which files have it, there are four ways I considered to get rid of it. I'm counting cost the way I count everything on these projects: CPU on every build, extra dependencies on the CI box, and bytes served.

Approach What it did in my test Cost
Re-encode everything with Pillow's default save GPS gone, along with all EXIF and the ICC profile for JPEG/WebP; Orientation dropped without rotating, so sideways photos stay sideways A full lossy decode and encode per file per build
piexif.remove() Removes all EXIF from JPEG without re-encoding; ICC kept; Orientation gone too Cheap, but you need to rotate first or portraits break
Delete only the GPS IFD (details below) JPEG: pixels identical, 219 to 325 bytes smaller per file. PNG: needs a re-save Cheap for JPEG; PNG re-save grew the file
ExifTool -gps:all= Not run: ExifTool isn't installed on this machine One more binary on CI, and backup files to clean up

The GPS-only route is what I'd pick. For JPEG it's a piexif job: load the EXIF, empty the GPS block, drop the pointer to it from the main IFD, and swap the APP1 segment back in without touching the pixels. PNG has no such shortcut, so the same step opens the file in Pillow, clears the GPS entry from its EXIF and saves it again, passing the ICC profile through. I ran it on the five flagged files and rescanned the whole folder: 11 files, 0 with GPS, 1 unreadable. Each JPEG got a little smaller: 219 bytes for the Pillow file that had kept its EXIF, 325 for the Madrid resize, and 232 for each of the two sips JPEGs.

The PNG went the other way, from 24,387,228 to 25,081,704 bytes, because Pillow's re-save compressed it less tightly than sips had. I compared pixels before and after for the three sips files and all were identical, so nothing visible changed, but that's 0.7 MB more for nothing on a single image. It's also worth being honest about why the PNG came out clean: Pillow's PNG save didn't write the XMP or the sRGB chunk back, so the second copy of the GPS disappeared as a side effect I didn't plan. Luck, not design. For PNGs I'd now convert to a web format in the build anyway and never ship the 24 MB file.

On ExifTool, I'm going by its official documentation only. exiftool -gps:all= file.jpg removes the GPS group, and by default the tool keeps the original next to it with _original appended. In a deploy folder, that means the untouched file with full coordinates is sitting right beside the cleaned one and gets uploaded with everything else unless the build deletes it. For a static site that is the trap I'd most expect to fall into.

The HEIC is still in the "unreadable" column, and I haven't solved it. For now the scanner failing on it is the answer: it's a signal to convert HEIC earlier in the pipeline, not to teach every step about HEIC. The scan itself is one Pillow import and runs on the CI box that already builds the site, so it costs me nothing beyond a few lines in the build config, which is the price range I like.

Top comments (2)

Collapse
 
raknaos profile image
Raknaos

The double-storage problem is the part most people miss. A scanner that only reads the EXIF GPS block will call a sips-produced PNG clean while the same coordinates sit in the XMP packet as exif:GPSLatitude, so the passing check is really a check of one of two copies. Your first version wasn't wrong code, it was a partial definition of "clean".

Two of your calls I'd copy: failing closed on HEIC (unreadable is not the same as clean, and a green build that never opened the file is worse than a red one), and the ExifTool _original trap — the untouched photo with full coordinates sitting next to the cleaned one, inside the folder you deploy. Did you end up making the scanner assert that no *_original files exist as a build precondition, or is that still on the to-verify list?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.