PDA

View Full Version : Extracting and displaying EXIF data with PHP



weegillis
06-02-2012, 05:18 PM
A question was posted a little more than a week ago relating to EXIF data in JPG files, and more particularly how to derive decimal equivalents for GPS latitude and longitude. A lot of time and effort went into helping with the OP, so in light of this, I have chosen continue to refine the work into something practical and usable, and post and explain it in full, in this thread.

Allow me to outline what we will be working with... There are five parts to this project, which for now all live in the same directory as the image files. Further refinement will eventually separate out the site wide components to prevent repetition and overlap, and allow for re-use, but for these purposes, it is just as easy to keep things as they are. The five parts are,

1) index.php

This is the thumbnail page that opens with a simple URL, "/folder/".

2) thumbnail.php

This is a small file that comes to us from the previous mentioned thread. Source unknown, so we cannot as yet attribute it to anybody.

3) gps_exif.php

This is the single frame view page, which is only linked to from links generated in index.php.

4) gps_exif_inc.php

This is the include file that contains all of the PHP methods called up by both pages (1 and 3).

5) gps_exif.css

The style sheet, which is very basic, and which was also derived from the previous thread, but way simplified and reworked.

In the following posts, I'll go into these components, as well as break down the methods each call upon.

weegillis
06-02-2012, 05:23 PM
Let's start with the index.php page. There's not much to it:


<?php require_once "gps_exif_inc.php"; ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Thumbnail page with EXIF data</title>
<meta name="robots" content="noindex,nofollow">
<style>
body { font-size: 100%; }
td { font-size: 0.8em;}
td * { font-size: 100%;}
</style>
</head>
<body>
<table>
<?php $imgDir = "."; getEXIF($imgDir); ?>
</table>
</body>
</html>

weegillis
06-02-2012, 05:30 PM
The above page imports the methods include file, and calls upon one method, getEXIF(), which method will query the folder, discover and load any JPG files it finds, extracting pertinent data from each file it loads. That data will become apparent as we go through the method.



function getEXIF($dir) {
global $exif;
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
$count = 1;
while (($file = readdir($dh)) !== false) {
if (stristr($file, '.jpg')) {
$exif = exif_read_data($file, 0, true);
$str = " <tr>\n <td><div><a href=\"gps_exif.php?img=" . strTrunc($file, 4) . "\"><img src=\"";
$str .= (hasSection('THUMBNAIL')) ? "thumbnail.php?file=" . $file . "\"" : $file . "\" width=\"195\"";
$str .= " alt=\"" . strTrunc($file, 4) . "\"></a></div></td>\n <td><div>";
$str .= "File: <b>" . $exif['FILE']['FileName'] . "</b><br>\n";
$str .= "Timestamp : " . date("m/d/Y h:i:s A T", $exif['FILE']['FileDateTime']) . "<br>\n";
$str .= "Date taken: " . $exif['EXIF']['DateTimeOriginal'] . "<br>\n";
$str .= "Dimensions: " . $exif['COMPUTED']['Width'] . " x " . $exif['COMPUTED']['Height'] . " <br>\n";
$gps = (hasSection('GPS')) ? getGPS() : null;
if ($gps != null) {
$str .= "Latitude : " . $gps[0] . "&deg;<br>\n";
$str .= "Longitude : " . $gps[1] . "&deg;<br>\n";
$str .= "<a href=\"http://www.wikimapia.org/#lat=" . $gps[0] . "&amp;lon=" . $gps[1] . "&amp;z=17\">Map Reference " . $count++ . "</a>\n";
}
$str .= " </div></td>\n";
$str .= " </tr>\n";
echo $str;
}
}
closedir($dh);
}
}
}

weegillis
06-02-2012, 05:53 PM
As mentioned in the previous thread, we don't really need to check if the directory exists--we're in it. I left this for future use once the methods become re-usable and named directories are fed in, rather than ".". We will need to verify their existence in that scenario. Likewise, we want to verify the existence of JPG files in the folder, which is what the discovery script does in the 'if (stristr($file,'.jpg'))' is all about. In future refinement we will need to tag in Error statements, such as 'Invalid directory' and 'No image files found'. That can wait for another day.

Having discovered a JPG file, we proceed to read in the EXIF data tagged into the file. Again, further refinement will include an Error statement to the effect, 'No EXIF tags found', which condition we are not testing for yet. For now we go on the assumption that if it is JPG it will contain EXIF tags, and go from there.

This statement, '$exif = exif_read_data($file, 0, true);' pulls all the data from the image and stores it in a collection (array). The 'true' parameter tells the function to include the thumbnail if it exists.

Now we can expect that some files will not contain thumbnails, and we need a way to find out whether they do or not. After a lot of failed attempts to verify the existence of a thumbnail, I finally came down to a fairly failsafe method. It comes in the form of a key contained in the 'FILE' array, called 'SectionsFound'.




[FILE] => Array



(
.
.



[SectionsFound] => ANY_TAG, IFD0, THUMBNAIL, COMMENT, EXIF, GPS



)




The value associated with this key is a string, which I later discovered after trying umpteen loop methods to match the data. Such is the life of a hobbyist. Always learning. The following method was derived from those attempts, and boils down to simple true/false question:



function hasSection($str) { global $exif; return stristr($exif['FILE']['SectionsFound'], $str); }

weegillis
06-02-2012, 06:06 PM
Armed with the ability to test for sections in the $exif array, we can now determine which form of thumbnail to use, the one extracted from the image if it exists using thumbnail.php, or failing that, one we generate from the actual image itself (being as it is already in memory). I chose the arbitrary width of 195 pixels as it matched many of the test files, and does not look out of place.

The image tag is wrapped in a link to gps_exif.php with an &img= query string made up from the image file's name, with the extension stripped by strTrunc(), the utility string method I pulled in from another project.


function strTrunc($str, $trunc) { return substr($str, 0, strlen($str)-$trunc); };


The method truncates from the right by a specified length, unlike others that truncate to a specific length from the left. In our case, the extension is 4 characters. Later, we will want to refine our main method to recognize '.jpeg' as well, and truncate by 5 characters, not 4.

