Check Image Dimensions Without getimagesize()

In a forthcoming project, it was necessary to check the dimensions of a remote image before deciding to cache it or not. While the PHP native function getimagesize() exists, it has some pitfalls. The biggest being: it relies on allow_url_fopen being enabled. While allow_url_fopen is not inherently dangerous, many web hosts do not have it set to prevent issues caused by inexperienced users (including Weblogs.us). Additionally, getimagesize() will retrieve the entire image—a waste of bandwidth, and time. This guide will cover finding the dimensions of images in the PNG and GIF formats with a word on JPEG and why it makes life difficult.

Download Sources

For either image type, we need to retrieve the remote data. In this guide, the following function, getContent(), will be used for all HTTP transactions. It employs PHP’s CURL wrappers (make sure your PHP environment supports CURL). The user can specify the range of bytes to download, if needed. Note that getContent() returns the raw data from CURL or false if there is a curl error.

function getContent($url, $range = null)
{
$curlOpt = array(
CURLOPT_RETURNTRANSFER => true, // Return web page
CURLOPT_HEADER	 => false, // Don't return headers
CURLOPT_FOLLOWLOCATION => true, // Follow redirects
CURLOPT_ENCODING => '', // Handle all encodings
CURLOPT_USERAGENT => 'image dimension grabber', // Useragent
CURLOPT_AUTOREFERER => true, // Set referer on redirect
CURLOPT_FAILONERROR	 => true, // Fail silently on HTTP error
CURLOPT_CONNECTTIMEOUT => 2, // Timeout on connect
CURLOPT_TIMEOUT => 2, // Timeout on response
CURLOPT_MAXREDIRS => 3, // Stop after x redirects
CURLOPT_SSL_VERIFYHOST => 0 // Don't verify ssl
);
//Conditionally set range, if passed in
if($range !== null)
{
$curlOpt[CURLOPT_RANGE] = $range;
}
//Instantiate a CURL context
$context = curl_init($url);
//Set our options
curl_setopt_array($context, $curlOpt);
//Get our content from CURL
$content = curl_exec($context);
//Get any errors from CURL
$error = curl_error($context);
//Close the CURL context
curl_close($context);
//Deal with CURL errors
if(empty($content))
{
return false;
}
return $content;
}

Image Dimensions in PNG

PNG is the more straight forward of the two formats covered in this guide. All multi byte entities are stored in big endian format, which is more natural for humans to read. Some may refer to this as “Motorola” format as the 68k series is big endian (this comes up more often with GIF documentation as that format was developed in the mid to late 1980s when the Motorola 68k was quite popular).

Byte 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
hex 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

All PNG files start with the 8 byte hex sequence: 0x89504E470D0A1A0A (0x89, ‘P’, ‘N’, ‘G’, 13, 10, 26, 10). After this sequence is the beginning of the first chunk, the IHDR chunk. All PNG chunks begin with a 4 byte field denoting the chunk data length in bytes. In the above example, we know the data field of the chunk will be 13 bytes in length. Next comes the 4 byte chunk identifier. While the value of the chunk length may vary from image to image, the chunk identifier for the IHDR chunk is 0x49484452 (‘I’, ‘H’, ‘D’, ‘R’).

Byte 10 11 12 13 14 15 16 17 18 19 1A 1B 1C
hex 00 00 00 3F 00 00 00 21 08 06 00 00 00

Immediately following the IHDR identifier (beginning on byte 0x10) are 4 bytes of the width and then 4 bytes for the height. The rest of the IHDR data field contains: image bit depth, color type, compression type, filter type, and interlace type; each occupying 1 byte for a total of 5 bytes. From the above data field for the IHDR chunk we see that the width is 0x3F (63 pixels) and the height is 0x21 (33 pixels). We really don’t care about anything past the IHDR, and in fact, past byte 23. Therefore, we only need to grab the first 24 bytes of any PNG image to find its dimensions.

Code

Now that we have a basic understanding of the PNG format’s header we can write code to return the dimensions of a PNG file. We will be using a common interface for both getPNGImageXY() and getGIFImageXY. A data string is passed in, and the function returns an array with the width in the first element and height in the second, or false if anything goes wrong.

function getPNGImageXY($data)
{
//The identity for a PNG is 8Bytes (64bits)long
$ident = unpack('Nupper/Nlower', $data);
//Make sure we get PNG
if($ident['upper'] !== 0x89504E47 || $ident['lower'] !== 0x0D0A1A0A)
{
return false;
}
//Get rid of the first 8 bytes that we processed
$data = substr($data, 8);
//Grab the first chunk tag, should be IHDR
$chunk = unpack('Nlength/Ntype', $data);
//IHDR must come first, if not we return false
if($chunk['type'] === 0x49484452)
{
//Get rid of the 8 bytes we just processed
$data = substr($data, 8);
//Grab our x and y
$info = unpack('NX/NY', $data);
//Return in common format
return array($info['X'], $info['Y']);
}
else
{
return false;
}
}

