Using PHP to Extract Image Exif Data

Posted by Nessa | Posted in uncategorized | Posted on 02-08-2010

8

Those of us fluent in digital photography have come across the term “Exif data” numerous times when it comes to software we use to digitally manipulate photographs. Exif (Exchangeable image file format) data is generally used to identify the properties of the camera that snapped a picture, and usually the software that altered it afterwards. It can tell you when a picture was taken, what kind of camera took it, as well as the camera’s model, shutter speed, focal length, and even provide a thumbnail of the image on the camera’s LCD screen.

Why would you need to extract this information?  If you’re ever uploaded images to stock photography sites and wonder how they know so much about your pictures, it’s because they extract the Exif data from your pictures to provide more information on how they were taken. This quick tutorial will demonstrate how to extract Exif data from an image using PHP.

Enabling the Exif Extension

The Exif functions for PHP may not be native to your installation, so you can check by viewing your phpinfo file or running “php -m” via command line to see a list of modules compiled in. If you don’t see Exif listed, there are three ways you can enable it depending on how you installed PHP:

  • If you compiled PHP manually, you can re-compile while adding –enable-exif to the configure line
  • If PHP is installed via package (rpm/deb), it should already have Exif enabled. If not, you can install an RPM for the extension manually
  • If you use cPanel, run EasyApache and select the Exif extension from the PHP module list, and recompile

Determining the Image Type

The exif_imagetype function identifies the format of an image, but returns the result as a code.  The PHP function reference provides a full list of these return codes, but 1-8 are the most common out of the 16:

1: GIF
2: JPEG
3: PNG
4: SWF
5: PSD
6: BMP
7/8: TIFF

Here’s a code example that lists all the desired valid image types in an array and detects the type of image from the specified file, returning the result in “human readable” format:

<?php
$image = "/path/to/myimage";

$types = array(
1 => "GIF",
2 => "JPEG",
3 => "PNG",
4 => "SWF",
5 => "PSD",
6 => "BMP",
7 => "TIFF",
8 => "TIFF"
);

$imagetype = exif_imagetype($image);

if (array_key_exists($imagetype, $types)) {
echo "Image type is: " . $types[$imagetype];
} else {
echo "Not a valid image type";
}
?>

Reading Exif Header Data

The exif_read_data function can be used to extract header data from JPEG and TIFF files:

<?php
$image = "/path/to/myimage";
$exif = exif_read_data($image, 0, true);
foreach ($exif as $key => $section) {
foreach ($section as $name => $val) {
echo "$key.$name: $val\n";
}
}
?>

This will return the elements of the array from exif_read_data, which can be very long depending on what information is available for the image. There are seven sections (arrays) of data types:

  • FILE:  Contains the file’s name, size, timestamp, and what other sections were found (as listed below)
  • COMPUTED: Contains the actual attributes of the image
  • ANY_TAG:  Any information that is tagged
  • IFD0:  Mostly contains information about the camera itself, the software used to edit the image, when it was last modified, etc
  • THUMBNAIL: Information about the embedded thumbnail for the image
  • COMMENT: Comment headers for JPEG images
  • EXIF: Contains more information supplementary to what is in IFD0, mostly related to the camera (includes focal length, zoom ratio, etc)

Depending on the information available for the image, you’ll actually see a lot of data in the output. Say, for instance, you want to only output the IFD0 data to see the information of the camera that took the image:

<?php
$image = "image.jpg";
$exif = exif_read_data($image, 0, true);

foreach ($exif as $key => $section) {
foreach ($section as $name => $val) {
if($key == "IFD0"){
echo "$key.$name: $val\n";
}
}
}
?>

This will output:

IFD0.ImageWidth: 2592
IFD0.ImageLength: 3872
IFD0.BitsPerSample: Array
IFD0.Compression: 1
IFD0.PhotometricInterpretation: 2
IFD0.Make: NIKON CORPORATION
IFD0.Model: NIKON D80
IFD0.Orientation: 1
IFD0.UndefinedTag:0x0000:
IFD0.XResolution: 72/10000
IFD0.YResolution: 72/1
IFD0.PlanarConfiguration: 1
IFD0.ResolutionUnit: 2
IFD0.Software: Adobe Photoshop CS4 Windows
IFD0.DateTime: 2010:02:06 22:24:09
IFD0.Exif_IFD_Pointer: 304