weegillis
06-02-2012, 07:05 PM
Next we move on to the data we want to appear with the thumbnail, which in this case includes, File Name, File Date Time, Date Time Original, and Width/Height. If it exists we will want the GPS data, as well.

The first in the list is self-explanatory. The FileDateTime is not derived from a formatted string in the data; it is an actual UNIX timestamp, and is updated each time the file is modified and re-saved. It will always relate to the last modified date/time. Being a timestamp, we need to call upon date() to format a human readable string for output.


"Timestamp : " . date("m/d/Y h:i:s A T", $exif['FILE']['FileDateTime'])

As we can see, the timestamp will only contain the actual date the picture is taken IF the image is not manipulated and re-saved, so it is not a reliable indication of the real date and time that the picture was taken. For this data, we need to look at DateTimeOriginal in the EXIF section, where is found a formatted date string we can output directly.


"Date taken: " . $exif['EXIF']['DateTimeOriginal']

These two dates are stored in the file, which is moot. In the absence of this data, PHP has only date references on the server to refer to as a fallback, if needed. They cannot be relied upon as real dates pertaining to the image, only when it was last uploaded. Something to bear in mind if writing a fallback method to incorporate with this page.

Dimension data can also be determined by PHP if the computed width and height cannot be found. Again, this relates only to fallback methods, and not directly in this method. The point being that we have dimensions if we choose to configure them for display or secondary use. Basic stuff but due proper consideration.

Correction: What I've discovered is that the server sets the 'FileDateTime' timestamp, being the last in the chain to store the file. In a sense, it is almost pointless to display this, unless for maintenance purposes.

weegillis
06-02-2012, 07:19 PM
Now the GPS data, which is what got this whole topic rolling ten days again. How is it stored, and how is it manipulated? Then arose, how do we convert it to decimal degrees?

For this, we need to call upon the getGPS() method that is adapted from the OOP methods attributed.


// below adapted from http://www.quietless.com/kitchen/extract-exif-data-using-php-to-display-gps-tagged-images-in-google-maps/
function toDecimal($deg, $min, $sec, $hem) {
$d = $deg + $min/60 + $sec/3600;
return ($hem=='S' || $hem=='W') ? $d*=-1 : $d;
}
function divide($a) {
$e = explode('/', $a);
if (!$e[0] || !$e[1]) {
return 0;
} else {
return $e[0] / $e[1];
}
}
function getGPS() {
global $exif, $lat_deg, $lat_min, $lat_sec, $lat_hem, $log_deg, $log_min, $log_sec, $log_hem;
if ($exif) {
$lat = $exif['GPS']['GPSLatitude'];
$log = $exif['GPS']['GPSLongitude'];
if (!$lat || !$log) return null;
$lat_deg = divide($lat[0]);
$lat_min = divide($lat[1]);
$lat_sec = divide($lat[2]);
$lat_hem = $exif['GPS']['GPSLatitudeRef'];
$log_deg = divide($log[0]);
$log_min = divide($log[1]);
$log_sec = divide($log[2]);
$log_hem = $exif['GPS']['GPSLongitudeRef'];
$ltd_dec = toDecimal($lat_deg, $lat_min, $lat_sec, $lat_hem);
$lgd_dec = toDecimal($log_deg, $log_min, $log_sec, $log_hem);
return array(round($ltd_dec, 7), round($lgd_dec, 7));
} else {
return null;
}
}
//

weegillis
06-02-2012, 07:40 PM
There is a lot in there to talk about, that's sure. At some later time I want to re-examine the calculations for correctness. I still have some questions of my own. Not doubts, mind, just questions, and wanting to learn ways to confirm the math I use, not that of the OOP author.

Rather than pass values into getGPS(), I draw them straight out of the $exif array, which is global in scope. All the other variables named in the global list are passed back out, as they are newly created in the method. I could have used an array for this, and still might. For the present, it is a ragtag list of individual values.

The return value is either null, or an array containing two elements, which I round to 7 decimal places to match the format used by WikiMapia.org in the URL's. These elements are respectively, Latitude and Longitude, in decimal form. After that it's just a matter of creating the link to WMO and we're off to the races, generating the next thumbnail, until all in the folder are exhausted. I've set the &z= value to '17' so there is some altitude above the map, about 1:1750 in scale. As we'll later see, we can manipulate this in our location bar URL's in the single frame page.

The WikiMapia.org links could be made to open in a new window, but I would only advise it if JavaScript is used, since 'target="_blank"' is invalid in some doctypes, and because it breaks the Back button, which in this instance might be good to have use of.

weegillis
06-02-2012, 08:01 PM
If we examine the source code, we can see the page that is generated by our methods. This page found six images. Can you spot the one that had no embedded thumbnail?


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Thumbnail page with EXIF data</title>
<meta name="robots" content="noindex,nofollow">
<style>
body { font-size: 100%; }
td { font-size: 0.8em;}
td * { font-size: 100%;}
</style>
</head>
<body>
<table>
<tr>
<td><div><a href="gps_exif.php?img=IMGP0170"><img src="thumbnail.php?file=IMGP0170.jpg"></a></div></td>
<td><div>File: <b>IMGP0170.jpg</b><br>
Timestamp : 05/31/2012 01:14:31 AM EDT<br>
Date taken: 2012:05:09 10:53:05<br>
Dimensions: 720 x 479 <br>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=IMGP0171"><img src="thumbnail.php?file=IMGP0171.jpg"></a></div></td>
<td><div>File: <b>IMGP0171.jpg</b><br>
Timestamp : 05/31/2012 01:14:32 AM EDT<br>
Date taken: 2012:05:09 10:59:56<br>
Dimensions: 720 x 479 <br>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=IMGP0172"><img src="thumbnail.php?file=IMGP0172.jpg"></a></div></td>
<td><div>File: <b>IMGP0172.jpg</b><br>
Timestamp : 05/31/2012 01:14:33 AM EDT<br>
Date taken: 2012:05:09 11:04:37<br>
Dimensions: 720 x 479 <br>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=IMGP0173"><img src="thumbnail.php?file=IMGP0173.jpg"></a></div></td>
<td><div>File: <b>IMGP0173.jpg</b><br>
Timestamp : 05/31/2012 01:14:34 AM EDT<br>
Date taken: 2012:05:09 11:19:38<br>
Dimensions: 720 x 479 <br>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=CN_78492_070930"><img src="CN_78492_070930.jpg" width="195"></a></div></td>
<td><div>File: <b>CN_78492_070930.jpg</b><br>
Timestamp : 06/01/2012 12:45:17 AM EDT<br>
Date taken: <br>
Dimensions: 720 x 459 <br>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=CN_78492_080624"><img src="thumbnail.php?file=CN_78492_080624.jpg"></a></div></td>
<td><div>File: <b>CN_78492_080624.jpg</b><br>
Timestamp : 06/01/2012 12:45:18 AM EDT<br>
Date taken: 2008:06:24 18:08:31<br>
Dimensions: 720 x 478 <br>
</div></td>
</tr>
</table>
</body>
</html>

