Photos, EXIF, GPS and Go

When you take a picture with any modern smartphone (iPhone, Android) it injects GPS data into resulting photo file unless owner changed default setting. GPS data is actually a part of Exif format, which represents a lot more information about photo itself. Some of its attributes include brand, device manufacturer, exposure, timestamp and geographical location. Photo sharing websites such as Flickr and 500px make a good use of that information and allow users to see photo specs, like which camera was used to make the shot.

I like to travel and i take a lot of pictures in the process. Over years i've accumulated an archive of photos made with various versions of iPhone (3-5). Sharing photos is usually not a problem, even Dropbox works fine. At some point i wanted to see the map of visited places. Most photo-sharing app dont have any good tools to achieve that. So after some experiments i've decided to give it a try and scan all my photo library and extract geolocation data. In fact i've done something like that with Ruby in the past and since i'm new to Go i though it would be good cool to try doing that in new language.

Install Dependencies

Initially i looked for any good exif libraries for Go and found a pretty decent one: github.com/gosexy/exif. Works great but it relies on libexif so its not 100% cross platform. On OSX you can install it pretty easity with homebrew:

brew install libexif

Now after the exif library is installed lets install Go package:

go get github.com/gosexy/exif

Parse EXIF

Lets parse out exif data from the photo:

package main

import(
  "log"
  "os"
  "fmt"

  "github.com/gosexy/exif"
)

func main() {
  path := "/path/to/photo.jpg"

  parser := exif.New()
  err := parser.Open(path)
  if err == nil {
    log.Fatalf("Error parsing exif:", err)
  }

  for k, v := range parser.Tags {
    fmt.Println(k, "=", v)
  }
}

Example output:

Color Space = sRGB
Manufacturer = Apple
GPS Image Direction = 353.081
Altitude Reference = Sea level
Latitude = 36,  8, 41.71
White Balance = Auto white balance
User Comment = Processed with VSCOcam with m3 preset
Compression = JPEG compression
YCbCr Positioning = Centered
Image Description = Processed with VSCOcam with m3 preset
Orientation = Right-top
GPS Date = 2014:01:01
Sensing Method = One-chip color area sensor
Sub-second Time (Original) = 419
Shutter Speed = 11.14 EV (1/2262 sec.)
Subject Area = Within rectangle (width 1795, height 1077) around (x,y) = (1631,1223)
Focal Length = 4.1 mm
...

The example above will iterate over all available exif tags and print them out as key-value pairs in the terminal. Here's the list of attributes related to GPS data:

  • Latitude
  • North or South Latitude
  • Longitude
  • East or West Longitude

Convert Coordinates

You cant just grab lat,lng pair from exif data. Exif stores coordinates in four separate tags. Latitude and longitude are expressed as three rational values giving the degrees, minutes, and seconds. Other tags specify reference, N/S for latitude and E/W for longitude. We need to make a function that converts them into regular floating point coordinates:

import (
  "strings"
  "strconv"
)

type Coordinate struct {
  Latitude  float64
  Longitude float64
}

func parseCoordString(val string) float64 {
  chunks     := strings.Split(val, ",")
  hours,   _ := strconv.ParseFloat(strings.TrimSpace(chunks[0]), 64)
  minutes, _ := strconv.ParseFloat(strings.TrimSpace(chunks[1]), 64)
  seconds, _ := strconv.ParseFloat(strings.TrimSpace(chunks[2]), 64)

  return hours + (minutes / 60) + (seconds / 3600)
}

func parseCoord(latVal, latRef, lngVal, lngRef string) *Coordinate {
  lat := parseCoordString(latVal)
  lng := parseCoordString(lngVal)

  if latRef == "S" { // N is "+", S is "-"
    lat *= -1
  }

  if lngRef == "W" { // E is "+", W is "-"
    lng *= -1
  }

  return &Coordinate{lat, lng}
}

The code above will parse geo data from exif and convert it into coordinate structure.