Geo exif data in WordPress

Did you know that WordPress imports EXIF data from your images when they’re uploaded?

If your image contains certain information (aperture, shutter speed, ISO, focal length, camera, creation time, credit, copyright, caption, title), it is in the database and you can use it as I do on my photoblog, Matt Mullenweg uses on his photo pages, and as described on Blogging Tips in WordPress Gallery and EXIF and Mixing Gallery and Blog Posts.

I’ve been thinking for a while that it would be cool to geotag images and show their location on a map. I finally got around to doing it (with some help from my husband, thanks honey!). Here’s how…

EXIF location on map

Before I get started, this post is PHP heavy and I’m not really going to explain the elements it uses (otherwise it would take me an age to write, and you just as long to read). Other than the posts I’ll link to, tizag.com has a good introductory PHP tutorial.

Geotagging

There’s a few different devices that you can connect to your camera to add GPS data to your photos and some cameras can do it themselves. I found an Aperture plugin, Maperture, which allows you to add the location of your images via Google maps.

I found the EXIF specifications and discovered the EXIF classes to get a map location are GPSLatitude and GPSLongitude (which store degrees, minutes and seconds), and GPSLatitudeRef and GPSLongitudeRef (which store N or S and E or W).

Importing EXIF data to the WordPress database

The information in this section is better handled by a function in a more recent post Add EXIF metadata to WordPress. Unlike what’s outlined below, you don’t have to change core WordPress files with the updated method. The rest of this post is up to date, though.

The file that determines which information is imported from images is wp-admin/includes/image.php. At line 231 (in WP 2.7) is a function called wp_read_image_metadata. To get WP to import the GPS data I added some values to the array:

	$meta = array(
		'aperture' => 0,
		'credit' => '',
		'camera' => '',
		'caption' => '',
		'created_timestamp' => 0,
		'copyright' => '',
		'focal_length' => 0,
		'iso' => 0,
		'shutter_speed' => 0,
		'title' => '',
	);

became,

	$meta = array(
		'aperture' => 0,
		'credit' => '',
		'camera' => '',
		'caption' => '',
		'created_timestamp' => 0,
		'copyright' => '',
		'focal_length' => 0,
		'iso' => 0,
		'latitude' => 0,
		'latitude_ref' => '',
		'longitude' => 0,
		'longitude_ref' => '',
		'shutter_speed' => 0,
		'title' => '',
	);

Then I had to tell WordPress which EXIF data is to be added to the database as latitude, longitude and the references.

After the lines,

if (!empty($exif['ISOSpeedRatings']))
	$meta['iso'] = $exif['ISOSpeedRatings'];

I added,

if (!empty($exif['GPSLatitude']))
	$meta['latitude'] = $exif['GPSLatitude'] ;
if (!empty($exif['GPSLatitudeRef']))
	$meta['latitude_ref'] = trim( $exif['GPSLatitudeRef'] );
if (!empty($exif['GPSLongitude']))
	$meta['longitude'] = $exif['GPSLongitude'] ;
if (!empty($exif['GPSLongitudeRef']))
	$meta['longitude_ref'] = trim( $exif['GPSLongitudeRef'] );

It really just copies the method used for the other EXIF fields.

GPS EXIF format

The difference between the latitude and longitude EXIF, compared to the others (e.g., aperture and shutter speed), is that GPSLatitude and GPSLongitude are stored as 3 rationals, or 3 fractions, which represent degrees, minutes and seconds. WordPress has a function to convert fractions to decimals for the database, but because these have an array of three fractions, it was a little more complicated.

I decided to import them as fractions and divide them out later.

This is what the EXIF (and IPTC) info from the MySQL database looks like,