Or, you can further narrow down the output by specifying specific values in the $exif multi-dimensional array:

<?php
$image = "image.jpg";
$exif = exif_read_data($image, 0, true);
echo "Software: " . $exif['IFD0']['Software'] . "\n";
?>

This will return:

Software: Adobe Photoshop CS4 Windows

Using Exif to generate a thumbnail

As touched on previously, many cameras and image manipulation software will include an embedded thumbnail for an image. You can extract this thumbnail using the exif_thumbnail function:

<?php
$image = "image.jpg";
$thumbnail = exif_thumbnail($image, $width, $height, $type);
echo "<img  width='$width' height='$height' src='data:image;base64,".base64_encode($thumbnail)."'>";
?>

Keep in mind that the thumbnail generated here is from the Exif data – there are other ways to create a thumbnails using many of the PHP image functions.

Simple PHP Script for RBL Checking

Posted by Nessa | Posted in uncategorized | Posted on 16-07-2010

0

It’s useful for ISP’s and email service providers to run occasional RBL checks against their IPs to know when they are being blacklisted by populate CBL services. I’ve written a simple script that utilizes the DNSBL pear library to check against common blacklists, when given a list of IPs in a file.

First, you need to download or install the NET_DNSBL pear module. (Command: pear install NET_DNSBL)

<?php
require_once('Net/DNSBL.php');

$iplist = file("/path/to/iplist");

foreach ($iplist as $ip){

$dnsbl = new Net_DNSBL();

$dnsbl->setBlacklists(array(
'sbl-xbl.spamhaus.org',
'dnsbl.sorbs.net',
'bl.spamcop.net',
'dnsbl-1.uceprotect.net',
'dnsbl-2.uceprotect.net',
'dnsbl-3.uceprotect.net',
'isps.spamblocked.com',
'zen.spamhaus.org'
));

if ($dnsbl->isListed($ip)) {

echo "IP $ip is blacklisted!\n";

}

else {
echo "IP $ip not listed\n";
}
}

?>

Of course, this script can be very easily modified to pull IPs from a database, or assign the $ip variable from a GET or POST request (like from a form or API).  Here’s an exclusive list of some RBL’s you can check against using this script, by adding them to the array shown:

asiaspam.spamblocked.com
bl.deadbeef.com
bl.emailbasura.org
bl.spamcop.net
blackholes.five-ten-sg.com
blacklist.woody.ch
bogons.cymru.com
cbl.abuseat.org    cdl.anti-spam.org.cn
combined.abuse.ch
combined.rbl.msrbl.net
db.wpbl.info
dnsbl-1.uceprotect.net
dnsbl-2.uceprotect.net
dnsbl-3.uceprotect.net
dnsbl.abuse.ch
dnsbl.ahbl.org
dnsbl.cyberlogic.net
dnsbl.inps.de
dnsbl.njabl.org
dnsbl.sorbs.net
drone.abuse.ch
duinv.aupads.org
dul.dnsbl.sorbs.net
dul.ru
dyna.spamrats.com
dynip.rothen.com
eurospam.spamblocked.com
fl.chickenboner.biz
http.dnsbl.sorbs.net
images.rbl.msrbl.net
ips.backscatterer.org
isps.spamblocked.com
ix.dnsbl.manitu.net
korea.services.net
lacnic.spamblocked.com
misc.dnsbl.sorbs.net
noptr.spamrats.com
ohps.dnsbl.net.au
omrs.dnsbl.net.au
orvedb.aupads.org
osps.dnsbl.net.au
osrs.dnsbl.net.au
owfs.dnsbl.net.au
owps.dnsbl.net.au
pbl.spamhaus.org
phishing.rbl.msrbl.net
probes.dnsbl.net.au
proxy.bl.gweep.ca
proxy.block.transip.nl
psbl.surriel.com
rbl.interserver.net
rdts.dnsbl.net.au
relays.bl.gweep.ca
relays.bl.kundenserver.de
relays.nether.net
residential.block.transip.nl
ricn.dnsbl.net.au
rmst.dnsbl.net.au
sbl.spamhaus.org
short.rbl.jp
smtp.dnsbl.sorbs.net
socks.dnsbl.sorbs.net
spam.dnsbl.sorbs.net
spam.rbl.msrbl.net
spam.spamrats.com
spamlist.or.kr
spamrbl.imp.ch
t3direct.dnsbl.net.au
tor.ahbl.org
tor.dnsbl.sectoor.de
torserver.tor.dnsbl.sectoor.de
ubl.lashback.com
ubl.unsubscore.com
virbl.bit.nl
virus.rbl.jp
virus.rbl.msrbl.net
web.dnsbl.sorbs.net
wormrbl.imp.ch
xbl.spamhaus.org
zen.spamhaus.org

