Extract EXIF data using PHP to display GPS-tagged Images in Google Maps

While cycling around the city this past weekend with my girlfriend I shot a couple quick photos with my iPhone to play with some of the encoded EXIF/GPS data for an upcoming project I am working on. After creating some quick thumbnails, I built the following Google Maps prototype to display the images where I photographed them extracting the GPS data with a PHP script I have running on the server.

In the process of building this simple example I came across a strange phenomenon where the GPS data wasn’t being preserved if the original iPhone image was altered in any way. This was happening whenever I saved an edited version of the image in Photoshop CS4 including the creation of a thumbail. Even with the preserve metadata flag selected when saving for web.

This was especially annoying because I typically like to use the popular CameraBag app to easily treat my images before saving them on the phone. An explanation from their website suggests that Apple does not allow third party applications to preserve the EXIF data on save. Seriously? Not sure if this is true although a very quick search through Apple’s iPhone dev portal hasn’t yielded any suggestions one way or the other.

Nonetheless, Apple’s own photo editing application Aperture does preserve the EXIF data when exporting out a version however this is a less than optimal solution as I find cropping and exporting batch sequences of thumbnails to a be a clumsy and tedious operation in Aperture.

So what to do? After some searching for a solution I came across Phil Harvey’s impressive ExifTool which can read/write/copy image metadata across files ala the command line.

After going here and installing the package and adding it to my path I can run the following script and copy the GPS data from my source iPhone image1.jpg to my treated version or thumbnail image2.jpg.

exiftool -overwrite_original -tagsfromfile image1.jpg -gps:all image2.jpg

Awesome, and there are a whole list of tags, arguments, etc that you can pass into the tool all outlined in the documentation. One thing to note though is that I have been unsuccessful in copying over the data that describes when the source image was created & last modified. Having tried all of the following commands without success I’m still searching for a solution to copy the timestamp over. If anyone has any ideas I’d love to hear how you did it.

# none of these worked for me...
exiftool -overwrite_original -tagsfromfile image1.jpg -all:all image2.jpg
exiftool -overwrite_original -tagsfromfile image1.jpg -createdate image2.jpg
# attempt to alter the existing date by a few hours...
exiftool -overwrite_original -createdate+=3 image2.jpg
exiftool -overwrite_original -alldates+=1:30 image2.jpg

So after treating the images and banging out some thumbnails, I added the GPS data back in via the ExifTool and pushed everything up my server to try working with the metadata inside of Flash.

The larger project that I am building this prototype for will require the image data to come to Flash via a DB which is why I wrote up a PHP solution instead of reading the image headers right out of the jpeg file using the methods of the AS3 ByteArray.

In my test case here I am communicating with the backend using AMFPHP and wrote a simple service class that includes the main ExifReader class, collects all the EXIF data and sends the results back to Flash.

class AMFDirectoryReader{
 
   public function __construct()
   {
   // relative path to the ExifReader script //
     require_once '../../php/gmaps-gps-images/ExifReader.php';
   }    
 
   public function getEXIFImageData($args)
   {
     $target = $args[0];
     $params = explode(',', $args[1]);
 
     $exif = new ExifReader();
     if (is_file($target)) return $exif->readImage($target, $params);        
     if (is_dir($target)) return $exif->readDirectory($target, $params);
 
     return array('ERROR: Target Does Not Exist');
   }     
}

I can then call getEXIFImageData() from Flash using my RemotingCall class passing in the $target file or directory I want to read and an array of strings that describe what metadata tags I want to get back.

import flash.display.Sprite;
import com.quietless.remoting.RemotingCall;
import com.quietless.events.HTTPRequestEvent;
 
public class AppMain extends Sprite {
 
private var _rc	        :RemotingCall;
private var _params	:Array = ['gps', 'date', 'size'];
private var _target	:String = '../../php/gmaps-gps-images/thumbs';
 