Based on our knowledge of the PNG header format, we know to check for 0x89504E47 as the first 4 bytes and 0x0D0A1A0A as the next 4 bytes. Should this not be the case the function returns false. Next, it removes the 8 bytes that we just processed and then checks for the IHDR chunk. If it is in the expected location, the width and length are read off of the string and placed into an array.

Note that getPNGImageXY() uses unpack() to return formatted variables from the input string. While this is not completely necessary for dealing with PNG images, it makes dealing with GIF images easier and was used for consistency between the two functions.

Example: Find the Dimensions of the Weblogs.us Logo

In this example, the first 24 bytes of the Weblogs.us logo are retrieved and the image dimensions are extracted from them. Then the dimensions are printed to the screen. This code requires getPNGImageXY() and getContent() to be defined before it (e.g. in the same file as in the code archive).

//Want to retrieve the Weblogs.us logo
$url = 'http://weblogs.us/images/weblogs.us.png';
//Only need the first 24 bytes
$data = getContent($url, '0-24');
//Check to make sure it was actually a PNG
if($temp = getPNGImageXY($data))
{
printf('Image width=%s, height=%s', $temp[0], $temp[1]);
}
else
{
echo "Image was not a PNG.";
}

Image Dimensions in GIF

GIF, while not painful like JPEG, it stores multi byte numbers in little endian format. This means that the number 18 (hex 0x12) is represented as 0x1200 if it is stored as 16bit number. Other than this little quirk, the over 20 year old format is easy to use.

Byte 00 01 02 03 04 05 06 07 08 09
hex 47 49 46 38 39 61 12 00 12 00

All GIF files begin with either ‘GIF89a’ or ‘GIF87a’ (0x474946383961 or 0x474946383761). Immediately following the GIF magic word, are 2 bytes for the width and then 2 bytes for the height. Thus, only the first 10 bytes are needed to get the dimensions of a GIF file. Above is the first 10 bytes from a GIF file. It happens to be of the GIF89a format, has a width of 18 pixels, and a height of 18 pixels.

Code

Now that we know what to expect in a GIF formatted file, we can write code to return the dimensions of a such file. For compatibility’s sake, this function will behave externally the same as getGIFImageXY().

function getGIFImageXY($data)
{
//The identity for a GIF is 6bytes (48Bits)long
$ident = unpack('nupper/nmiddle/nlower', $data);
//Make sure we get GIF 87a or 89a
if($ident['upper'] !== 0x4749 || $ident['middle'] !== 0x4638 ||
($ident['lower'] !== 0x3761 && $ident['lower'] !== 0x3961))
{
return false;
}
//Get rid of the first 6 bytes that we processed
$data = substr($data, 6);
//Grab our x and y, GIF is little endian for width and length
$info = unpack('vX/vY', $data);
//Return in common format
return array($info['X'], $info['Y']);
}

The first thing we need to do is verify the file format by looking for the ‘GIF87a’ or ‘GIF89a’ magic word. This code checks these 6 bytes, 2 bytes at a time, returning false if anything is amiss. Next, it removes the 6 processed bytes and grabs the width and height data.

Note that getGIFImageXY() uses unpack(), with good reason, to return formatted variables from the input string. By using the ‘v’ format flag, the integers returned by unpack() are read correctly as little endian rather than big endian. This saves us grief should the function be used in a big endian environment.

Example: Find the Dimensions of the Google Logo

In this example, the first 10 bytes of the Google logo are retrieved and the image dimensions are extracted from them. Then the dimensions are printed to the screen. Since the getGIFImageXY() and getPNGImageXY() functions have the same external interface, the code is almost exactly the same as that for the Weblogs.us logo dimension finder covered earlier.

//Want to retrieve the Google logo
$url = 'http://www.google.com/intl/en_ALL/images/logo.gif';
//Only need the first 10 bytes
$data = getContent($url, '0-10');
//Check to make sure it was actually a GIF
if($temp = getGIFImageXY($data))
{
printf('Image width=%s, height=%s', $temp[0], $temp[1]);
}
else
{
echo "Image was not a GIF.";
}

A Word (or Three) on JPEG

Finding the dimensions of a JPEG image is no trivial matter. Unlike other formats (e.g. PNG and GIF), JPEG does not store its dimension information in its headers. Instead, the dimension information is located at the beginning of the baseline frame. This is immediately after the 0xFFC0 magic word. Preceding the this in the file is:

  • header information
  • ASCII comments
  • EXIF data
  • thumbnail data (a JPEG file embedded within the current file)
  • Huffman coding tables for the frame

All of this can easily take over 5KiB, in some cases there is over 1KiB of white space (either actual ASCII spaces or hex zeros). Hence, it is impossible to know how much data must be retrieved to find the image dimensions before opening a particular JPEG file. Thus, this guide does not cover the JPEG format. However, in the comments section for the documentation of getimagesize() there is a function that will try to determine the dimensions.

-John Havlik

[end of transmission, stay tuned]