Using PHP to Perform DNS Lookups

Posted by Nessa | Posted in uncategorized | Posted on 30-06-2010

4

PHP has a couple DNS functions you can use to perform record lookups.

Most of us are familiar with the two basic ones – gethostbyname() and gethostbyaddr(), both of which perform a single function – returning a hostname or IP address. Here’s an example of both:

<?php

$ip = gethostbyname("v-nessa.net");
$host = gethostbyaddr("69.174.114.71");

echo "v-nessa.net has the IP $ip, which reverses to $host";
?>

The above will return:

v-nessa.net has the IP 69.174.114.71, which has a PTR of server.v-nessa.net

Similarly to gethostbyname, there’s gethostbynamel which is useful for hostnames with multiple A records:

$ips = gethostbynamel("test.v-nessa.net");
foreach ($ips as $ip => $value){
echo $value . "\n";
}

Will return:

69.174.114.71
69.174.115.243

A more advanced function is dns_get_record, which can pull any valid record for a hostname or IP.  Think about the dig command you use in Unix to find DNS records:

nessa@nessa-desktop:~$ dig +short v-nessa.net A
69.174.114.71

The dns_get_record function works in a similar way, and can obtain the following DNS record types:

DNS_A, DNS_CNAME, DNS_HINFO, DNS_MX, DNS_NS, DNS_PTR, DNS_SOA, DNS_TXT, DNS_AAAA, DNS_SRV, DNS_NAPTR, DNS_A6, DNS_ALL or DNS_ANY.

The following will give you a similar result:

$recs = dns_get_record("v-nessa.net", DNS_A);
print_r($recs);

Will return:

Array
(
[0] => Array
(
[host] => v-nessa.net
[type] => A
[ip] => 69.174.114.71
[class] => IN
[ttl] => 13728
)
)

If you want to obtain multiple DNS types, you can do so by separating the record types with a plus sign:

$recs = dns_get_record("v-nessa.net", DNS_A + DNS_MX);

Will return:

Array
(
[0] => Array
(
[host] => v-nessa.net
[type] => A
[ip] => 69.174.114.71
[class] => IN
[ttl] => 13736
)

[1] => Array
(
[host] => v-nessa.net
[type] => MX
[pri] => 0
[target] => v-nessa.net
[class] => IN
[ttl] => 14145
)

)

You’ll notice that the output is a double array, so to call individual values you can do either of the following:

// will return the IP for array 0 ( A record)

echo $recs[0]['ip'];

// will return results for common records

foreach ($recs as $type => $value){
echo $value[ip] . "\n";
}

Similar to the example above, you can use the DNS_ALL type to show any records available for the hostname, and use a minus sign to exclude certain record types. For example, the below code will return all DNS results for v-nessa.net, but exclude NS records:

$recs = dns_get_record("v-nessa.net", DNS_ALL - DNS_NS );

foreach ($recs as $type => $value){
echo $value[type] . "\n";
}

The following records were returned:

A
SOA
MX
TXT

Here’s a more practical example – a simple PHP DNS lookup script. Say you have a form on your site that allows a user to type in a hostname and select a type of record: A, MX, NS, and TXT. The passed form values are $host and $type, and the below script will accept the variables and output the results according to the record type:

HTML Form:

<form action="dns.php" method="POST">
Hostname: <input type="text" name="host" />
Type: <select name="type">
<option value="a">A</option>
<option value="mx">MX</option>
<option value="ns">NS</option>
<option value="txt">TXT</option> </select>
</form>

