Image Geotagger


Ladoga lake, Karelia, Russia. Open in new window.

I have been using GPSPhotoLinker for years. I become bored recently with its dependance on old Perl libraries which were no longer shipped with OS X. Some workaround suggested on manufacturer's forum helped to deal with problem for the next two OS X releases. Nevertheless, GPSPhotoLinker started to crash with Fujifilm X100T RAF files and with large Canon CR2s as well.

Internally GPSPhotoLinker uses Exiftool to complete geotagging tasks. So I decided to develop my own solution which would essentially run Exiftool console utility with carefully adjusted parameters.

Geotagger context menu

The solution is made as OS X workflow and appears in Finder context menu. So it's very easy to georeference your fresh or old images. You have not even to remember the app name :)
Basically, it is straightforward and self-explanatory, however, I'd comment on the workflow logic.

Geotagger initial options

When started, script asks for a geotagging mode. You have two options, to sync files with a GPS track recorded with your phone of standalone GPS receiver, or enter coordinates manually.

Geotagger selecting GPS track files

The first option is as easy as selecting one or several track files (think of a few consecutive shooting days).

Geotagger specifying time difference

Then you are suggested an option to specify the time shift between GPS device and your camera. Still, it's better to set the correct time before shooting.

Geotagger manual entry of coordinates

The second option is to specify coordinates manually for photos taken when your device was off or received no signal (in a cave or canyon). The script will pick up your clipboard text item or use previously stored data after the first run.

Geotagger specify country Geotagger specify state or region Geotagger specify city Geotagger specify sublocation

Then in both cases you have an option to specify the following XMP geolocation data: country, state/province, city and sublocation. It's up to you to ignore (leave the field empty) any value, so it will not be embedded in the images. Useful for cases when your photographs were made within the same country and region, but at different locations. Again, these data are stored for next runs.

Previous run data are stored in standard OS X Preference List file here:
~/Library/Preferences/com.pozhvanov.geotagger.plist

When received user data, script runs Exiftool on selected files, and then shows a notification with statistics. In addition, it checks for every file whether that one had geo-coordinates recorded and XMP location data embedded. If a given file contains both, it is marked with green label. If it has either coordinates or text location data only, it is marked with yellow or grey label, respectively. That allows you to easily identify files that require further attention after automated run.

You are free to download the Image Geotagger script in form of ZIP archive:

Geotag_images.workflow.zip
642 KB.

Unarchive it and save to:
~/Library/Services/

You can enable or disable Finder context menu item under  > System Preferences > Keyboard > Shortcuts > Services. Happy geotagging!

Source code

For your safety, here is the full AppleScript source code for Image Geotagger workflow:
property preferences_file : "com.pozhvanov.geotagger.plist"
property preferences_path : "~/Library/Preferences/"
property exiftool_path : "/usr/local/bin/exiftool"
to SelectInputGPX(title, folder_location)
  set InputGPX to choose file with prompt title default location folder_location with multiple selections allowed
  return InputGPX as list
end SelectInputGPX
on replace_chars(this_text, search_string, replacement_string)
  set AppleScript's text
item delimiters
to the search_string
  set the item_list to every text item of this_text
  set AppleScript's text item delimiters to the replacement_string
  set this_text to the item_list as string
  set AppleScript's text item delimiters to ""
  return this_text
end replace_chars
to checkPreferences()
  tell application "System Events"
    if exists file preferences_file of (path to preferences from user domain) then
      return 1
    else
      return 0
    end if
  end tell
end checkPreferences
on run {input, parameters}
  if user locale of (get system info) starts with "ru" then
    set lang to "ru"
    set msg_coord_src to "Выберите источник координат:"
    set msg_coord_src_gpx to "Файл GPX"
    set msg_coord_src_man to "Ввести вручную"
    set msg_select_gpx to "Выберите файл(ы) трека GPX/KML/XML/TCX:"
    set msg_enter_coords to "Введите координаты через пробел (широта долгота высота).