weegillis
06-02-2012, 08:05 PM
Oops! None of those images contain GPS tags. Let's look at another folder...



<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<title>Thumbnail page with EXIF data</title>
<meta name="robots" content="noindex,nofollow">
<style>
body { font-size: 100%; }
td { font-size: 0.8em;}
td * { font-size: 100%;}
</style>
</head>
<body>
<table>
<tr>
<td><div><a href="gps_exif.php?img=IMG_0633"><img src="thumbnail.php?file=IMG_0633.jpg"></a></div></td>
<td><div>File: <b>IMG_0633.jpg</b><br>
Timestamp : 05/29/2012 08:38:26 PM EDT<br>
Date taken: 2011:07:17 06:51:17<br>
Dimensions: 574 x 720 <br>
Latitude : 49.283&deg;<br>
Longitude : -123.1036667&deg;<br>
<a href="http://www.wikimapia.org/#lat=49.283&amp;lon=-123.1036667&z=17">Map Reference 1</a></p>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=IMG_0635"><img src="thumbnail.php?file=IMG_0635.jpg"></a></div></td>
<td><div>File: <b>IMG_0635.jpg</b><br>
Timestamp : 06/02/2012 02:21:45 AM EDT<br>
Date taken: 2011:07:17 06:51:56<br>
Dimensions: 538 x 720 <br>
Latitude : 49.2833333&deg;<br>
Longitude : -123.1041667&deg;<br>
<a href="http://www.wikimapia.org/#lat=49.2833333&amp;lon=-123.1041667&z=17">Map Reference 2</a></p>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=IMG_0189"><img src="thumbnail.php?file=IMG_0189.jpg"></a></div></td>
<td><div>File: <b>IMG_0189.jpg</b><br>
Timestamp : 06/01/2012 11:36:15 PM EDT<br>
Date taken: 2011:04:30 15:00:57<br>
Dimensions: 720 x 538 <br>
Latitude : 48.4208333&deg;<br>
Longitude : -123.3686667&deg;<br>
<a href="http://www.wikimapia.org/#lat=48.4208333&amp;lon=-123.3686667&z=17">Map Reference 3</a></p>
</div></td>
</tr>
<tr>
<td><div><a href="gps_exif.php?img=IMG_0425"><img src="thumbnail.php?file=IMG_0425.jpg"></a></div></td>
<td><div>File: <b>IMG_0425.jpg</b><br>
Timestamp : 06/02/2012 12:10:25 AM EDT<br>
Date taken: 2011:06:01 11:47:52<br>
Dimensions: 720 x 538 <br>
Latitude : 51.1318333&deg;<br>
Longitude : -114.0111667&deg;<br>
<a href="http://www.wikimapia.org/#lat=51.1318333&amp;lon=-114.0111667&z=17">Map Reference 4</a></p>
</div></td>
</tr>
</table>
</body>
</html>

weegillis
06-02-2012, 08:18 PM
Aside: We can see from this generated HTML that our page, while valid HTML5, is not AAA compliant. "Map Reference" is a repeated link phrase that points to multiple URL's. Fail. Some refinement here will be warranted, but this is for another day.

Update: Another day has come. AAA refinement implemented. See above edited method post containing this resolved issue (http://www.webproworld.com/webmaster-forum/threads/119120-Extracting-and-displaying-EXIF-data-with-PHP?p=619722&viewfull=1#post619722).

At this point we have geo-position data, and a range of maps to view it (on WMO, the Map Type menu offers a whole list of them). More than that would be well beyond the scope of this page.

Now we move on to the single frame page gps_exif.php.


<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<title>Geo-position of a photo subject</title>
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="gps_exif.css">
</head>
<body>
<?php
require_once "gps_exif_inc.php";
$image = isset($img) ? $img . ".jpg" : null;
readExif($image);
?>
<div id="header"><div id="breadcrumb"><a href="./" title="Back to Thumnail page">&lt;&lt;&lt; Thumbnails</a></div><div class="headerbox">Where was this picture taken?</div></div>
<div id="image"><div class="imagebox"><div class="imageshow"><?php getImg($image); ?></div></div></div>
<div id="exif">
<div class="exifbox">
<div class="exifboxtable">
<table>
<?php print_data(); ?>
</table>
</div>
</div>
</div>
</body>
</html>


Not much to this one, either, but a couple of things to talk about.

weegillis
06-02-2012, 08:48 PM
For this page I put the PHP right in the body for reasons that will reveal themselves, presently. We import the methods include, and test the REQUEST_URI for the presence of &img in the query string. If the variable exists, we copy its value into $image and tack '.jpg' on the end to restore the resource name.

We also test for the presence of a hidden variable, &h, which is our map view altitude adjust, ranging in value from -4 to +4. These adjust values are arbitrary, just as was setting the initial &z= to 17. In my experiments I've found that z=21 is the upper limit (smallest scale) and z=13 is a large enough scale to show several miles around the target. This is a suitable range for our purposes, and validating ensures we are always generating reasonable requests to WMO.

The validation method can probably be moved to the methods include file... on another day. {update: done, see #19 (http://www.webproworld.com/webmaster-forum/threads/119120-Extracting-and-displaying-EXIF-data-with-PHP?p=619740&viewfull=1#post619740)}

Armed with $image the page makes a call to readExif(). As it resides in the global scope, passing $image in is kind of unnecessary, in this instance. But it does make the method a little more enclosed; and, closure/re-use is always a good thing.

weegillis
06-02-2012, 08:54 PM
From within our page, there are three function calls:

1) readExif($image);

2) getImg($image);

