Blog

Extracting GPS location from uploaded images

Exif analyzer for Active Storage

Rails 5.2 introduces Active Storage, which can replace external file uploading gems like CarrierWave, PaperClip or Shrine. There are some helpful articles and tutorials about it, e.g. by Evil Martians, GoRails or Drifting Ruby. I want to show how to add an additional feature.

Active Storage in Rails 5.2.0 is just the beginning. Recently, Pull Request #32471 by Janko Marohnić (the author of Shrine) was merged, which allows us to use the ImageProcessing gem instead of mini_magick. This results in faster image processing, automatic orientation, thumbnail sharpening and more.

Active Storage is able to extract metadata from uploaded images, but currently this means width and height only. One thing I’m missing so far is extracting GPS location (latitude / longitude / altitude) from the Exif part. The following describes how this feature can be added by using the gem exifr.

In Active Storage there are Analyzers to extract metadata - for every file type there is a separate analyzer - currently for images and videos only, but you can add your own. For images, there is the ImageAnalyzer. Because of the internal structure of this class the only way of enhancement seems to be Monkey Patching.

First, add this to your Gemfile:

gem 'exifr'

Then, add the file config/initializers/exif.rb with this content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
require 'exifr/jpeg'

module ActiveStorage
  class Analyzer::ImageAnalyzer < Analyzer
    def metadata
      read_image do |image|
        if rotated_image?(image)
          { width: image.height, height: image.width }
        else
          { width: image.width, height: image.height }
        end.merge(gps_from_exif(image) || {})
      end
    rescue LoadError
      logger.info "Skipping image analysis because the mini_magick gem isn't installed"
      {}
    end

    private

    def gps_from_exif(image)
      return unless image.type == 'JPEG'

      if exif = EXIFR::JPEG.new(image.path).exif
        if gps = exif.fields[:gps]
          {
            latitude:  gps.fields[:gps_latitude].to_f,
            longitude: gps.fields[:gps_longitude].to_f,
            altitude:  gps.fields[:gps_altitude].to_f
          }
        end
      end
    rescue EXIFR::MalformedImage, EXIFR::MalformedJPEG
    end
  end
end

That’s all. Now for every processed image the GPS location data (if there is any) will be extracted as metadata and stored to the database in the table active_storage_blobs.