15 thoughts on “Check Image Dimensions Without getimagesize()

  1. Very useful article. Being able to determine image size without loading the entire image is awesome :-). This could definitely save a lot of bandwidth!

    • In some cases the savings can be large (these methods typically require less than 0.1% of the image’s size). However, the actual numbers are small, only talking about a few thousand bytes for most PNG and GIF images. However, in an environment that does these checks often, the savings in time will be substantial.

      -John Havlik

  2. that’s really nice code and much faster that getimagesize() but have you checked the png code? because its not working, it only getting me “Image was not a PNG” even the image is PNG!
    so why is that?

    • Yes, it’s true.
      The Png function code is not working for me neither.

      plus i don’t understand the hex to use the jpeg.
      please can you double check the png function

      thanks

      • Mohsen,

        I’ve double checked the code in the .zip archive and the code on this page for the PNG image size checker. They are the same, and are the same as the code in the demo running on this site. I’m wondering if it is an environmental issue (byte encoding in particular). What version of PHP, OS, and processor type are you running this on? I developed it in a PHP5, 64bit Linux (2.6.x kernel), Intel Atom 330 environment, and this site is hosted on a PHP5, 64bit Linux (2.6.x kernel), Intel Core 2 environment.

        For finding the image size of a JPEG file, there is a function in comments of the getimagesize() function. For it to work you may need to have a significant amount of the JPEG image downloaded (which didn’t mesh well with what I was using this code for). The gist of what is going on with JPEG images is you are looking for that HEX magic word, immediately after it there will be your width and height.

        -John Havlik

    • Hamid,

      Is the PNG image corrupted? The IHDR chunk must come first (after the PNG declaration magic word). The first 16 bytes in every PNG file are determined by the spec (see explanation above).

      -John Havlik

      • Hmm, it works now. i uploaded it on a remote server to try and it work perfectly but when i try it on my localhost on the computer its not working, i have tried with wamp and xampp so why is that do you think?

        • Hamid,

          It may be due to Windows being your host OS. I haven’t tested this case much as I have a local Linux testbed. My guess is that unpack is functioning differently in Windows (IIRC the meaning of the parameters passed in change). Have you had a chance to test the GIF code?

          -John Havlik

          • Yes the GIF is working very well on the localhost and also the remote server. :)
            and now we are trying to developing your code to get all images from a remote url, with a validate of the images dimensions to get all images between 100px and 1000px without downloading all images in one time, and just download one by one. and that to improve the speed.

          • Hamid,

            Ok, well if the GIF is working then I’d say it is most likely a unpack issue on Windows causing the PNG one to not work (you can verify this by looking at what unpack returns in the cases it is used).

            -John Havlik

  3. Hey guys, just thought I’d chime in here on those having trouble with the PNG images.

    According the unpack docs (http://us.php.net/manual/en/function.unpack.php):

    Note that PHP internally stores integral values as signed. If you unpack a large unsigned long and it is of the same size as PHP internally stored values the result will be a negative number even though unsigned unpacking was specified.

    If you add these two lines just after line 4 in the getPNGImageXY function above:

    if($ident['upper'] < 0) $ident['upper'] += 4294967296;
    if($ident['lower'] < 0) $ident['lower'] += 4294967296;

    That should solve your problems.

    You can thank Nhon’s comment in the unpack docs (linked above) for this one.

    • John,

      So that fixes it? To be honest, your suggested fix does not make much sense (if it works). If I was comparing decimal (base 10) values it would. However, aware of the behavior you referred to I used hex values in my comparison.

      Hex values should not be affected by if PHP pretends an unsigned long is signed or not. The bits don’t change and the hex representation is the same. For example, an 8bit variable with hex value of 0x9C is -100 if the variable is signed, or 156 in an unsigned variable. Treating it as signed changes the decimal (base 10) representation, but does not affect the actual bits or the hex representation.

      -John Havlik

      • Well! It is working under windows.
        Without those 2 lines of code for png it does not working at all!

        • With the above “fix” are you getting the correct dimensions out of the function? I ask as all of the reads are 4 byte reads and if the first one causes issue in Windows, the rest should as well.

          Note that the above “fix” will most likely only work on Windows systems. 64bit linux systems, which I develop and deploy on, don’t experience this issue, and the “fix” will actually confuse them.

          The real solution, at the moment, is to split all of the 4 byte reads into two 2 byte reads. This will avoid the problem on all systems. But, it also makes the code harder to read and longer since PNG is best read in 4 byte chunks.

          -John Havlik

  4. First, thanks very much for this code and explanation; I need this functionality.

    Adding my two cent’s worth, on a Windows XP machine the png code does give the message
    “Image was not a PNG.”
    copying and pasting in the fix results in the message
    “Warning: unpack() [function.unpack]: Type N: not enough input, need 4, have 2 in \image_size\pngExample.php on line 84
    Image was not a PNG.”
    “line 84” is line 13.

    I’m just now digging into this and I’ll boot to linux and continue on because I need this functionality and for it to also work with .jpg images. I’ll try the code in the comments section of getimagesize and combine everything into one function.

Comments are closed.