3) print_data();

Let's start with the middle one, since it's the image view portion of these methods, and fairly straight forward. I say fairly, because there is some explanation needed.



function getImg($img) {
global $exif;
if (file_exists($img)) {
$str = "<img src=\"$img\" ";
$chtm =$exif['COMPUTED']['html'];
if (!$chtm) {
$fil_wid = $exif['COMPUTED']['Width'];
$fil_hgt = $exif['COMPUTED']['Height'];
$str .= "width=\"" . (($fil_wid > 0) ? $fil_wid : "100%") . "\" height=\"" . (($fil_hgt > 0) ? $fil_hgt : "100%") . "\"";
} else {
$str .= $chtm;
}
$str .= " alt=\"" . strTrunc($img, 4) . "\">";
echo $str;
} else {
echo "Image not found.";
}
}

weegillis
06-02-2012, 09:03 PM
We have the method to test for the 'COMPUTED' section, but for this method I just jump right in and ask for the ['COMPUTED']['html']. If it exists, its value is stored in $chtm. It will look like this:


width="720" height="538"

This can be handily inserted in our generated IMG tag. If it does not exist, we fall back to the computed width and height, and if they end up not existing, we fall back on width="100%" and height="100%" and let the browser sort it out. The alternate text in our example is the image file name, truncated of its extension.

Again, we only pass the image name to the method, and extract EXIF data directly from the $exif array.

weegillis
06-02-2012, 09:40 PM
Aside: It follows that as a web application, we are limited by connection speeds and bandwidth, and while we are able to work with files of any size, the two to three MB files coming from cameras today are way beyond practical for use online. Remember, the index page requests every image in the folder.

For this reason we take those huge files and scale them to something a little more manageable, like 720 or less by 720 or less. This is the standard for Facebook, so I adopted it for use in this setting. Quality is set to 80% and EXIF data is left intact (of course).

It is at this stage in the process that we can add a JPEG Comment to the file. We'll see shortly how these can be displayed along with the picture.

File size falls in the range of 68 to 118 KB. It would take 20-25 of these files to equal one bulky camera file. That tells us we get 20X the performance with optimized files.

And it helps with AAA compliance (fast loading pages).

weegillis
06-02-2012, 09:53 PM
Now we get to the hard part, which will require a lot of explanation, the readExif() method. This method centers itself around GPS data mostly, but also checks for a COMMENT section. Only Error strings are generated within this method, and all created/changed variables reside in the global scope with exception to $img (the argument) and $gps, the volatile results holder in the sequence of expressions. Let's have look under the hood:


function readExif($img) {
global $exif, $gps_alt, $lat_dec, $lon_dec, $use_com, $errStr;
$errStr = "";
if (file_exists($img)) {
if (@exif_read_data($img)) {
$exif = @exif_read_data($img, 0, true);
echo "<div id=\"exifdump\">\n";
print_r($exif);
echo "</div>\n";
$use_com = (hasSection('COMMENT')) ? $exif['COMMENT'][0] : null;
if (hasSection('GPS')) {
$gps = $exif['GPS']['GPSAltitudeRef'];
$alt_ref = ($gps !== null) ? $gps : null;
$gps = $exif['GPS']['GPSAltitude'];
$gps_alt = ($gps !== null) ? round(divide($gps), 4) : "N/A";
$gps = getGPS();
if ($gps != null) {
$lat_dec = $gps[0];
$lon_dec = $gps[1];
}
normalize();
} else {
$errStr .= " <tr>\n<td colspan=\"3\"><p>No GPS tags found.<p></td>\n";
}
} else {
$errStr .= " <tr>\n<td colspan=\"3\"><p>No EXIF tags found.</p></td>\n";
}
}
}

weegillis
06-02-2012, 10:16 PM
Without knowing a most effective way to test for EXIF data, I went with just calling it, and letting that be the TRUE that lets the whole process get underway. Allowing that, a genuine call is made to the exif_read_data() function. Whoa! What is this?



echo "<div id=\"exifdump\">\n";
print_r($exif);
echo "</div>\n";


Yes, it is what you see. Debugging code woven right into a display: none; div tag. This is why I had to load the PHP inside the body element, so I could apply CSS to it. What we have here is the complete dump of the EXIF data collection, but we must View Source to read it. It's all nicely laid out for us by PHP, to boot, not the sort of thing you would see if it were displaying in the page, that's certain.

Now we create a user comment string if one exists in the $exif array, and move on to GPS. We require AltituteRef to determine if the geo-location is above or below sea level. We don't want to assume anything. I just wish the standard would have been 1 and -1, instead of 1 (below) and 0 (above). Then we could test for zero value rather than null. It would be more elegant.

Here is where $gps takes on its first assignment:


$gps = $exif['GPS']['GPSAltitudeRef'];
$alt_ref = ($gps !== null) ? $gps : null;

Then it gets another assignment. Note that we re-use the borrowed divide() function:


$gps = $exif['GPS']['GPSAltitude'];
$gps_alt = ($gps !== null) ? round(divide($gps), 4) : "N/A";

And finally the assignment as our results array:

$gps = getGPS();

This is where the 'my math' statement comes back into play. I'm pretty sure that the normalization is correct, but not certain. Here is the method I came up with that seems to hit the nail right on the head for most coordinates generated:


function normalize() {
global $lat_min, $lat_sec, $log_min, $log_sec;
$x = (int)$lat_min;
$y = (int)$log_min;
$lat_sec = round(($lat_min - $x) * 60, 2);
$log_sec = round(($log_min - $y) * 60, 2);
$lat_min = $x;
$log_min = $y;
}