PHP Script (dns.php):

<?php

if(!empty($_POST['host']) && !empty($_POST['type'])){

    // Grab variable from form and define valid types

    $host = $_POST['host'];
    $type = strtoupper($_POST['type');
    $validtypes=array("A","MX","NS","TXT");

    // Check that dns type is defined or allowed

    if(!defined("DNS_" . $type) or !in_array($type,$validtypes)){
       echo "Invalid DNS Type!";
    }else{

       $type = constant("DNS_" . $type);
       $rec = dns_get_record($host, $type);

       // Set result types - can be modified by using available elements from $rec array

       switch($type){
             case DNS_A:
                    $recvals=array("Hostname" => "host","Type" => "type", "IP" => "ip");
                    break;
             case DNS_MX:
                    $recvals=array("Hostname" => "host","Type" => "type", "Target" => "target", "Priority" => "pri");
                    break;
             case DNS_NS:
                    $recvals=array("Hostname" => "host","Type" => "type", "Target" => "target");
                    break;
             case DNS_TXT:
                    $recvals=array("Hostname" => "host","Type" => "type", "Record" => "txt");
                    break;
        }

      // Output results

      foreach ($rec as $arr => $num){
             foreach ($recvals as $title => $value){
                    echo $title . " : " . $num[$value] . "\n";
             }
      }

    }
} else {

     echo "Either hostname or record type is missing";
}

It’s of course easy to modify the above code accordingly, and I’m sure there may be better ways to do this. Feel free to post your own code snippets or comments.

Command Line PHP: Part 3

Posted by Nessa | Posted in uncategorized | Posted on 25-05-2010

4

This is part third and final part in my PHP command line tutorial series. If you didn’t see parts 1 and 2:

Command Line PHP: Part 1

Command Line PHP: Part 2

Command Line PHP: Part 2

Posted by Nessa | Posted in uncategorized | Posted on 21-05-2010

3

This post is continuing on my three-part series on command line PHP programming. Missed part one? It’s right behind you. This part will go over command execution and processes.

Command Line PHP – Part 1

Posted by Nessa | Posted in uncategorized | Posted on 18-05-2010

13

PHP isn’t just for websites anymore. In fact, almost every script I’ve written to perform server-side functions is either written in bash or PHP, rather than Perl or Python as preferred by my colleagues. It’s a common belief that PHP isn’t suited for CLI programming since it’s mainly used in web applications, but PHP has over a hundred functions specifically intended for system management.

These kinds of posts can be rather lengthy, so I’m making this into a series with three parts.  Part 1 will go over the basic filesystem functions. You can find a complete listing here, but I’ll just go over a few of the more important and common ones.

Sexifying WHM with XML API

Posted by Nessa | Posted in uncategorized | Posted on 07-06-2008

5

.!.

I don’t know about you other cPanel system admins out there, but I find WHM to be very useful for the more advanced and time-consuming tasks, such as installing SSL certificates. However, the easy stuff like changing an account’s package and resetting passwords is a royal pain in the ass as far as convenience is concerned when you have to log into WHM, list accounts, and make whatever change.

I recently became favorable towards the WHM XML API functionality which will let me do a majority of the everyday account-related tasks from command line without ever opening my browser, which is a lot easier when managing thousands of users across multiple servers. Below are a couple scripts I’ve put together using the XML API from a base script in the cPanel forums:

Change account password

Change account package

Both are run via command line, and the arguments passed to the PHP script as variables. For example, to change an account’s password:

./chacctpass myuser mypass1234

Customizing these scripts to perform different functions is easy via the following steps:

- change if ($argc != 3) to the number of command line arguments you wish to pass to the script plus one. In the above example there are two arguments and since the script name counts, add one and that makes 3.

- in the section where the arguments are assigned to variables (like $cpuser, etc), name your variables. The first one should have an array value of 0, then 1, 2, etc.

- edit the usage example, which will come up if the required number of arguments is not provided…you can add any text you like

- if you’re using a hash (which is more secure than user/pass authentication), go fetch your remote access key from WHM and put it in the $hash value within quotes, format intact. Otherwise, put in your WHM user’s username and password

- change the $server variable to your server’s hostname

- change $apipath to the WHM path for the function you are using. You can find a whole list of them here, and most will give you the path to use in the examples sections. In the API path, insert your variable names where the values are suppose to be. For instance:

$apiPath = “/xml-api/passwd?user=myuser&pass=mypass1234″;

Would be:

$apiPath = “/xml-api/passwd?user=$cpuser&pass=$newpass”;

In the header section, uncomment whichever $header .= “Authorization: line that matches your authentication method (user/pass or hash)

Once you’ve configured your API script, chmod to 700 and run from the command line as show in my example. It’s better to lock down the script by changing its ownership only to the user that will be using it, and not giving read, write, or execute permissions to anyone else.

Note: for these scripts to work you have to have PHP compiled with OpenSSL support, otherwise change the socket variables to http over port 2086.

Whitespace is Evil

Posted by Nessa | Posted in uncategorized | Posted on 19-12-2007

2

I have this recurring nightmare of PHP apps that don’t trim whitespace when entering info. As a habitual copy and paster, it would be nice if some programmers could tend to my laziness and reluctance to type. Yes, Tony, I’m talking about you. Ever hear of the trim() function? Yea, it’s the Brazilian wax of PHP you inconsiderate bastard.

$text = "some text with extra spaces ";
$trimmed = trim($text);
echo $trimmed;

More of Using PHP for Server Info

Posted by Nessa | Posted in uncategorized | Posted on 17-12-2007

1

I’ll eventually get the whole thing up here, but I’ve been working on a simple server info script to help me and the other members of the system admin team keep up with the gazillion servers we have and all their different configurations. One of the reasons it’s taking so long (aside from my recent alcohol binges) is that it has to be portable to every server without the need for specific modifications, regardless of their setup. This eventually calls for using a simple if statement and empty() function to decide what info to output. Really, it’s so easy that I don’t even know why I’m posting it, but it kinda supplements this and this.

I’ll take the Ruby example I used earlier to find out what version of Ruby is installed:

$rubyver = exec("ruby -v |awk {'print $2'}"); ?>

Most of our servers don’t have Ruby installed, so I instead of getting an ugly ass error or nothing at all, I’d rather the script gracefully output its absence:

if (empty($rubyver)) {
echo "<font color='red'>Ruby is not installed on this server</font>";
} else {
echo "Ruby Version $rubyver";
}

This code fragment will check the output of the $rubyver variable, so if Ruby is not installed then the variable will return no value. Since the variable is then considered empty, the first echo statement is executing telling the viewer that Ruby is not installed on the server. If Ruby is installed on the server, then the second echo statement will run.

Common PHP Errors

Posted by Nessa | Posted in uncategorized | Posted on 07-12-2007

7

I’m going back to the basics here, you know, when you wrote your first PHP script and saw an ugly-ass error message pop up on your screen? Error messages are the best tool a programmer has.

Set up Error Reporting

Most PHP errors are straight forward, but there are times where you don’t see any which makes it very difficult to tell what the problem is.

The first step of PHP troubleshooting is to turn error reporting on. For security reasons you’ll want error reporting off by default, but if something goes wrong you’ll need the information for debugging. You can usually enable error reporting by adding this line to the problem script:

<?php error_reporting(E_ALL) ?>

Or you can add these lines to the root .htaccess:

php_flag display_errors on
php_value error_reporting 6143

This will usually display an error useful for troubleshooting, that is, if the software and your server configuration allows it.

Parse Errors

Parse error: parse error, unexpected T_STRING in……

This is a syntax error. Perhaps you forgot a semi-colon at the end of a line, or you forgot a double quote (“) or an end bracket (}) after you started one. For quote and semicolon issues, the problem is usually the line above the one reported in the error. For brackets, it may be at the end of the script.