"image_meta";a:14:{s:8:"aperture";s:1:"8";s:6:"credit";s:0:"";s:6:"camera";s:9:"NIKON D50";s:7:"caption";s:0:"";s:17:"created_timestamp";s:10:"1228061663";s:9:"copyright";s:0:"";s:12:"focal_length";s:2:"50";s:3:"iso";s:3:"200";s:8:"latitude";a:3:{i:0;s:4:"33/1";i:1;s:8:"5237/100";i:2;s:3:"0/1";}s:12:"latitude_ref";s:1:"S";s:9:"longitude";a:3:{i:0;s:5:"151/1";i:1;s:7:"906/100";i:2;s:3:"0/1";}s:13:"longitude_ref";s:1:"E";s:13:"shutter_speed";s:5:"0.004";s:5:"title";s:0:"";}

Unless you’re familiar with viewing database info, it may look like a bunch of jibberish at first, but if you look through, you can see that latitude has the values 33/1, 5237/100 and 0/1, and latitude_ref is S. So, the latitude of where that photo was taken is 33° 52.37′ 0″ S. Likewise for the longitude, which is 151° 9.06′ 0″ E.

Calling the metadata from the database

As shown in WordPress Gallery and EXIF, you can get the information in the database using
$imgmeta = wp_get_attachment_metadata( $imgid );
$imgmeta['image_meta']['shutter_speed']
, for example.

If I just use that for latitude and longitude, it returns the word “array” because the values are stored as an array. So, I converted the individual array values into slightly friendlier decimals.

I’m using the Thesis theme, so the following goes in my custom_functions.php file. If you’re using another theme you could put it in your theme’s functions.php (if your theme doesn’t have one, create one).

I copied the exif_frac2dec function from image.php (I’m not sure why, but trying to use that function from custom_functions.php didn’t work), and then applied the function to each value of the arrays.

function geo_frac2dec($str) {
	@list( $n, $d ) = explode( '/', $str );
	if ( !empty($d) )
		return $n / $d;
	return $str;
}

function geo_pretty_fracs2dec($fracs) {
	return	geo_frac2dec($fracs[0]) . '° ' .
		geo_frac2dec($fracs[1]) . '′ ' .
		geo_frac2dec($fracs[2]) . '″ ';
}

Then, in the loop that calls the exif info from the database, after the bit that says $imgmeta = wp_get_attachment_metadata( $imgid );, I defined where the latitude and longitude info comes from and ran it through the geo_pretty_fracs2dec function.

$imgmeta = wp_get_attachment_metadata( $imgid );
			
$latitude = $imgmeta['image_meta']['latitude'];
$longitude = $imgmeta['image_meta']['longitude'];
$lat_ref = $imgmeta['image_meta']['latitude_ref'];
$lng_ref = $imgmeta['image_meta']['longitude_ref'];

Using geo_pretty_fracs2dec($latitude). $lat_ref . ' ' . geo_pretty_fracs2dec($longitude) . $lng_ref outputs something that looks like 33° 52.37′ 0″ S 151° 9.06′ 0″ E.

Google uses a full decimal latitude and longitude; you need to divide minutes by 60 and seconds by 3600 and add them all together. Google also uses negative values to determine South and West.

The following function extracts the array values, divides them accordingly, and adds them together.

function geo_single_fracs2dec($fracs) {
	return	geo_frac2dec($fracs[0]) +
		geo_frac2dec($fracs[1]) / 60 +
		geo_frac2dec($fracs[2]) / 3600;
}

After the other variable definitions from before,

$lat = geo_single_fracs2dec($latitude);
$lng = geo_single_fracs2dec($longitude);
if ($lat_ref == 'S') { $neg_lat = '-'; } else { $neg_lat = ''; }
if ($lng_ref == 'W') { $neg_lng = '-'; } else { $neg_lng = ''; }

The whole list item to call the location reference and link to the map looks like,

if ($latitude != 0 && $longitude != 0) echo '<li>Location: <a href="http://maps.google.com/maps?q=' . $neg_lat . number_format($lat, 6) . '+' . $neg_lng . number_format($lng, 6) . '&z=11">' . geo_pretty_fracs2dec($latitude). $lat_ref . ' ' . geo_pretty_fracs2dec($longitude) . $lng_ref . '</a></li>';