weegillis
06-02-2012, 10:31 PM
The first two assignments to $gps generate strings, the first either a zero (or a one) or null, the second either a value rounded to four decimal places, or a string reading "N/A". The first false sets a null flag, and the second false creates the error string. We will test these during the output stage. The latter assignment to $gps is the array holding the decimal latitude and longitude.

The purpose of normalizing is to generate sexagesimal values for degrees, minutes and seconds. With the seconds being stored in the decimal fraction of the minutes, we cannot display the seconds properly. Normalizing removes the decimal fraction from the minutes, and calculates and stores the normalized value in the seconds variable, where we want them. I round them to two decimal places.

weegillis
06-02-2012, 10:38 PM
And finally, the last function call from our single frame page, and the one that will output (if they exist) the user comment in a caption below the image frame, and the GPS data that's available, as well as a map reference link out to WikiMapia.org. We'll refer to this link again in our closing discussion.

print_data() is the HTML factory for the page (excluding the IMG tag already generated). All of the little bits that have been floating around out in the global scope will now get put in their proper place. We have created the variable $z which holds the calculated value for &z= cast as a string, for use in generating the WMO link. Note we added $h to the global list:



function print_data() {
global $errStr, $exif, $gps_alt, $lat_deg, $lat_min, $lat_sec, $lat_hem, $lat_dec, $log_deg, $log_min, $log_sec, $log_hem, $lon_dec, $zindx, $use_com, $h;
$str = "";
if ($use_com) { $str .= " <tr>\n <td colspan=\"3\">\n <p class=\"comment\">$use_com</p>\n </td>\n </tr>\n"; }
if (!$errStr) {
$str .= " <tr><th scope=\"col\">LATITUDE</th><th scope=\"col\">LONGITUDE</th><th scope=\"col\">ALTITUDE</th></tr>\n <tr>\n";
$str .= " <td>\n <p>$lat_deg&deg; $lat_min&rsquo; $lat_sec&rdquo; $lat_hem</p>\n <p>$lat_dec&deg;</p>\n </td>\n";
$str .= " <td>\n <p>$log_deg&deg; $log_min&rsquo; $log_sec&rdquo; $log_hem</p>\n <p>$lon_dec&deg;</p>\n </td>\n";
$z = isset($h) ? ((intval($h) <= 4 && intval($h) >= -4) ? "&amp;z=" . (string)(17 + intval($h)) : "&amp;z=17") : "&amp;z=17";
$str .= " <td>\n <p>" . (($gps_alt == "N/A") ? $gps_alt : "$gps_alt m " . (($alt_ref) ? "Below" : "Above") . " Sea Level.") . "</p>\n <p>Go to this <a href=\"http://www.wikimapia.org/#lat=" . $lat_dec . "&amp;lon=" . $lon_dec . $z . "\">location on the map</a></p>\n </td>\n";
} else {
$str .= $errStr;
}
$str .= " </tr>";
echo $str;
}

weegillis
06-02-2012, 10:52 PM
All pretty much straight forward. We can see the Sea Level reference is tested, and where $zindx gets inserted in the link's href. To make use of our hidden adjust feature (that will take JavaScript and possibly AJAX to fully incorporate in the page) we simply add &h= to the URL in the location bar and hit enter (not F5) to re-request the page. Nothing on the page will change but if you examine the link, you will see the new z= value.

Eg. gps_exif.php?img=IMG0189&h=3

The URL in the status bar should read, ... &z=20 ...

That's a wrap. This will continue to be a subject of some study for the next while, but will no doubt gradually fade from the radar in my development pursuits. I've learned a lot this week, and hope you, the reader will be able to take this away with you for use on your own site. Attribution to me (Roy Pierce) would be appreciated, with a link back to this thread.

Should you have any questions about this script, please bring them up on this forum not somewhere else on the web. Thanks for indulging.

The latest version of this project is available in the attached ZIP.

weegillis
06-04-2012, 02:37 AM
On another day... Let's get rid of the confines of a two column table and swap-in an elastic index page.

If you have followed the link in my sig you will notice that things have evolved along these lines. I'm sporting an HTML5 doctype, after all, so may as well show it. Since this page was never linked in to the main style sheet, I've left the new sheet in-page for now. It will get separated, in due course.

weegillis
06-04-2012, 03:14 AM
This module, and the index page itself have changed...


function getEXIF($dir) {
global $exif;
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
$count = 1;
while (($file = readdir($dh)) !== false) {
if (stristr($file, '.jpg')) {
$exif = @exif_read_data($file, 0, true);
$str = " <div class=\"item\">\n <div class=\"thum\"><a href=\"viewer.php?img=" . strTrunc($file, 4) . "\"><img src=\"";
$str .= (hasSection('THUMBNAIL')) ? "thumbnail.php?file=" . $file . "\"" : $file . "\" width=\"195\"";
$str .= " alt=\"" . strTrunc($file, 4) . "\"";
$str .= (hasSection('COMMENT')) ? " title=\"" . $exif['COMMENT'][0] . "\"" : null;
$str .= "></a></div>\n <ul>\n";
$str .= " <li>File: <b>" . $exif['FILE']['FileName'] . "</b></li>\n";
// $str .= " <li>Timestamp : " . date("Y-m-d", $exif['FILE']['FileDateTime']) . "</li>\n"; // maintenance only
$str .= " <li>Date taken: " . $exif['EXIF']['DateTimeOriginal'] . "</li>\n";
$str .= " <li>Dimensions: " . $exif['COMPUTED']['Width'] . " x " . $exif['COMPUTED']['Height'] . " </li>\n";
$gps = (hasSection('GPS')) ? getGPS() : null;
if ($gps != null) {
$str .= " <li>Latitude : " . $gps[0] . "&deg;</li>\n";
$str .= " <li>Longitude : " . $gps[1] . "&deg;</li>\n";
$str .= " <li class=\"win\"><a href=\"http://www.wikimapia.org/#lat=" . $gps[0] . "&amp;lon=" . $gps[1] . "&amp;z=17\">Map Reference " . $count++ . "</a></li>\n";
}
$str .= " </ul>\n </div>\n";
echo $str;
}
}
closedir($dh);
}
}
}


