Extracting Geolocation Image Data with Carrierwave and RMagick on Heroku

Many smartphones embed latitude and longitude EXIF data relative to where the photo was taken. So why not extract that data and store it to the database when a user uploads an image?

I am currently using this technique in an API I built which allows users to post a picture of an item they would like to sell. Next an ad is created for the item with the location populated by the image’s embedded geolocation data.

On Heroku we would like the exif data extracted from the temporary image file before it is uploaded to S3. To do this we can call a method to extract the exif date before_save. In the model file:

app/models/item_image.rb
class ItemImage < ActiveRecord::Base
  belongs_to :item
 
  mount_uploader :image, ImageUploader
 
  before_save :extract_geolocation
 
  def extract_geolocation
    img = Magick::Image.read(image)[0] rescue nil
 
    return unless img
    img_lat = img.get_exif_by_entry('GPSLatitude')[0][1].split(', ') rescue nil
    img_lng = img.get_exif_by_entry('GPSLongitude')[0][1].split(', ') rescue nil
 
    lat_ref = img.get_exif_by_entry('GPSLatitudeRef')[0][1] rescue nil
    lng_ref = img.get_exif_by_entry('GPSLongitudeRef')[0][1] rescue nil
 
    return unless img_lat && img_lng && lat_ref && lng_ref
 
    latitude = to_frac(img_lat[0]) + (to_frac(img_lat[1])/60) + (to_frac(img_lat[2])/3600)
    longitude = to_frac(img_lng[0]) + (to_frac(img_lng[1])/60) + (to_frac(img_lng[2])/3600)
 
    latitude = latitude * -1 if lat_ref == 'S'  # (N is +, S is -)
    longitude = longitude * -1 if lng_ref == 'W'   # (W is -, E is +)
 
    self.lat = latitude
    self.lng = longitude
  end
 
  def to_frac(strng)
    numerator, denominator = strng.split('/').map(&:to_f)
    denominator ||= 1
    numerator/denominator
  end
end

The extract_geolocation method uses RMagick to access the image EXIF data. If it contains latitude and longitude data expressed as linear units (degree, minute, second), but we would like it in decimal format so that it can be fed into the Google Maps API and returned as a city, state and zipcode. This is what the calculations in the method are handling.

With a gem like Geocoder we can easily wire in geocode look up and store the city, state, and zipcode in the image record.

  if geo = Geocoder.search("#{latitude},#{longitude}").first
    self.city = geo.city
    self.state = geo.state
    self.zipcode = geo.postal_code
  end

Putting it all together:

Feedback on how to improve the process is welcome.