Brad Lucas

Programming, Clojure and other interests
September 21, 2019

Building An Image Location Service With Clojure

Recently I was building a small library to extract information from image files. While doing so I thought a simple site to extract GPS information from images would be useful to build as well as to describe how it was created using Clojure.

The source code for this project is available on GitHub at https://github.com/bradlucas/imagelocation while a running example is available at http://imagelocation.beaconhill.com/. If you don't have a photo handy and would quickly like to see what the site returns see the About page http://imagelocation.beaconhill.com/about.

Overview

I'll assume that you the reader have some experience with Clojure and have build something in the language. With that in mind I'll just go through the steps I went through to build up the application.

They are as follows:

  1. Create Initial Project
  2. Add Compojure
  3. Command line support and Jetty Adapter
  4. Process Image Files
  5. Adding Template and Selmer
  6. Final Version

Step 1. Create Initial Project

Let's start with a basic project.

$ lein new imagelocation

https://github.com/bradlucas/imagelocation/commit/0ccb62ab4996b85bcd31931e972b89b086fd3ade

Step 2. Add Compojure

Here we add the Compojure library and get a simple route working.

Add the following to the project.clj file's dependencies.

[compojure "1.6.1"]
[ring/ring-defaults "0.3.2"]   

Also, add the following to the project.clj file.

 :repl-options {:init-ns imagelocation.core}

  :plugins [[lein-ring "0.12.5"]]
  :ring {:handler imagelocation.handler/app}
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [ring/ring-mock "0.3.2"]]}}

Create a file called handler.clj add a require entries and an initial app-routes and app.

(ns imagelocation.handler
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))

Details: https://github.com/bradlucas/imagelocation/commit/9581ccdfc6d464b57fe165d6332160d51d1d3d02

Now test this step with:

$ lein ring server

Command line support and Jetty Adapter

Using lein ring server is fine to get started but we'll want to run a stand alone web app. Also, we'll need to run our system from the command line. To do this see the following commit:

https://github.com/bradlucas/imagelocation/commit/026ac4c4ab4fd0bc092f351e5766aef9d5c411ea

Here we are adding the following libraries.

  [ring/ring-jetty-adapter "1.7.1"]
  [org.clojure/tools.cli "0.4.2"]

The ring-jetty-adapter lets use run standalone with a statement like:

(jetty/run-jetty handler/app {:port 4002}

The org.clojure/tools.cli library lets us process command line arguments easily. See the above mentioned commit for details on how to check for a -f filename to process a single file.

Step 4 - Process Image Files

The following commit introduces the main functionality to extract location information from images.

See this commit:

https://github.com/bradlucas/imagelocation/commit/e39a49516f97c8f899b0d58e0ec0dc77a483cf78

The library the application is using is the metadata-extractor from Drew Noakes https://github.com/drewnoakes/metadata-extractor.

To start a reference to the library is added to the project.clj file.

[com.drewnoakes/metadata-extractor "2.12.0"]

Then see the image.clj file https://github.com/bradlucas/imagelocation/blob/e39a49516f97c8f899b0d58e0ec0dc77a483cf78/src/imagelocation/image.clj.

The main routine to extract the data is image-data.

(defn image-data
  "Return map of all image data fields
"
  [filename]
  (->> (io/file filename)
       ImageMetadataReader/readMetadata
       .getDirectories
       (map #(.getTags %))
       (into {} (map extract-from-tag))))

The output of which is passed to get-location-data to remove just the GPS fields.

(defn get-location-data
  "Return `GPS Latitude` and `GPS Longitude` values in a map
"
  [filename]
  (let [info (image-data filename)]
    {:lat (get info "GPS Latitude")
     :lng (get info "GPS Longitude")}))

There are some routines to convert the three field lat/lng value strings to single numbers in the file as well as a routine to create a Google Map link.

Step 5 - Adding Template and Selmer

The next to last step is to add a simple UI. I choose to use the Selmer libary for templating and a basic Bootstrap template from [Bootswatch])https://bootswatch.com/lux/).

The commit which introduces the files is https://github.com/bradlucas/imagelocation/commit/ce92691f84ee620cb5d353bd73441b3f3f398779.

Adding Selmer consists of adding the following to your project.clj file.

[selmer "1.12.12"]

Getting things setup is a bit more involved than previous steps. It might be worth while looking carefully over the above mentioned commit. What you'll need is the following:

  • A resources/templates directory with a base.html file along with files for each view
  • The Bootstrap template files in resources/public
  • Modifications to your handlers to display the upload form and process the posted upload

If you are following along and building as you go focus on getting a static page working first. This means the base.html and index.html files working with the associated public files.

Then focus on the handlers to process the upload. This may be tricky at first but review the repo for the solution.

Step 6 - Final Version

The last step is after many tweaks to make the system more robust and look better. Feel free to step through the commits to see the details.

The final version as of this writing is version 1.0 and available on this branch https://github.com/bradlucas/imagelocation/tree/release/1.0.

Also, a running version to try is availabe at http://imagelocation.beaconhill.com/

Tags: ring example clojure compojure