index page with CSS...


<?php require_once "library.php"; ?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Thumbnail page with EXIF data</title>
<meta name="robots" content="noindex,nofollow">
<style>
body { font-size: 100%; text-align: center;}
div#wrap {
width: 98%;
margin: 0 auto;
overflow: auto;
}
div.item {
float: left;
width: auto;
overflow: auto;
text-align: left;
font: normal 0.8em/1.0em Calibri, Arial, Helvetica, sans-serif;
}
div.thum {
float: left;
width: auto;
height: auto;
margin: 0 1em 1em 0;
}
.item ul {
list-style: none;
margin: 1em 0;
padding: 0;
}
</style>
</head>
<body>
<div id="wrap">
<?php $imgDir = "."; getEXIF($imgDir); ?>
</div>
</body>
</html>

weegillis
06-04-2012, 03:26 AM
This should allow smart phones to at least easily access and peruse the thumbnails. Since it's all server-side, they're not feeling the full hit of the bandwidth until they go to single frame view. As I said earlier, optimizing the images really pays off here. These frames, even as optimized as they are all cost money to view on a smart phone.

juto
06-04-2012, 02:18 PM
Hi weegillis. That will become handy when it's ready for production use. My compliments for a very informative and nicely written article.

Sara

weegillis
06-25-2012, 03:27 AM
That will become handy when it's ready for production use.
While not there yet, I am using it in trial production on a live site, in it's revised form, of course.

This version is not compatible with the earlier posted one, so out with the old, and in with the new. Drats! All that work...

I'm still working at limiting the number of files needed in a folder to zero, but not there yet. We still need these three files in the respective jpeg folder.

1. index.php
2. viewer.php
3. thumbnail.php (I know, this one has no excuse for still being here...)

Having created templates for the two pages, the files are very small, and limited only to an editable title and a couple of requires. How easy is that? Edit a title and create not one page, but a whole folder of them. Headings are generated from the photo, separate from the HTML title tag.



index.php

<?php
// v0.4.120623
$title = "text plus a space ";
$imgDir = ".";
require_once "[../path back to root]/includes/library.php";
require_once "[../path back to root]/templates/thumbs.php";
?>


viewer.php

<?php
// v0.4.120623
$title = "text plus a space ";
require_once "[../path back to root]/includes/library.php";
require_once "[../path back to root]/templates/viewer.php";
?>



thumbnails.php you already have.

The templates are very simple, and will be tacked on next. The library has undergone/is undergoing significant revision, ergo the need to scrap the old stuff. It will need a little explanation, but I won't repeat myself if it's in the earlier proof of concept above. We'll stick to the new stuff. There is a CSS component to this, some of which is derived from the very original seeds of this project. I'll tack it on last.

weegillis
06-25-2012, 03:52 AM
Here are the two template files. They are named .php but needn't be. They can actually have any extension that suits you.



thumbs.php

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><?php ins($title); ?> &bull; photo navigator</title>
<meta name="robots" content="noindex,nofollow">
<link href="/css/exif.css" rel="stylesheet">
</head>
<body>
<div id="wrap">
<?php getEXIF($imgDir); ?>
</div>
</body>
</html>

viewer.php

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> <?php ins($title); ?>&bull; photo inspector</title>
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="/css/exif.css">
</head>
<body>
<?php
$image = isset($_REQUEST['img']) ? $_REQUEST['img'] : null;
readExif($image);
?>
<div id="header"><div id="breadcrumb"><a href="./" title="Back to Photo Navigator">&lt;&lt;&lt; Thumbnails</a></div><div class="headerbox"><?php ins($tags['title']) ?></div></div>
<div id="image"><div class="imagebox"><div class="imageshow"><?php getImg($image); ?></div></div></div>
<div id="exif">
<div class="exifbox">
<div class="exifboxtable">
<table>
<?php printData(); ?>
</table>
</div>
</div>
</div>
</body>
</html>



thumbs.php is pretty straight up. We only have two variables, $title and $imgdir. In the viewer we have the $title variable, as well as the query string variable which is the foundation of the page, else it wouldn't exist. By the time page content is being generated a table of tags has already been populated with values; ergo, $tags['title'] in the 'headerbox' (yes, this will be history soon). Instead of passing a slough of variables to the output function, we're now only passing a simple table, as promised. It's in the global scope, though, so invisible to the templates, source code wise.

library.php is next.

weegillis
06-25-2012, 04:51 AM
The library contains a derived part, which had to go through more change to allow for a single table, instead of the chorus of variables. I also moved the normalization into this method and removed the earlier function, as it is no longer needed. The revision I'm currently using is here:



// adapted from www.quietless.com/kitchen/extract-exif-data-using-php-to-display-gps-tagged-images-in-google-maps/
function toDecimal($deg, $min, $sec, $hem) { $d = round(($deg + $min/60 + $sec/3600), 7); return ($hem=='S' || $hem=='W') ? $d*=-1 : $d;}
function divide($a) { $e = explode('/', $a); return (!$e[0] || !$e[1]) ? 0 : $e[0] / $e[1]; }
function getGPS() {
global $exif, $tags;
if ($exif) {
$lat = $exif['GPS']['GPSLatitude'];
$log = $exif['GPS']['GPSLongitude'];
if (!$lat || !$log) return null;
$tags['latdeg'] = divide($lat[0]);
$lat_min = divide($lat[1]);
$lat_min_frac = $lat_min - (int)($lat_min);
$lat_min = (int)($lat_min);
$tags['latmin'] = $lat_min;
$tags['latsec'] = round($lat_min_frac * 60, 2);
$tags['lathem'] = $exif['GPS']['GPSLatitudeRef'];
$tags['logdeg'] = divide($log[0]);
$log_min = divide($log[1]);
$log_min_frac = $log_min - (int)($log_min);
$log_min = (int)($log_min);
$tags['logmin'] = $log_min;
$tags['logsec'] = round($log_min_frac * 60, 2);
$tags['loghem'] = $exif['GPS']['GPSLongitudeRef'];
$tags['latitude'] = toDecimal($tags['latdeg'], $tags['latmin'], $tags['latsec'],$tags['lathem']);
$tags['longitude'] = toDecimal($tags['logdeg'], $tags['logmin'], $tags['logsec'], $tags['loghem']);
return array($tags['latitude'], $tags['longitude']);
} else {
return null;
}
}