Для ввода координат в западном или южном полушарии используйте знак -.
Значение высоты можно опустить.
Десятичный разделитель – точка.
Чтобы только геокодировать файлы, оставьте поле пустым."
    set msg_enter_coords_title to "Ввод координат вручную"
    set msg_button_next to "Далее"
    set msg_button_cancel to "Отмена"
    set msg_gpx_offset to "Укажите сдвиг времени трека относительно съёмки, если необходимо (HH:MM:SS)"
    set msg_gpx_offset_title to "Сдвиг по времени"
    set msg_enter_country to "Введите название страны:"
    set msg_enter_country_title to "Страна"
    set msg_enter_state to "Введите название региона или штата:"
    set msg_enter_state_title to "Регион/штат/провинция"
    set msg_enter_city to "Введите название города:"
    set msg_enter_city_title to "Город"
    set msg_enter_sublocation to "Введите название локации:"
    set msg_enter_sublocation_title to "Локация"
    set msg_result_1 to "Запись геоданных завершена. Обработано файлов: "
    set msg_result_2 to " Сообщение exiftool: "
  else
    set lang to "en"
    set msg_coord_src to "Select coordinates source:"
    set msg_coord_src_gpx to "GPX file"
    set msg_coord_src_man to "Enter manually"
    set msg_select_gpx to "Select GPX/KML/XML/TCX track file(s):"
    set msg_enter_coords to "Enter coordinates delimited by space (latitude longitude
altitude).
Use - sign to enter coordinates for western or southern hemispheres.
Altitude value could be omitted.
Decimal delimiter is dot.
To only geocode files, leave this field empty."
    set msg_enter_coords_title to "Manual entry of coordinates"
    set msg_button_next to "Next"
    set msg_button_cancel to "Cancel"
    set msg_gpx_offset to "Specify time difference of track to photos, if necessary
(HH:MM:SS)"
    set msg_gpx_offset_title to "Time difference"
    set msg_enter_country to "Enter the Country name:"
    set msg_enter_country_title to "Country"
    set msg_enter_state to "Enter the name of State or Region:"
    set msg_enter_state_title to "State/Region/Province"
    set msg_enter_city to "Enter the City name:"
    set msg_enter_city_title to "City"
    set msg_enter_sublocation to "Enter the Sublocation name:"
    set msg_enter_sublocation_title to "Sublocation"
    set msg_result_1 to "Geodata recording is completed. "
    set msg_result_2 to "
