Verify GPS Removal from iOS Photo Exports

Security ·~5 min read

Verify GPS Removal from iOS Photo Exports

A photo-sharing screen may hide the location where a picture was taken, while the exported file still contains coordinates. When building an iOS photo export feature, test the file the recipient actually receives—not the preview inside the app. A test photo with GPS data, prepared on a cloud Mac, turns that privacy requirement into a repeatable file check.

Define the export boundary first

A typical path is to select a photo, edit or resize it, create a temporary file, then pass that file to the sharing flow. Each step may handle metadata differently. Checking only the image returned by the picker does not prove that the final file is clean; checking a thumbnail before sharing does not prove it either.

Create the test fixture yourself and confirm that the original file contains a GPS dictionary. Do not use real users’ photos as continuous integration fixtures or put a fixture containing coordinates in a public repository. Keep it in a controlled test-assets directory, record its expected format and pixel dimensions, and remove generated files after the test.

The pass condition here is “the final file has no GPS dictionary,” not “the export is byte-for-byte identical to the original.” Re-encoding will usually change the file hash.

Generate and inspect the file with ImageIO

The Swift script below reads an input image, decodes it, and re-encodes it in the original file type. It explicitly passes through only the orientation; it does not hand the input’s entire metadata dictionary to the output. It then reopens the output and checks the GPS dictionary, dimensions, orientation, and format. Save it as redact.swift in an isolated directory:

import Foundation
import ImageIO
import CoreGraphics

guard CommandLine.arguments.count == 3 else {
    fatalError("Usage: redact input output")
}

let input = URL(fileURLWithPath: CommandLine.arguments[1])
let output = URL(fileURLWithPath: CommandLine.arguments[2])
guard input.standardizedFileURL != output.standardizedFileURL else {
    fatalError("Input and output must differ")
}
guard let source = CGImageSourceCreateWithURL(input as CFURL, nil),
      let type = CGImageSourceGetType(source),
      let image = CGImageSourceCreateImageAtIndex(source, 0, nil),
      let original = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
        as? [CFString: Any],
      let destination = CGImageDestinationCreateWithURL(
        output as CFURL, type, 1, nil
      ) else {
    fatalError("Cannot read input or create output")
}

let orientation = original[kCGImagePropertyOrientation] ?? 1
let options: [CFString: Any] = [
    kCGImagePropertyOrientation: orientation
]
CGImageDestinationAddImage(destination, image, options as CFDictionary)
guard CGImageDestinationFinalize(destination),
      let result = CGImageSourceCreateWithURL(output as CFURL, nil),
      let finalImage = CGImageSourceCreateImageAtIndex(result, 0, nil),
      let metadata = CGImageSourceCopyPropertiesAtIndex(result, 0, nil)
        as? [CFString: Any] else {
    fatalError("Cannot verify output")
}

guard metadata[kCGImagePropertyGPSDictionary] == nil,
      CGImageSourceGetType(result) == type,
      finalImage.width == image.width,
      finalImage.height == image.height,
      String(describing: metadata[kCGImagePropertyOrientation] ?? 1)
        == String(describing: orientation) else {
    fatalError("Export verification failed")
}
print("GPS removed; format, dimensions and orientation checked")

Run it in the cloud Mac’s terminal:

swiftc -framework Foundation -framework ImageIO \
  -framework CoreGraphics redact.swift -o redact
./redact fixtures/with-gps.jpg output/without-gps.jpg

Create the output directory first, and replace the example input with a JPEG confirmed to contain a GPS dictionary. A successful run shows only that this particular output passed. If the production app has cropping, compression, or share-extension paths, check the final file produced by each path. The script is not a substitute for testing the app’s actual export implementation.

Add a negative test case

A passing “cleaned” file alone does not prove that the assertion works. Use the same reading logic on the input fixture: it must detect a GPS dictionary. Then check the output: the dictionary must be absent. If the input had no coordinates to begin with, a test that passes for both files tells you nothing. To avoid leaking data into logs, do not print coordinate values or the complete metadata.

Also keep an unmodified copy as a negative test case and confirm that the checker rejects it. Copy the file bytes directly, without passing them through an image tool that might rewrite the metadata. This validates both the removal step and the checker: it must not mistake a read failure for “no GPS.”

What else to check after re-encoding

Removing location data does not mean every image feature still works correctly. Decoding and re-encoding can change lossy compression quality, color appearance, or other non-location metadata. Matching dimensions do not guarantee identical pixels. Include photos from different sources in the regression set, covering at least portrait and landscape shots and different orientation tags. Visually inspect the final files for rotation, color shifts, or obvious distortion.

Check Pass condition
Location data No GPS dictionary in the final file
Image dimensions Width and height match expectations for that export path
Display orientation Displays correctly according to the output orientation tag
File format The recipient receives the expected format
Read failure The test fails; the result is not treated as “no GPS”

The example script requires the output dimensions to match the input, which suits an export path that does not resize images. If the product crops or compresses images, replace that assertion with the explicit dimensions expected for the path. If color profiles or particular copyright fields must be preserved, test those fields separately; do not copy the original metadata dictionary wholesale to retain them.

Add the check to cloud Mac acceptance tests

Keep the fixture, the files actually generated by the app, and the checking script in separate locations. The pipeline should run the export first, then read the file at the final delivery path. A missing file, decoding failure, or metadata read failure must fail the test. Give parallel jobs separate output directories so a file left by an earlier run cannot produce a false pass.

Finally, perform a manual check: complete a real share or save operation in the app and inspect the file the recipient gets, not just an intermediate cache. This catches path errors such as a cleaned editing preview followed by sharing the original file. Delete temporary exports afterward. Test reports should record only whether a GPS dictionary exists, the dimensions and orientation, and the step that failed—not the coordinates. That makes the result reproducible without exposing the very information the export is meant to remove in troubleshooting logs.

Frequently asked questions

Does hiding the location in the app preview remove GPS data from the export?

No. Inspect the final exported file with a metadata reader and confirm that it has no GPS dictionary.

Should the original and re-encoded files have identical hashes?

No. Re-encoding can change file bytes. Check GPS metadata, dimensions, orientation, and the rendered image separately.

Choose a rental term

Validate your next step on a dedicated physical Mac mini

VMCommit offers dedicated physical M4 Mac mini rentals by the day, week, month, or quarter. Choose a model and location, then check real-time availability in the console.

Rent a Mac mini