The math in the normalization method agrees with what Windows calculates, albeit I round to 7 decimal places for WikiMapia.

At some point I plan to write in a fallback for missing GPS. The lat and long can be added via one of the editable tags, and the script could detect them, allowing for a WM map reference.

I know this isn't Geotagging, but if you ask me, it's a lot more accurate, and not susceptible to the whims of a black hole giant. Truth be known, they soak it up, anyway. I've had lots of places that I marked in WM eventually show up on GE. And the detail cards on WM show up almost immediately in search.

And what's more, the client has not the need to download a s---load of bulky script to see these place marks and detail cards. Still, it's ad driven, we cannot escape that. It all ends up in somebody's pail.

But I digress. All we've got so far is the GPS data. Let's move on...

weegillis
06-25-2012, 05:11 AM
We need a few utilities to help us the rest of the way, so I'll tack them here. Their purpose will reveal itself presently.




function imgConfirm($arg) { $img1 = $arg . ".jpg"; $img2 = $arg . ".JPG"; return file_exists($img1) ? $img1 : (file_exists($img2) ? $img2 : null);};
function hasSection($arg) { global $exif; return stristr($exif['FILE']['SectionsFound'], $arg); }
function strTrunc($arg, $trunc) { return substr($arg, 0, strlen($arg)-$trunc); };
function filter($arg) { return preg_replace('/[^\p{L}\p{M}\p{Z}\p{N}\p{P}]/u', '', $arg); };
function ins($arg) { echo $arg; }



As stated, purpose will reveal itself. We've already used ins() a couple of times to this point, and, hasSection() and strTrunc() are previously discussed. The rest will come out.

weegillis
06-25-2012, 06:34 AM
Almost forgot where we were here...

The index page is just thumbnails, either extracted or derived from discovered images. The navigator hooks onto any jpg/JPG files that exist in the folder. If one is detected it will get a thumbnail, by hook or by crook. EXIF data is extracted and given back as available. Map links are created if GPS data is available. Socially, this system is not viable as any social media would strip GPS data on all photo uploads to its servers. We are dealing strictly with what an individual publisher wishes to do with GPS laden images on their own domain. One would always be well advised to consider the privacy of others.



// thumbs
function getEXIF($dir) {
global $exif;
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
$count = 1;
while (($file = readdir($dh)) !== false) {
if (stristr($file, '.jpg')) {
$exif = @exif_read_data($file, 0, true, true);
$str = " <div class=\"item\">\n <div class=\"thum\"><a href=\"viewer.php?img=" . strTrunc($file, 4) . "\"><img src=\"";
$str .= hasSection('THUMBNAIL') ? "thumbnail.php?file=" . $file . "\"" : $file . "\" width=\"195\"";
$sub = filter($exif['IFD0']['Subject']);
$alt = $sub ? $sub : strTrunc($file, 4);
$str .= " alt=\"$alt\" title=\"";
$com = hasSection('COMMENT') ? $exif['COMMENT'][0] : null;
$ttl = filter($exif['IFD0']['Title']);
$title = $ttl ? $ttl : $com;
$str .= $title ? $title : strTrunc($file, 4);
$str .= "\"></a></div>\n <ul>\n";
$str .= " <li>File: <b>" . $exif['FILE']['FileName'] . "</b></li>\n";
$str .= $sub ? " <li>Subject: $sub</li>\n" : null;
$str .= " <li>Date taken: " . strTrunc($exif['EXIF']['DateTimeOriginal'], 9) . "</li>\n";
$str .= " <li>Dimensions: " . $exif['COMPUTED']['Width'] . " x " . $exif['COMPUTED']['Height'] . " </li>\n";
$gps = hasSection('GPS') ? getGPS() : null;
if ($gps != null) {
$str .= " <li>Latitude : " . $gps[0] . "&deg;</li>\n";
$str .= " <li>Longitude : " . $gps[1] . "&deg;</li>\n";
$str .= " <li class=\"win\"><a href=\"http://www.wikimapia.org/#lat=" . $gps[0] . "&amp;lon=" . $gps[1] . "&amp;z=17\" title=\"Off-site\">Map Reference " . $count++ . "</a></li>\n";
}
$str .= " </ul>\n </div>\n";
echo $str;
}
}
closedir($dh);
}
}
}


The viewer describes slightly more than the navigator page as one would hope. The GPS, if available, is part of the output, with WM link, otherwise a report, "No GPS tags found". The GPS at this point is displayed in both sexagesimal and decimal formats, the latter being what is fed to WM. The heading is taken from the available tag data or truncated file name as a fallback. The caption for the image combines both the COMMENT and the IFD0 Comments sections, if they exist.

weegillis
06-25-2012, 02:56 PM
These functions power the viewer. Each does what their names suggest: read the EXIF data and store available tags; display the image; print the available data.