Parse error: syntax error, unexpected $end in

You’re most likely missing a } somewhere. Make sure that each { you have is also closed with a }.

Parse error: syntax error, unexpected T_STRING, expecting ‘,’ or ‘;’ in..

There may be double quotes within double quotes. They either need to be escaped or brought to single quotes. It’s also possible that a new PHP statement was started before the previous was finished.

Header Errors

Warning: Cannot add header information – headers already sent by (output started at /home/vnessa5/www/errors.php:9) in….

Warning: Cannot send session cache limiter – headers already sent in somefile.php on line 222

Naturally, HTML will parse before PHP. The script is trying to send header information after you’ve already sent output to the browser. HTTP headers are required to be sent before any output from your script, which means that a header function must be placed before any html or even a white space. There are two solutions for this. Either (1) Set the header tags the top of the document, or (2) insert a header redirect by adding this to the very top of the page to force the output buffer:

<?php ob_start();

Then this at the very end of the page (not usually required)

ob_end_flush(); ?>

mySQL Result Source Errors

Warning: Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in…

You need to take a look at the $result variable used to define the loop. More than likely there is a syntax error on the reported line before the $result field, or the value of $result does not exist.

Supplied argument is not a valid stream resource…

This is usually caused when your code is looking for a table or other resource in the database that does not exist.