Where the if != 0 says to only do this stuff if the location is not equal to zero, because there’s no point sending people to the South Atlantic Ocean. Would be a problem if that’s where you actually were though… The z=11 bit is the zoom on the map, play around with it until it suits you. Everything else is PHP or stuff I mentioned before, but feel free to ask what stuff does if you don’t understand it.

This will work for other maps, such as Yahoo, you just need to find out what the URL structure for the other map is.

EXIF location displayed

That’s it. My photoblog EXIF uses a combination of the above, the code at Blogging Tips and also some from A Moment of EnLitenment to show shutter speed as fractions.

Have a look at my complete EXIF code to see it all combined, and if you made it to the end of this post, congratulations!

Comments

  1. says

    This sounds and looks amazing! This is something that I will keep in the back of my mind for any future projects that may require a solution like this.

    Thanks.

  2. says

    Cheers Max! Yeah, it’s really one of those back-burner things, but I thought it would be useful for anyone who searches for this sort of thing. :)

  3. Ed says

    I have the new image.php page getting the new geo data. I confirmed it is going into the database when I upload an image as well.

    Quesion, can this work by just posting an image in a blog post and not using the Gallery option?

  4. says

    Ed, you have to upload the image using WordPress’ image uploader to get the data into the database. If you want to show the image and its data in a post it needs to be attached to the post, which is done either by uploading with that post or navigating the media library while editing a post and attaching it to that post. The way I retrieve info in my photoblog is via the attachments of the photoblog posts.
    Hope that answers your question.

  5. Sean says

    Great tutorial. One question though… do you know of any way to add this exif data to the caption of an image in a regular post? I found that the caption gets built in wp-includes/media.php, but can’t figure out how to add your exif info.

  6. says

    Sean — The caption code has a filter built in wp-includes/media.php at line 545.

    I was able to replace the image caption with the following code in my functions.php

    function geo_frac2dec($str) {
    	@list( $n, $d ) = explode( '/', $str );
    	if ( !empty($d) )
    		return $n / $d;
    	return $str;
    }
    function geo_pretty_fracs2dec($fracs) {
    	return	geo_frac2dec($fracs[0]) . '° ' .
    		geo_frac2dec($fracs[1]) . '′ ' .
    		geo_frac2dec($fracs[2]) . '″ ';
    }
    
    function coords() {
    	global $post;
    		$args = array(
    			'post_type' => 'attachment',
    			'numberposts' => -1,
    			'post_status' => null,
    			'post_parent' => $post->ID
    		);
    		$attachments = get_posts($args);
    		foreach ($attachments AS $attachment) {
    			$imgid = $attachment->ID;
    			$imgmeta = wp_get_attachment_metadata( $imgid );
    			
    			$latitude = $imgmeta['image_meta']['latitude'];
    			$longitude = $imgmeta['image_meta']['longitude'];
    			$lat_ref = $imgmeta['image_meta']['latitude_ref'];
    			$lng_ref = $imgmeta['image_meta']['longitude_ref'];
    
    if ($latitude != 0 && $longitude != 0) return geo_pretty_fracs2dec($latitude). $lat_ref . ' ' . geo_pretty_fracs2dec($longitude) . $lng_ref;
    		}
    }
    
    function add_to_caption($deprecated, $attr, $content) {
    $coords = coords();
    
    	return '<div ' . $id . 'class="wp-caption ' . $align . '" style="width: ' . (10 + (int) $width) . 'px">'
    	. $content . '<p class="wp-caption-text">' . $caption . $coords . '</p></div>';
    }
    add_filter('img_caption_shortcode', 'add_to_caption', 10, 3);

    I left out the link to Google maps, but you could easily add back that code from the post. I couldn’t get it to add the coordinates instead of replacing the whole caption. If I manage to do that I’ll let you know.

  7. Sean says

    Excellent! Thanks for helping me get on the right track. I dropped in your code and was able to see why the caption wasn’t showing up. None of the $attr attributes were actually making it into your new return statement. So, I took the code above the return statement in the media.php and added it above your return statement – then it worked…

    I added this above your return statement…


    extract(shortcode_atts(array(
    'id' => '',
    'align' => 'alignnone',
    'width' => '',
    'caption' => ''
    ), $attr));

    if ( 1 > (int) $width || empty($caption) )
    return $content;

    if ( $id ) $id = 'id="' . $id . '" ';

  8. says

    Sean — nice! I wondered about that. Some of the combos I tried removed the image as well, so I thought the attrs must have been working, but I guess not. ;)
    Thanks for the question. It was a learning experience for me!

  9. says

    Thanks very much for sharing this! Although I have a light programming background, I’m unlikely to try this myself unless it gets added to a plugin (or becomes one itself). I geotag most of my out-of-home photos, and that little bit of information I’ve always wanted to share with others. Having the link to google maps is great too – but in my mind I want it to go one step further.

    Similar to what Google’s own Picasa Web Albums does, put a thumbnail on the world map. It would be fantastic to have a small little map showing exactly where the photo was taken, embedded beside or below the image itself.

    Another useful extension of this idea would be to somehow query google maps or a similar service to provide town/city, province/state, and country information. GPS info is a little cryptic to look at just off the bat, imagine it says this instead: Location: NSW, Australia (33° 52.37? 0? S 151° 9.06? 0? E).

    Food for thought, keep up the great work!

  10. says

    Hey Don,

    I’ve considered making it into a plugin, it’s definitely possible. I’m not too good with plugins though. I guess I need some practice to learn them better. Perhaps if I have time. :)

    Yeah, the map is a good idea, I just didn’t have time, or necessarily the desire, to implement it. It has been done with the code (or inspiration) from this tute though at sleepingmonkey.co.uk.

    Cheers!

  11. Sean says

    I’ve got this working, but I’ve noticed that it doesn’t work if there’s more than 1 photo in a post. See this example…. http://seanpopke.com/blog/?p=443

    Photos that have a blue globe in the caption indicate geotag information (which you can click on the bring up a map)

    In this post, only the first and last photos actually have geotag information, but all photos are taking on the geotag information of the last photo. Any ideas on how to make this work with multiple photos in one post?

    Thanks!!!

  12. says

    Sean — It’s not only that they’re all taking on the location data, but they’re all taking on data from the last image in San Francisco.

    I think it’s most likely a problem with the loop needed to call the image meta. I always wondered what I would do if I wanted to post more than one photo to a photoblog post! I’m can’t promise I’ll be able to help, but if I am able to help I think I’ll need to see your code regarding the images and metadata. Can you post it here or in a pastebin?

  13. Sean says

    Sure, here is the code I’m using in my functions.php file. Let me know if you’d like to see anything else.


    function geo_frac2dec($str) {
    @list( $n, $d ) = explode( '/', $str );
    if ( !empty($d) )
    return $n / $d;
    return $str;
    }
    function geo_pretty_fracs2dec($fracs) {
    return geo_frac2dec($fracs[0]) . '° ' .
    geo_frac2dec($fracs[1]) . '? ' .
    geo_frac2dec($fracs[2]) . '? ';
    }

    function geo_single_fracs2dec($fracs) {
    return geo_frac2dec($fracs[0]) +
    geo_frac2dec($fracs[1]) / 60 +
    geo_frac2dec($fracs[2]) / 3600;
    }

    function coords() {
    global $post;
    $args = array(
    'post_type' => 'attachment',
    'numberposts' => -1,
    'post_status' => null,
    'post_parent' => $post->ID
    );
    $attachments = get_posts($args);
    foreach ($attachments AS $attachment) {
    $imgid = $attachment->ID;
    $imgmeta = wp_get_attachment_metadata( $imgid );

    $latitude = $imgmeta['image_meta']['latitude'];
    $longitude = $imgmeta['image_meta']['longitude'];
    $lat_ref = $imgmeta['image_meta']['latitude_ref'];
    $lng_ref = $imgmeta['image_meta']['longitude_ref'];
    $lat = geo_single_fracs2dec($latitude);
    $lng = geo_single_fracs2dec($longitude);
    if ($lat_ref == 'S') { $neg_lat = '-'; } else { $neg_lat = ''; }
    if ($lng_ref == 'W') { $neg_lng = '-'; } else { $neg_lng = ''; }

    if ($latitude != 0 && $longitude != 0) {
    return ' ';
    }
    else {

    return '';
    }
    }
    }

    function add_to_caption($deprecated, $attr, $content) {
    $coords = coords();

    extract(shortcode_atts(array(
    'id' => '',
    'align' => 'alignnone',
    'width' => '',
    'caption' => ''
    ), $attr));

    if ( 1 > (int) $width || empty($caption) )
    return $content;

    if ( $id ) $id = 'id="' . $id . '" ';

    return ''
    . $content . '' . $coords . '' . $caption . '';

    }
    add_filter('img_caption_shortcode', 'add_to_caption', 10, 3);

  14. says

    Sean — the only idea I had was to try to condense the functions together so that you’re using the same ID when you call the location data and rewrite the caption, but I didn’t get it to work with the playing that I did. I’ve gotta get back to work though. Let me know if you make any progress and I’ll have another look if I get the chance.

  15. says

    Kristarella,

    Thank you so much for this post. It is incredibly informative and easy to follow. I’ve managed to implement this code in less than an hour and it works perfectly.

    Cheers!

  16. says

    Thanks for the feedback Adam! Glad you found it useful. I’m hoping to make a light plugin to get WordPress to extract the extra EXIF so we don’t have to edit image.php every time WordPress upgrades, haven’t quite got it going yet though.

  17. says

    I’m pretty clueless. I got the code to add exit data from the image placed in my custom_functions.php file (I’m using Thesis 1.5). I know PHP, I just don’t know WordPress.
    I added this hook:
    add_action(‘thesis_hook_after_post’, ‘insert_exif’);
    but it produces strange results.

  18. says

    Khürt — Looks like a normal result to me if you have three images attached to that post. I only post one image at a time and have one image attached to each photoblog post so I set the numberposts in the custom query for the attachments to 1.

  19. says

    Thanks for the tips. Well done for putting it all together! Saved me a few hours of work on getting the data out. Did you work out a way to put multiple points on google maps?

  20. says

    Adam — I haven’t tried to put multiple points on a map, it may just be a mater of figuring out the URL to do so.

    You might be interested in my plugin Thesography which enables the addition of geo data to the database upon image upload and makes it easier to display EXIF in posts.

  21. Del Putnam says

    Hi there,

    I’ve been tinkering with trying to extract GPS EXIF data for a few days and have hit a brick wall. I thought that maybe I might be able to borrow some of the brainpower expressed here :-)

    I’m using a test image that I know has GPS EXIF data included in it. I can use my mac’s preview app to display it. I upload it to wordpress and try to extract it, but there is no GPS data in the results I get. I’m baffled.

    Here is the function call I’m using to extract the EXIF data:

    $exif = @exif_read_data( $_FILES['media']['tmp_name'], 0, true );

    Here are the contents of the $exif variable after this call:

    exif: Array
    (
    [FILE] => Array
    (
    [FileName] => php35zLOg
    [FileDateTime] => 1269514888
    [FileSize] => 129219
    [FileType] => 2
    [MimeType] => image/jpeg
    [SectionsFound] => ANY_TAG, IFD0, EXIF
    )

    [COMPUTED] => Array
    (
    [html] => width=”700″ height=”525″
    [Height] => 525
    [Width] => 700
    [IsColor] => 1
    [ByteOrderMotorola] => 1
    )

    [IFD0] => Array
    (
    [Orientation] => 1
    [Exif_IFD_Pointer] => 38
    )

    [EXIF] => Array
    (
    [ColorSpace] => 1
    [ExifImageWidth] => 700
    [ExifImageLength] => 525
    )

    )

    Any hints would be greatly appreciated.

    Thanks for all of the great pictures and posts, Kristarella!

  22. says

    Del — Did you see my more recent post with a filter function to add GEO exif to WP database? It piggy-backs on what WordPress is already doing to grab the latitude and longitude, you could probably add elevation in there to if you needed it.

    I’m not really sure what you’re doing with the code you showed, or how it works, so I can’t help you with that specifically, but I know my filter works.

    I also wrote a plugin called Thesography that includes the geo filter and provides a shortcode or function to display the EXIF in posts.

  23. Del Putnam says

    Hi Kristarella,

    Thanks so much for responding. After running some more tests last night, I’ve solved my problem…sort of.

    I have seen your filter and your great plugin. In fact, it was your Thesography plugin that inspired my current project. I’m trying to display geo data for images that are posted via Tweetie or the WordPress for iPhone client.

    What I’ve figured out is that while the images that the iPhone takes do have GPS EXIF data included in them, it appears that both Tweetie and the WordPress for iPhone clients strip the GPS data (and most of the other EXIF data) from the images before they are uploaded.

    So, I’m back to square one :-) I’m going to see if I can come up with a way to provide the GPS data through some other channel. (If anyone else who reads this has any ideas, I’m open to them.)

    Thanks again for responding and for all of the greatness here on your blog. This is one of my consistent favorite spots on the internet :-)

  24. says

    Del — Thank you for the compliments. I am honoured that my site is one of your favourites!

    Regarding iPhone imaging and EXIF, sadly Apple hasn’t made EXIF available in their API so any application dealing with images will probably strip the EXIF data. This is what I was told by a developer when I expressed my disappointment that EXIF data wasn’t saved by his app; I think it was the geo data that I specifically wanted. So yeah, that sucks! You either need to download it straight from your phone to your computer without editing or add the geo back in at some point. :-(
    The other thing the WordPress app didn’t do last time I checked was attach photos to posts properly. It inserts them in the post but doesn’t attach them in the metadata.

  25. Del Putnam says

    Just a quick follow up to close the loop on this topic.

    I did get confirmation (sort of) from one of the WordPress for iPhone forum moderators that the iPhone client does strip EXIF GPS data from images when they are uploaded. He didn’t say whether this was something they did or something that was forced upon them by Apple not exposing this functionality, but since EVERY other iPhone app I’ve used also seems to strip off the GPS EXIF data, I suspect you are right that it has something to do with the Apple libraries used.

    Also, I’m sure you’re aware that version 2.3.1 of the WordPress for iPhone client was just released and, while it still doesn’t maintain the photos’ GPS EXIF data, it DOES add geotag data to the post meta, which I think I can work with.

    For Twitter applications, I can do something similar. Tweetie and other apps also strip off the uploaded photo GPS EXIF data, but since Twitter stores the geotag data, I can go fetch the geotag data from twitter, add it in to the post meta data and use that info.

    So, I’m thinking of modifying your awesome Thesography plugin to work with post meta data in addition to EXIF data. It’s a bit convoluted, but I think it will work.

  26. says

    Del — Good detective work. I imagine that both WordPress and Twitter are using the current location data when dealing with geo location, so you would want to make sure that you post the photo when you take it, not a few hours later after editing.

  27. says

    Funny thing is, using this Google Maps link and the coordinates from a photo taken in Montreal, I end up in a lake in Kazakstan!

  28. says

    Rico — You need to add the ll parameter to the maps link, it can be the same as the q parameter, but helps show the location in the right place… I don’t know why, but I discovered it a year or two after this post.

  29. says

    Thanks… just delete my comment! I used a positive value instead of negative ;-)

    Thanks again for your generosity!

  30. says

    Hi Kristen. Just wanted to say thanks for creating this code. It’s exactly what I was looking for and much lighter and simpler than any of the plugins I tried.

    I did have a bit of trouble getting it working until I altered the if statement on line 24 of the complete EXIF code. Just couldn’t get it to display until I realized you had it set to only display if category 118. Which I don’t have.

    Thanks though!