   public function AppMain()
   {		
     _rc = new RemotingCall('http://quietless.com/amfphp/gateway.php');
     _rc.call('AMFDirectoryReader', 'getEXIFImageData', [_target, _params.join()]);
     _rc.addEventListener(HTTPRequestEvent.COMPLETE, onRequestComplete);
   }
 
   private function onRequestComplete(e:HTTPRequestEvent):void
   {
     var exif:Array = e.target.response;
   }
}

The real work happens inside of ExifReader where the target image or all images in a target directory are read using calls to exif_read_data(). Currently the class only supports the $params arguments ‘gps’, ‘date’, ‘size’ but if you take a look at what’s going on and reference the method’s man page you can easily extend this to send back what you need.

 
class ExifReader{
 
  public function __construct() { }
 
  /**
  * Returns an array of EXIF data objecta for all jpgs in the target directory 
  **/	
  public function readDirectory($dir, $props)
  {
  // $dir points to this directory from amfphp //
    $files = scandir($dir);
    $images = array();
  // loop over the files array and look for images
    for ($i=0; $i < count($files); $i++) { 
      if (stristr($files[$i], '.jpg')){
  // if the jpeg has gps data add it to the images array //	
      $img = $this->readImage($dir.'/'.$files[$i], $props);
      if ($img) array_push($images, $img);
      }
    }
    return $images;
  }
 
/**
* Returns the EXIF data object of a single image 
**/	
  public function readImage($image, $props)
  {
  $exif = exif_read_data($image, 0, true);		
  if ($exif){
    $data = array();
    $data['name'] = $exif['FILE']['FileName']; 
    foreach($props as $val)
    {				
  // return an array [lat, lng] //
      if ($val=='gps') $data['gps'] = $this->getGPS($image);
  // return date value in milliseconds //
      if ($val=='date') $data['date'] = $exif['FILE']['FileDateTime']*1000;
  // return size in kilobytes //	
      if ($val=='size') $data['size'] = floor(($exif['FILE']['FileSize'] / 1024 * 10 )/10)."KB";
    }
    return $data;
    }	else{
    return null;
    }
  }
 
/**
* Returns GPS latitude & longitude as decimal values
**/	
  private function getGPS($image)
  {
    $exif = exif_read_data($image, 0, true);
    if ($exif){
      $lat = $exif['GPS']['GPSLatitude']; 
      $log = $exif['GPS']['GPSLongitude'];
      if (!$lat || !$log) return null;
  // latitude values //
      $lat_degrees = $this->divide($lat[0]);
      $lat_minutes = $this->divide($lat[1]);
      $lat_seconds = $this->divide($lat[2]);
      $lat_hemi = $exif['GPS']['GPSLatitudeRef'];
 
  // longitude values //
      $log_degrees = $this->divide($log[0]);
      $log_minutes = $this->divide($log[1]);
      $log_seconds = $this->divide($log[2]);
      $log_hemi = $exif['GPS']['GPSLongitudeRef'];
 
      $lat_decimal = $this->toDecimal($lat_degrees, $lat_minutes, $lat_seconds, $lat_hemi);
      $log_decimal = $this->toDecimal($log_degrees, $log_minutes, $log_seconds, $log_hemi);
 
      return array($lat_decimal, $log_decimal);
      }	else{
      return null;
    }
  }
 
  private  function toDecimal($deg, $min, $sec, $hemi)
  {
    $d = $deg + $min/60 + $sec/3600;
    return ($hemi=='S' || $hemi=='W') ? $d*=-1 : $d;
  }
 
  private function divide($a)
  {
  // evaluate the string fraction and return a float //	
    $e = explode('/', $a);
  // prevent division by zero //
    if (!$e[0] || !$e[1]) {
      return 0;
    }	else{
    return $e[0] / $e[1];
    }
  }
}

At some point in the future I’d like to write an elegant way to load a batch of thumbnails and just extract the image metadata right from within Flash. This solution does allow you to read the image tags first though before you load them, ideal for querying a DB to collect a result set of images that meet a specific requirement say like falling within a coordinate boundary, before actually loading them into Flash.

As always downloads of the above classes.
Comment and suggestions are most welcome.