// viewer
function getImg($img) {
global $exif, $tags;
$imgx = imgConfirm($img);
if ($imgx) {
$str = "<img src=\"$imgx\" ";
$chtm =$exif['COMPUTED']['html'];
if (!$chtm) {
$fil_wid = $exif['COMPUTED']['Width'];
$fil_hgt = $exif['COMPUTED']['Height'];
$str .= "width=\"" . (($fil_wid > 0) ? $fil_wid : "100%") . "\" height=\"" . (($fil_hgt > 0) ? $fil_hgt : "100%") . "\"";
} else {
$str .= $chtm;
}
$str .= " alt=\"" . $tags['alt'] . "\" title=\"" . $tags['title'] . "\">";
echo $str;
} else {
echo "Image not found.";
}
}
function readExif($img) {
global $exif, $use_com, $errStr, $tags;
$errStr = "";
$imgx = imgConfirm($img);
if ($imgx) {
if (@exif_read_data($imgx)) {
$exif = @exif_read_data($imgx, 0, true, false);
echo "<div id=\"exifdump\">\n";
print_r($exif);
echo "</div>\n";
$comment = hasSection('COMMENT') ? $exif['COMMENT'][0] : null;
$alt = filter($exif['IFD0']['Subject']);
$title = filter($exif['IFD0']['Title']);
$comments = filter($exif['IFD0']['Comments']);
$author = filter($exif['IFD0']['Author']);
$alt= $alt ? $alt : strTrunc($imgx, 4);
$title = $title ? $title : strTrunc($imgx, 4);
$comments = $comments ? $comments : null;
$author = $author ? $author : null;
$tags = array(
'alt'=> $alt,
'title' => $title,
'author' => $author,
'comments' => $comments,
'comment' => $comment
);
if (hasSection('GPS')) {
$gps = $exif['GPS']['GPSAltitudeRef'];
$alt_ref = ($gps !== null) ? $gps : null;
$tags['altref'] = (int)($alt_ref);
$gps = $exif['GPS']['GPSAltitude'];
$gps_alt = ($gps !== null) ? round(divide($gps), 4) : "N/A";
$tags['altitude'] = $gps_alt;
$gps = getGPS();
if ($gps != null) {
$tags['latitude'] = $gps[0];
$tags['longitude'] = $gps[1];
}
} else {
$errStr .= " <tr>\n<td colspan=\"3\"><p>No GPS tags found.<p></td>\n";
}
} else {
$errStr .= " <tr>\n<td colspan=\"3\"><p>No EXIF tags found.</p></td>\n";
}
}
}
function printData() {
global $errStr, $exif, $tags;
$str = "";
if ($tags['comment']) { $str .= " <tr>\n <td colspan=\"3\">\n <p class=\"comment\">" . $tags['comment'] . "</p>\n </td>\n </tr>\n"; }
if ($tags['comments']) { $str .= " <tr>\n <td colspan=\"3\">\n <p class=\"comment\">" . $tags['comments'] . "</p>\n </td>\n </tr>\n"; }
if (!$errStr) {
$str .= " <tr><th scope=\"col\">LATITUDE</th><th scope=\"col\">LONGITUDE</th><th scope=\"col\">ALTITUDE</th></tr>\n <tr>\n";
$str .= " <td>\n <p>" . $tags['latdeg'] . "&deg; " . $tags['latmin'] . "&rsquo; " . $tags['latsec'] . "&rdquo; " . $tags['lathem'] . "</p>\n <p>" . $tags['latitude'] . "&deg;</p>\n </td>\n";
$str .= " <td>\n <p>" . $tags['logdeg'] . "&deg; " . $tags['logmin'] . "&rsquo; " . $tags['logsec'] . "&rdquo; " . $tags['loghem'] . "</p>\n <p>" . $tags['longitude'] . "&deg;</p>\n </td>\n";
$h = isset($_REQUEST['h']) ? $_REQUEST['h'] : 0;
$z = ((intval($h) <= 4 && intval($h) >= -4) ? "&amp;z=" . (string)(17 + intval($h)) : "&amp;z=17");
$str .= " <td>\n <p>" . (($tags['altitude'] == "N/A") ? $tags['altitude'] : $tags['altitude'] . " m " . (($tags['altref']) ? "Below" : "Above") . " Sea Level.") . "</p>\n <p>Go to this <a href=\"http://www.wikimapia.org/#lat=" . $tags['latitude'] . "&amp;lon=" . $tags['longitude'] . $z . "\">location on the map</a></p>\n </td>\n";
} else {
$str .= $errStr;
}
$str .= " </tr>";
echo $str;
}
//

weegillis
06-25-2012, 03:16 PM
Last, the CSS. Everything is rolled into this one external sheet:



body {
margin: 0;
text-align: center;
font-size: 100%;
}
/* viewer */
p,
.exifboxtable th,
#breadcrumb {
font-family: Helvetica Neue, Geneva, Arial, Verdana;
font-size: 0.8em;
color: #303030;
margin: 0;
text-align: left;
}
p {
font-weight: normal;
line-height: 1.6em;
}
img { border: none; }
#header {
position: relative;
height: 50px;
width: 100%;
margin-top: 20px;
margin-bottom: 0;
}
.headerbox {
width: 60%;
height: 50px;
margin-left: 20%;
font-family: Helvetica Neue, Geneva, Arial, Verdana;
font-size: 2em;
font-weight: normal;
color: #d3d3d3;
}
#image {
position: relative;
height: 500px;
}
.imagebox {
float: left;
width: 60%;
height: 500px;
margin: 0 0 0 20%;
overflow: auto;
}
.imageshow { text-align: center; }
#exif {
width: 100%;
}
.exifbox {
width: 50%;
margin-left: 25%;
}
.exifboxtable {
text-align: center;
}
.exifboxtable table {
width: 100%;
padding: 0;
border-collapse: collapse;
border: none;
}
.exifboxtable td,
.exifboxtable th { width: 33%; vertical-align: top; padding: 0; }
.exifboxtable th { font-weight: bold; line-height: 3em; }
#breadcrumb { position: absolute; }
#breadcrumb a { display: block; float: left; margin: 1em 0 0 3em; }
#exifdump { display: none; }
/* navigator */
div#wrap {
width: 98%;
margin: 0 auto;
overflow: auto;
}
div.item {
float: left;
width: 205px;
overflow: auto;
text-align: left;
font: normal 0.8em/1.0em Calibri, Arial, Helvetica, sans-serif;
border: 1px solid #999;
border-radius: 5px;
}
div.thum {
text-align: center;
margin: 5px 0 1em 0;
}
.item ul {
list-style: none;
margin: 1em 0;
padding: 0;
}
.item li {
margin-left: 5px;
}