Sessions are not being created or maintained

This can apply to any of the scenarios below:

(1) The program isn’t remembering your login
(2) Your shopping cart won’t hold items
(3) Your php script is redirecting like crazy
(4) “Call undefined function session_start” error
(5) PHP isn’t processing pages called by something like index.php?page=home&id=7

Your site is most likely dependent on register_globals. You can enable them by putting this line in your .htaccess (or just enabling in your php.ini if you have access):

php_flag register_globals On

Stream Errors

Warning: failed to open stream…

Warning: main(/index.php): failed to open stream: No such file or directory in…

This is usually because either the specified file is missing, or a file declared in a require() or include() function is missing. The easiest way to fix this is by re-installing the PHP program from a freshly-uploaded copy, or restoring the original config.php and just changing the db information. The include path may also be incorrect, but either way your script is looking for a file that isn’t there, or it is looking in the wrong place.

Warning: fopen(…): failed to open stream: Permission denied in…

This is a permissions and/or ownership issue. Try first setting the permissions to 777 just to see if the script will run. If so, you should narrow down the permissions to 775. If not, set the user/group to user:nobody.

Warning: <…> is not a valid stream resource…

Warning: fread(): supplied argument is not a valid stream resource in…

This is an error seen when trying to use functions like fopen(), fread(), feof(), etc. and are usually caused by an invalid or unavailable resource that is being called in the line specified. For instance, if the fread() function is returning this error, it could be that the file it is trying to access does not have the correct permissions or does not exist.

Warning: Failed opening….

Warning: Failed opening ‘…’ for inclusion (include_path=’.:/usr/local/lib/php’) in Unknown on line 0

Make the sure that the file mentioned (and its holding directorie) has read + execute permissions, and that the path to the file is correct. If not, you’ll need to add the path into the PHP code: (or .htaccess)

include(“/path/to/files”);
Blank PHP Pages

You go to a .php page, but it’s blank.

The scope of what can cause blank pages is very broad, but there are a few things to look at:

-Is error reporting turned off anywhere in the script or in the .htaccess? If so, turn it on to see what is happening (php_flag display_errors on), or add the lines at the top of this page into the script.

-Is the PHP script even generating any output (usually you can tell my finding the print function?

-Check the database connection, i.e, username, dbname, user added to db, etc.

-Try using the full <?PHP ?> tags, rather than the shorter versions <? ?>

Also, if the software is prebundled (like phpBB or Gallery), then the index or one of the include pages could be corrupted. Usually you can just replace the problem page with a working version from another installation.
Max Execution Time Error

You receive some variant of a “Max_execution_time” error when loading a page.

This is caused when a PHP script takes longer to execute than the server allows, but can be adjusted by adding a PHP directive to your .htaccess: (in seconds, 0 = unlimited) or modifying the value in php.ini.

php_value max_execution_time 0

Open_basedir Errors

Warning: Unknown(): open_basedir restriction in effect.

This is a protective feature of Apache that restricts PHP from accessing files/folders outside the user’s home directory. Most of the time this is due to an incorrect include path in one or more of the config files (which are usually mentioned). Look for something like this:

/includes/somefile.php
/admin/files/anotherfile.php

The heading / tells the filesystem that these folders are on the server root, and thus prevents PHP from accessing them. You can usually fix this by changing the path to these files to be absolute to their location:

/home/username/public_html/includes/somefile.php

or

./includes/somefile.php