files were processed.
Exiftool output: "
  end if
  set xml_gpx_path to POSIX path of (path to documents folder) as text
  set xml_latitude to ""
  set xml_longitude to ""
  set xml_altitude to ""
  set xml_timeDifference to ""
  set xml_country to ""
  set xml_state to ""
  set xml_city to ""
  set xml_sublocation to ""
  (*  Read preferences from Preferences File  *)
  if checkPreferences() is equal to 1 then
    set the geotag_plist to preferences_path & preferences_file
    tell application "System Events"
      tell property list file geotag_plist
        tell contents
          if exists property list item "gpx_path" then
            set xml_gpx_path to value of property list item "gpx_path"
          end if
          if exists property list item "latitude" then
            set xml_latitude to value of property list item "latitude"
          end if
          if exists property list item "longitude" then
            set xml_longitude to value of property list item "longitude"
          end if
          if exists property list item "altitude" then
            set xml_altitude to value of property list item "altitude"
          end if
          if exists property list item "timeDifference" then
            set xml_timeDifference to value of property list item "timeDifference"
          end if
          if exists property list item "country" then
            set xml_country to value of property list item "country"
          end if
          if exists property list item "state" then
            set xml_state to value of property list item "state"
          end if
          if exists property list item "city" then
            set xml_city to value of property list item "city"
          end if
          if exists property list item "sublocation" then
            set xml_sublocation to value of property list item "sublocation"
          end if
        end tell
      end tell
    end tell
  end if
  set coord_src to display dialog msg_coord_src buttons {msg_coord_src_gpx, msg_coord_src_man} default button msg_coord_src_gpx
  if (button returned of coord_src as string) is equal to msg_coord_src_gpx then
    set gpx_file to SelectInputGPX(msg_select_gpx, xml_gpx_path)
    (*
    if ((time to GMT) / hours > 0) then
      set gpx_offset to "-" & (round
((time to GMT) / hours)) & ":00:00"

    else
      set gpx_offset to "+" & (round
(get (characters 2 thru length of (round (time to GMT))) / hours) &
":00:00")

    end if
    *)
    tell application "Finder"
      set file1 to (item 1 of (gpx_file as list))
      set folder1 to (container of file1) as string
    end tell
    set xml_gpx_path to (POSIX path of folder1) as text
    --    display notification xml_gpx_path
    set gpx_offset to xml_timeDifference
    set gpx_offset to text returned of (display dialog msg_gpx_offset with title msg_gpx_offset_title default answer gpx_offset buttons {msg_button_next, msg_button_cancel} default button msg_button_next cancel button msg_button_cancel)
    set xml_timeDifference to gpx_offset as text
  else
    if (length of xml_latitude < 1 and length of xml_longitude < 1) then
      if (length of (get the clipboard as text)) > 30 then
        set msg_clipboard to ((get characters 1 thru 30 of (get the clipboard as string)) as text)
      else
        set msg_clipboard to get the clipboard as text
      end if
    else
      set msg_clipboard to xml_latitude & " " & xml_longitude & " " & xml_altitude
    end if
    set coord_manual to text returned of (display dialog msg_enter_coords with title msg_enter_coords_title default answer replace_chars(msg_clipboard, ",", "") buttons {msg_button_next, msg_button_cancel} default button msg_button_next cancel button msg_button_cancel)
  end if
  set select_Country to text returned of (display dialog msg_enter_country with title msg_enter_country_title default answer xml_country buttons {msg_button_next, msg_button_cancel} default button msg_button_next cancel button msg_button_cancel)
  set select_State to text returned of (display dialog msg_enter_state with title msg_enter_state_title default answer xml_state buttons {msg_button_next, msg_button_cancel} default button msg_button_next cancel button msg_button_cancel)
  set select_City to text returned of (display dialog msg_enter_city with title msg_enter_city_title default answer xml_city buttons {msg_button_next, msg_button_cancel} default button msg_button_next cancel button msg_button_cancel)
  set select_Sublocation to text returned of (display dialog msg_enter_sublocation with title msg_enter_sublocation_title default answer xml_sublocation buttons {msg_button_next, msg_button_cancel} default button msg_button_next cancel button msg_button_cancel)
  set xmp_Country to select_Country as text
  set xmp_State to select_State as text
  set xmp_City to select_City as text
  set xmp_Sublocation to select_Sublocation as text
  set xml_country to xmp_Country
  set xml_state to xmp_State
  set xml_city to xmp_City
  set xml_sublocation to xmp_Sublocation
  set CommandString to exiftool_path & " -overwrite_original "
  --set CommandString to "exiftool -overwrite_original "
  if (button returned of coord_src as string) is equal to msg_coord_src_gpx then
    if length of gpx_offset as text is greater than 0 then
      set CommandString to (CommandString & "-geosync=\"" & gpx_offset as text) & "\" "
    end if
    if (count of gpx_file) as list is greater than 1 then
      repeat with i in gpx_file as list
        set CommandString to CommandString & "-geotag \"" & POSIX path of (i as string) & "\" "
      end repeat
    else
      set CommandString to CommandString & "-geotag \"" & POSIX path of (gpx_file as string) & "\" "
    end if
  else
    set coord_manual to replace_chars(coord_manual as text, ", ", " ")
    if length of coord_manual > 0 then
      set AppleScript's text item delimiters to " "
      set the coord_list to every text item of coord_manual
      if ((count of coord_list) as list) is greater than 2 then
        set CommandString to (((CommandString & "-exif:GPSLatitude=\"" & (item 1 of coord_list) as text) & "\" -exif:GPSLongitude=\"" & (item 2 of coord_list) as text) & "\" -exif:GPSAltitude=\"" & (item 3 of coord_list) as text) & "\" "
        set xml_latitude to (item 1 of coord_list) as text
        set xml_longitude to (item 2 of coord_list) as text
        set xml_altitude to (item 3 of coord_list) as text
      else
        set CommandString to ((CommandString & "-exif:GPSLatitude=\"" & (item 1 of coord_list) as text) & "\" -exif:GPSLongitude=\"" & (item 2 of coord_list) as text) & "\" "
        set xml_latitude to (item 1 of coord_list) as text
        set xml_longitude to (item 2 of coord_list) as text
      end if
    end if
  end if
  if length of xmp_Country is greater than 0 then
    set CommandString to CommandString & "-xmp:Country=\"" & xmp_Country & "\" "
  end if
  if length of xmp_State is greater than 0 then
    set CommandString to CommandString & "-xmp:State=\"" & xmp_State & "\" "
  end if
  if length of xmp_City is greater than 0 then
    set CommandString to CommandString & "-xmp:City=\"" & xmp_City & "\" "
  end if
  if length of xmp_Sublocation is greater than 0 then
    set CommandString to CommandString & "-xmp:Sublocation=\"" & xmp_Sublocation & "\" "
  end if
  repeat with i in input as list
    set CommandString to CommandString & "\"" & POSIX path of (i as text) & "\" "
  end repeat
  set ScriptResult to do shell script CommandString
  (*  Color label files depending on their geotagging completeness  *)
  repeat with i in input as list
    set counter to 0
    set exifstatus to (do shell script exiftool_path & " -exif:GPSLatitude -exif:GPSLongitude -xmp:Country -xmp:State -xmp:City -xmp:Sublocation \"" & POSIX path of (i as text) & "\"") as text
    if (exifstatus contains "GPS Latitude") and (exifstatus contains "GPS Longitude") then
      set counter to counter + 2
    end if
    if (exifstatus contains "Country") or (exifstatus contains "State") or (exifstatus contains "City") then
      set counter to counter + 1
    end if
    if counter is equal to 3 then
      --green label
      tell application "Finder"
        set thefile to (i as alias)
        set label index of thefile to 6
      end tell
    else if counter is equal to 2 then
      --yellow label
      tell application "Finder"
        set thefile to (i as alias)
        set label index of thefile to 3
      end tell
    else if counter is equal to 1 then
      --grey label
      tell application "Finder"
        set thefile to (i as alias)
        set label index of thefile to 7
      end tell
    end if
  end repeat
  (*  Save preferences in Preferences File  *)
  tell application "System Events"
    set geotag_dictionary to make new property list item with properties {kind:record}
    set geotag_plist to preferences_path & preferences_file
    set this_plist to make new property list file with properties {contents:geotag_dictionary, name:geotag_plist}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"gpx_path", value:xml_gpx_path}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"latitude", value:xml_latitude}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"longitude", value:xml_longitude}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"altitude", value:xml_altitude}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"timeDifference", value:xml_timeDifference}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"country", value:xml_country}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"state", value:xml_state}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"city", value:xml_city}
    make new property list item at end of property list items of contents of this_plist with properties {kind:string, name:"sublocation", value:xml_sublocation}
  end tell
  display notification ((msg_result_1 & ((count of input) as list) as text) & msg_result_2 & ScriptResult as text) with title "Geotagger" sound name "Submarine"
  return ScriptResult
end run

Leave a Reply