Chapter 4. Graphics

Hacks 27–33: Introduction

It’s easy to think of PHP as nothing more than a scripting language for HTML. But PHP is far more than that, with support for databases, graphing, image manipulation, and a lot more. This chapter details hacks for building beautiful graphics with bitmaps, vector graphics, and even Dynamic HTML (DHTML). You’ll even see how you can take photos from your iPhoto library and export them into HTML—all with that “HTML scripting language,” PHP.

Create Thumbnail Images

Use the GD graphics API in PHP to create thumbnails of your images.

This simple hack takes a set of JPEG images in a directory named pics and creates thumbnails of them in a directory named thumbs. It also creates a file in the same directory as the script called index.html, which contains all of the thumbnails, as well as links to the original images.

The Code

Save the code in Example 4-1 as mkthumbs.php.

Example 4-1. A script for handling thumbnail creation
	<?php
	$dir = opendir( "pics" );
	$pics = array();
	while( $fname = readdir( $dir ) )
	{
		if ( preg_match( "/[.]jpg$/", $fname ) )
			$pics []= $fname;
	}
	closedir( $dir );
	foreach( $pics as $fname )
	{
	  $im = imagecreatefromjpeg( "pics/$fname" );
	  $ox = imagesx( $im );
	  $oy = imagesy( $im );
	
	  $nx = 100;
	  $ny = floor( $oy * ( 100 / $ox ) );
	
	  $nm = imagecreatetruecolor( $nx, $ny );

	  imagecopyresampled( $nm, $im, 0, 0, 0, 0, $nx, $ny, $ox, $oy );
	
	  print "Creating thumb for $fname\n";
	
	  imagejpeg( $nm, "thumbs/$fname" );
	 }
	
	 print "Creating index.html\n";
	
	 ob_start();
	 ?>
	 <html>
	 <head><title>Thumbnails</title></head>
	 <body>
	 <table cellspacing="0" cellpadding="2" width="500">
	 <tr>
	 <?php
	 $index = 0;
	 foreach( $pics as $fname ) {
	 ?>
	 <td valign="middle" align="center">
	 <a href="pics/<?php echo( $fname ); ?>"><img src="thumbs/<?php echo( $fname );
		?>" border="0" /></a>
	 </td>
	 <?php
	 $index += 1;
	 if ( $index % 5 == 0 ) { echo( "</tr><tr>" ); }
	 } 
	 ?>
	 </tr>
	 </table>
	 </body>
	 </html>
	 <?php
	 $html = ob_get_clean();
	 $fh = fopen( "index.html", "w" );
	 fwrite( $fh, $html );
	 fclose( $fh );
	?>

The script starts by iterating through the pictures in the pics directory. It then creates a thumbnail for each image in the thumbs directory using the GD imagint functions.

To create a thumbnail, the file first has to be read in and handled by the imagecreatefromjpeg() function. After that, the new (thumbnail) size is calculated, and a new image is created (using imagecreatetruecolor()). The original file is then copied in and resized using the imagecopyresampled() function. Finally, the thumbnail is saved with imagejpeg().

The rest of the script creates an HTML index for the thumbnails by using the output buffering functions ob_start() and ob_get_clean(); both are used to store the HTML into a string. That string is then written into a file using fopen(), fwrite(), and fclose().

Running the Hack

Place the mkthumbs.php script into a directory, and then create two subdirectories: pics and thumbs. In the pics directory, place a bunch of JPEG images. Run the script with the PHP command-line interpreter:

	% php mkthumbs.php

This creates all of the thumbnail images and the index.html file in Figure 4-1.

The HTML file showing the thumbnails
Figure 4-1. The HTML file showing the thumbnails

This type of script can be really handy for creating family photo albums. I wish more people would use some sort of thumbnail script. Far too often, I get a message from my friends or relatives pointing me to an Apache directory listing of images from their recent trip—all full size and with none of the blurry or bad shots removed! Lucky for all of us, PHP can turn those reels into a manageable set of thumbnails, and still preserve the originals.

See Also

Create Beautiful Graphics with SVG

Use the SVG XML standard to create scalable graphics that render beautifully.

Adobe’s Scalable Vector Graphics (SVG) XML standard provides a whole new level of graphics functionality to PHP web applications. In this hack, I’ll use a web page and a simple PHP script to create a scalable vector graphic.

Tip

It’s important to note that before you can view an SVG image you must have an SVG viewer plug-in installed in your browser. Adobe hosts plug-in viewers on its web site, http://www.adobe.com/svg/main.html. SVG is an open standard, which Adobe strongly supports. The SVG.org site (http://svg.org/) is an open community supporting the standard across multiple browsers and now even cell phones.

Figure 4-2 demonstrates how the SVG plug-in interacts with the circle_svg. php script, which generates the SVG. The SVG object embedded on the page requests the XML from the script, and the script then returns the XML with the SVG plug-in plots.

The SVG plug-in requesting SVG from the circle_svg.php script
Figure 4-2. The SVG plug-in requesting SVG from the circle_svg.php script

The Code

Save the code in Example 4-2 as index.html.

Example 4-2. HTML for demonstration purposes
	<html>
	<body>
	<embed width="400" height="400" src="circle_svg.php" name="printable"
		type="image/svg+xml" />
	</body>
	</html>

The work is done in circle_svg.php, shown in Example 4-3.

Example 4-3. Where the real SVG work occurs
	<?php
	header( "content-type: text/xml" );
	
	$points_count = 20;

	$points = array();
	for( $p=0; $p<$points_count; $p++ )
	{
		$d = ( 360 / $points_count ) * $p;
		$x = 50 + ( cos( deg2rad( $d ) ) * 50 );
		$y = 50 + ( sin( deg2rad( $d ) ) * 50 );
		$points []= array( 'x' => $x, 'y' => $y );
	}
	
	echo ("<?xml version=\"1.0\" standalone=\"no\"?>\n" );
	?>
	<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN"
		"http://www.w3.org/TR/SVG/DTD/svg10.dtd">
	<svg style="shape-rendering:geometricPrecision;" viewBox="0 0 100 100" xml
		space="preserve" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://
		www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
	<?php
	foreach( $points as $start ) {
			$sx = $start['x'];
			$sy = $start['y'];
	foreach( $points as $end ) {
			$ex = $end['x'];
			$ey = $end['y'];
	?>
		<path fill-rule="nonzero" style="fill:#000000;stroke:#FF0000;stroke-width:0.2"
				d="M<?php echo( $sx." ".$sy ); ?> L<?php echo( $ex." ".$ey ); ?> Z"/>
	<?php
	} }
	?>
	</svg>

Running the Hack

Install both files on your server and browse to them in your SVG-enabled browser. In this case, I used Internet Explorer; the result is shown in Figure 4-3.

The circle rendered with SVG
Figure 4-3. The circle rendered with SVG

The HTML page embedded an SVG object, which triggered a call to the circle_svg.php script to get the XML code for the SVG file. That PHP page creates an SVG image, which is simply a bunch of vectors connecting the various points on a circle, much like a Spirograph. Users can even click and zoom around the image if they want, and the image will scale appropriately since it’s based on vector graphics.

SVG supports a huge range of graphics and effects features. It also supports animation and has JavaScript scriptability (I have used none of that here for the sake of simplicity). In fact, when you think about SVG in terms of functionality, you should probably consider it to be on the same level as Flash 8, with the notable exception that SVG uses XML files rather than compiled SWF files. The big downside with SVG, of course, is the install base, which at the time of this writing was far smaller than that of Flash.

If you are interested in using XML to build Flash movies, check out Laszlo (http://www.laszlosystems.com/), an open source XML compiler that builds SWF movies.

See Also

Simplify Your Graphics with Objects

Use the object-oriented features of PHP to simplify your graphics using layering, object-oriented drawing, and viewport scaling.

PHP’s support for graphics is great. But when you try to build a complex visualization, PHP becomes difficult to use for several reasons. First, the drawing order is really important. Things that you draw first will be covered by the stuff that you draw later (and there’s no good way to get around that limitation). This means that you have to sequence your code based on the drawing order, even when it’s difficult to do so programmatically.

Another problem is scaling. To draw into an image, you have to know how big the image is and how to scale your drawing. That means passing around a lot of information about the drawing context. Add that to the layering issues, and PHP image code becomes a real mess.

Lucky for all of us hackers who aren’t graphics pros, all of these problems have been solved via graphics libraries like PHP’s GD library. In this hack, I build a simple object API for graphics that manages drawing order through z buffering, and handles scaling by creating a viewport.

The Code

Save the code in Example 4-4 as layers.php.

Example 4-4. Defining several classes used to layer graphics
<?php
class GraphicSpace
{
    var $image;
	var $colors;

	var $xoffset;
	var $yoffset;	
	var $xscale;
	var $yscale;
	
	functionGraphicSpace()
	{
		$this->colors = array();
	}
	
	function get_image() { return $this->image; }
	function set_image( $im )			
	{
		$this->image = $im;
	}

	function get_color( $id ) { return $this->colors[ $id ]; }
	function set_color( $id, $color ) { $this->colors[ $id ] = $color; }
	
	function set_viewport( $left, $top, $right, $bottom )
	{
		$this->xoffset = $left;
		$this->yoffset = $top;
		
		$this->xscale = imagesx( $this->image ) / ( $right - $left );
		$this->yscale = imagesy( $this->image ) / ( $bottom - $top );
	}
	
	function transform_x( $x ) { return ( $x - $this->xoffset ) * $this->xscale; }
	function transform_y( $y ) { return ( $y - $this->yoffset ) * $this->yscale; }
	function scale_x( $x ) { return $x * $this->xscale; }
	function scale_y( $y ) { return $y * $this->yscale; }
  }

  class RenderItem
  { 
	var $left; 
	var $right;
	var $top;
	var $bottom;
	var $color;
	var $z;

	function RenderItem( $left, $top, $right, $bottom, $color, $z )
	{
		$this->left = $left;
		$this->right = $right;
		$this->top = $top;
		$this->bottom = $bottom;
		$this->color = $color;
		$this->z = $z;
	}

		function get_left() { return $this->left; }
		function get_right() { return $this->right; }
		function get_top() { return $this->top; }
		function get_bottom() { return $this->bottom; }
		function get_z() { return $this->z; }

		function render( $gs ) { }
		function transform( $x, $y ) { }
	}
	 
	class Line extends RenderItem
	{ 
	var $sx;
	var $sy;
	var $ex;
	var $ey;
	var $thickness;
		
	function Line( $sx, $sy, $ex, $ey, $color, $z, $thickness )
	{
		$this->RenderItem( min( $sx, $ex ), min( $sy, $ey ),
				max( $sx, $ex ), max( $sy, $ey ),
				$color, $z );
				
		$this->sx = $sx;
		$this->sy = $sy;
		$this->ex = $ex;
		$this->ey = $ey;
		$this->thickness = $thickness;
		}
		function render( $gs )
		{
		
			if ( $this->thickness > 1 )
			  imagesetthickness( $gs->get_image(), $this->thickness );
			$this->drawline( $gs->get_image(),
				$gs->transform_x( $this->sx ),
				$gs->transform_y( $this->sy ),
				$gs->transform_x( $this->ex ),
				$gs->transform_y( $this->ey ),
				$gs->get_color( $this->color ) );
				
			if ( $this->thickness > 1 )
			   imagesetthickness( $gs->get_image(), 1 );
		}
		function drawline( $im, $sx, $sy, $ex, $ey, $color )
		{
			imageline( $im, $sx, $sy, $ex, $ey, $color );
		}
	  }
	  
	  class DashedLine extends Line 
	  {
	  function drawline( $im, $sx, $sy, $ex, $ey, $color )
	  {
		imagedashedline( $im, $sx, $sy, $ex, $ey, $color );
	  }
	}
	
	class Ball extends RenderItem
	{
		var $text;
		
		function Ball( $x, $y, $size, $color, $text, $z )
		{
			$width = $size / 2;
			if ( $text )
			  $width += 20;
			 $this->RenderItem( $x, $y,
				$x + $width, $y + ( $size / 2 ),
				$color, $z );
				
			$this->text = $text;
			$this->size = $size;
	    }
		
		function render( $gs )
		{
			imagefilledellipse( $gs->get_image(),
				$gs->transform_x( $this->left ),
				$gs->transform_y( $this->top ),
				$gs->scale_x( $this->size ),
				$gs->scale_x( $this->size ),
				$gs->get_color( $this->color ) );
		    if ( strlen( $this->text ) )
			  imagestring($gs->get_image(), 0,
				$gs->transform_x( $this->left ) + 7,
				$gs->transform_y( $this->top )-5,	$this->text,
				$gs->get_color( $this->color ) );
			}
		  }
		  
		  function zsort( $a, $b )
		  {
			if ( $a->get_z() == $b->get_z() )
				return 0;
			return ( $a->get_z() > $b->get_z() ) ? 1 : -1;
		  }
		  
		  class RenderQueue
		  {
			var $items;
			
			function RenderQueue() { $this->items = array(); }
			function add( $item ) { $this->items [] = $item; }
		function render( $gs )
		{
			usort( &$this->items, "zsort" );
			foreach( $this->items as $item )	{$item->render( $gs );}
		}
		function get_size()
		{
			$minx = 1000; $maxx = -1000;
			$miny = 1000; $maxy = -1000;
			foreach( $this->items as $item )
			{
				if ( $item->get_left() < $minx )
				$minx = $item->get_left();
				if ( $item->get_right() > $maxx )		
				$maxx = $item->get_right();
				if ( $item->get_top() < $miny )
				$miny = $item->get_top();
				if ( $item->get_bottom() > $maxy )
				$maxy = $item->get_bottom();
			}
			return array( left => $minx, top => $miny, right => $maxx, bottom => $maxy );
			}
		}
		$width = 400;
		$height = 400;

		function calcpoint( $d, $r )
		{
			$x = cos( deg2rad( $d ) ) * $r;
			$y = sin( deg2rad( $d ) ) * $r;
			return array( $x, $y );
		}

		$render_queue = new RenderQueue();
   
		$ox = null;
		$oy = null;

		for( $d = 0; $d < 380; $d += 10 )
		{
			list( $x, $y ) = calcpoint( $d, 10 );
	
			$render_queue->add( new Ball( $x, $y, 1, "line", "", 10 ) );
			$render_queue->add( new Line( 0, 0, $x, $y, "red", 1, 1 ) );

			if ( $ox != null && $oy != null )
			{
			$render_queue->add( new Line( $ox, $oy, $x, $y, "red", 1, 1 ) );
			}
			$ox = $x;
			$oy = $y;
		}
	
		$gsize = $render_queue->get_size();

		$fudgex = ( $gsize['right'] - $gsize['left'] ) * 0.1;
		$gsize['left'] -= $fudgex;
		$gsize['right'] += $fudgex;
		$fudgey = ( $gsize['bottom'] - $gsize['top'] ) * 0.1;	
		$gsize['top'] -= $fudgey;
		$gsize['bottom'] += $fudgey;
		
		print_r( $gsize );

		$im = imagecreatetruecolor( $width, $height );
		imageantialias( $im, true );
		$bg = imagecolorallocate($im, 255, 255, 255);
		imagefilledrectangle( $im, 0, 0, $width, $height, $bg );
	
		$gs = new graphicspace();
		$gs->set_image( $im );
		$gs->set_color( 'back', $bg );
		$gs->set_color( 'line', imagecolorallocate($im, 96, 96, 96) );
		$gs->set_color( 'red', imagecolorallocate($im, 255, 0, 0) );
		$gs->set_viewport( $gsize['left'], $gsize['top'], $gsize['right'],
		$gsize['bottom'] );
		
		$render_queue->render( $gs );
	
		imagepng( $im, "test.png" );
		imagedestroy( $im );
		?>

Figure 4-4 shows the layout of the classes in this script. RenderQueue refers to two objects: an array of RenderItems, and a GraphicSpace that holds the image and the transformation information. The derived classes of RenderItem create the different types of shapes: lines, balls, and dashed lines. To add more items, just add more child classes of RenderItem.

Each RenderItem has a z level associated with it. Items that have a lower z level (or z value) will be rendered behind items that have a larger z value. This allows you to create objects in any order you like, assign them z values, and know that they will be sorted and rendered in the proper order.

For the coordinate system, I used a mechanism called a viewport. The viewport is a virtual graphics space. The coordinates can be anything you like, ranging from 0 to 1 or from 0 to 1 billion. The system automatically scales the graphics to the size of the image.

The UML of the graphics objects
Figure 4-4. The UML of the graphics objects

Starting with this simple object API, you can create much more complex graphics far more easily than if you used the PHP graphics API directly. The object code is also far easier to understand and maintain.

Running the Hack

Use the PHP command-line interpreter to run the layers.php script:

	% php layers.php
	Array
	(
	[left] => -12.05
	[top] => -12.05
	[right] => 12.55
	[bottom] => 12.55
)

Then use your browser to look at the resulting test.png file, shown in Figure 4-5.

A circle built with graphics objects
Figure 4-5. A circle built with graphics objects

There are three sets of objects in this graph: the lines that go from the center to the balls, the gray balls, and the lines that go between the balls along the perimeter. The lines radiating out from the center are at a z level of 1. The balls are at a z level of 10. And the lines that run along the perimeter are at a z level of 20.

To change the z order of the perimeter lines, you can just change the z value from, for example, 20 to 1:

	if ( $ox != null && $oy != null )
	{
		$render_queue->add( new Line( $ox, $oy, $x, $y, "red", 1, 1 ) );	
	}

Now if you rerun the script and look at the results in the browser, you’ll see that the balls are on top of the lines (see Figure 4-6), whereas before they were underneath them.

Dropping the connectors behind the gray balls
Figure 4-6. Dropping the connectors behind the gray balls

Now the perimeter lines have dropped behind the gray balls because of the lower z order.

See Also

Split One Image into Multiple Images

Use PHP’s graphics engine to break a single large image into multiple small images.

Sometimes it’s handy to have a group of smaller images that make up a single image rather than the one small image. An example is “Create the Google Maps Scrolling Effect” [Hack #26] , which scrolls many smaller images around the screen seamlessly, creating the effect of moving around one large image. To accomplish that trick, though, you might have to break up a large image first; this hack does just that.

The Code

Save the code in Example 4-5 as imgsplit.php.

Example 4-5. Breaking up images
	<?php
	$width = 100;
	$height = 100;
	
	$source = @imagecreatefromjpeg( "source.jpg" );
	$source_width = imagesx( $source );
	$source_height = imagesy( $source );
	
	for( $col = 0; $col < $source_width / $width; $col++)
	{
		for( $row = 0; $row < $source_height / $height; $row++)
		{
			$fn = sprintf( "img%02d_%02d.jpg", $col, $row );
			
			echo( "$fn\n" );
			
			$im = @imagecreatetruecolor( $width, $height );
			imagecopyresized( $im, $source, 0, 0,
				$col * $width, $row * $height, $width, $height,
				$width, $height );
			imagejpeg( $im, $fn );
			imagedestroy( $im );
			}
		} 
		?>

This is the inverse of the code in Example 4-6 in the upcoming “Hacking the Hack” section of this hack. It creates—from a single big image—lots of smaller images and puts them in a grid. It’s a useful match for the Google Maps scrolling effect hack in “Create the Google Maps Scrolling Effect” [Hack #26] .

The constants at the top of the file define how big the output images should be. The script reads in the source image and figures out how big it is; then it uses a set of nested for loops to iterate around all of the grid items, creating an image, copying the section from the original image, and then saving the image.

Running the Hack

This code is run on the command line using PHP’s command-line interpreter:

	% php imgsplit.php
	img00_00.jpg
	img01_00.jpg
	…

The script looks for a file called source.jpg and breaks it up into a set of files named img<col>_<row>.jpg, where the col and row items are padded with zeroes. So the image in column zero, row zero would be named img00_00.jpg. Each created image is 100x100 pixels. Those values are set with the $width and $height values at the top of the script.

Hacking the Hack

Instead of splitting images, how about merging images? The next script creates a single large image from a collage of smaller images. You can use this code as the foundation of a scrolling panorama, as shown in “Create the Google Maps Scrolling Effect” [Hack #26] , or simply as a collage of multiple images suitable for a background or desktop image.

Save the code shown in Example 4-6 as imgmerge.php.

Example 4-6. Merging images, yet another task PHP can handle easily
	<?php
	$targetsize_x = 4000;
	$targetsize_y = 4000;
	$outfile = "merged.jpg";
	$quality = 100;

	$im = @imagecreatetruecolor( $targetsize_x, $targetsize_y );
	
	$sources = array();
	$dh = opendir( "." );
	while (($file = readdir($dh)) !== false)
	{
		if ( preg_match( "/[.]jpg$/", $file ) &&
			$file!= $outfile )
		{
			$sources []= imagecreatefromjpeg( $file );
		}
	}
	
	$x = 0;
	$y = 0;
	$index = 0;
	while( true )
	{
		$width =imagesx( $sources[ $index ] );
		$height = imagesy( $sources[ $index ] );
		
		imagecopy( $im, $sources[ $index ],
			$x, $y, 0, 0, $width, $height );
			
		$x += $width;
		if ( $x >= $targetsize_x )
		{
			$x = 0;
			$y += $height;
			if ( $y >= $targetsize_y )
				break;
		}

		$index += 1;
		if ( $index >= count( $sources ) )
			$index = 0;
	}

	imagejpeg( $im, $outfile, $quality );

	imagedestroy( $im );
	?>

This is a fairly simple script that takes image files from a directory and stores them into an array of sources. Then the script creates a huge image, into which it copies the original source images. The while loop wraps around the large image, creating rows of smaller images, until it gets to the bottom of the output image. At the end of the script, the large composite image is saved as a JPEG using imagejpeg().

This code is run from the command line in a directory full of JPEG images.

Tip

The input files must end with the .jpg extension, and all of the input images must be the same size. Otherwise, the composite will have lots of “empty” spaces in it (if it doesn’t crash altogether).

An example image is shown in Figure 4-7.

The command is run in this way:

	% php imgmerge.php
One of the sample source images
Figure 4-7. One of the sample source images

The output file is created in the same directory as the source images, and it is named merged.jpg. With some sample images of my wife (Lori) and my daughter (Megan), I created the composite shown in Figure 4-8.

The completed graphic, shown scaled down in Firefox
Figure 4-8. The completed graphic, shown scaled down in Firefox

Even better—and nothing more than a happy accident—Firefox does something pretty cool here. It scales the image down to fit it within the browser. If you hold the mouse over the image, the cursor will turn into a magnifying glass, and you can zoom in on the composite to see it at 100% magnification.

You can adjust the quality of the merged image by tweaking the $quality value. This value goes from 0 to 100, with 100 being the best quality. $targetsize_x and $targetsize_y define the desired width and height of the merged image, and the $outfile variable specifies the filename of the merged image.

See Also

Create Graphs with PHP

Use PHP’s image toolkit to create dynamic graphs from your data.

PHP has excellent dynamic imaging capabilities. You can use these to overlay images [Hack #32] , or to create whole new images on the fly. This hack uses the image toolkit to do some simple scientific graphing of sine waves (proving that PHP is great for math as well as for imaging).

The Code

Save the code in Example 4-7 as graph.php.

Example 4-7. Graphing a mathematical function
	<?
	$width = 400;
	$height = 300;	

	$data = array();
	for( $i = 0; $i < 500; $i++ )
	{
		$data []= sin( deg2rad( ( $i / 500 ) * 360 ) );
	}
	

	$xstart = $width/10;
	$ystart = $height - ($height/10);
	
	$image = imagecreate($width, $height);
	$back = imagecolorallocate($image, 255, 255, 255);
	$border = imagecolorallocate($image, 64, 64, 64);
	
	imageline( $image, $xstart, 0, $xstart, $ystart, $border );imageline( $image, $xstart, $ystart, $width, $ystart, $border );
	
	imagestring( $image, 2, $xstart-20, $ystart-10, "1", $border );
	imagestring( $image, 2, $xstart-20, 0, "-1", $border );
	imagestring( $image, 2, $xstart, $ystart+5, "0", $border );
	imagestring( $image, 2, $width-20, $ystart+5, "360", $border );
	
	$datatop = 1;
	$databottom = -1;
	
	$oldx = 0;
	$oldy = 0;
	$datacount = count( $data );
	$xscale = ( $width - $xstart ) / $datacount;
	$yscale = $ystart / ( $datatop - $databottom );
	$midline = $ystart / 2;
	for( $i = 0; $i < $datacount; $i++ )
	{
		$x = $xstart + ( $i * $xscale );
		$y = $midline - ( $data[$i] * $yscale );
		if ( $i > 0 )
		{
			imageline( $image, $oldx, $oldy, $x, $y, $border );
		}
		$oldx = $x;
		$oldy = $y;
	}

	header("Content-type: image/png");
	imagepng($image);
	imagedestroy($image);	
	?>

The script starts with some constants that define the size of the output image. Then the new image is created, and colors are allocated. Next, the border of the graph is drawn, along with the axis values using imagestring(). With the axis values in place, the script draws the mathematical data using a for loop to iterate over each data point and uses the imageline() function to draw a line between the current position and the previous position.

Because this script is intended for use on the Web, the content type of the output needs to be set properly to image/png, which tells browsers to expect a PNG graphic. Many browsers will automatically detect image content, but it’s best to set the content type properly. With that done, the image is output using the imagepng() function.

It’s best to leave the content-type header for the end of the script; that way, if the script fails, you will see the error results in the browser. If you set the header too early, the browser will get the content type and attempt to interpret the PHP error message as a PNG image.

Running the Hack

Put the files up on the PHP server and navigate to the graph.php page. You should see something like Figure 4-9.

The resulting graph
Figure 4-9. The resulting graph

If you don’t see the graph in Figure 4-9, it’s likely that there is a server configuration problem. PHP is very flexible about how it’s installed, and the image library doesn’t need to be installed for PHP (in general) to run properly; but without the graphing libraries (obviously), you won’t get a graphical PNG (you should see an error message).

See Also

Create Image Overlays

Using PHP’s graphics capabilities to build a single image from several source images.

One common graphics scenario is to put some overlay images at specific data-driven locations, stacking those overlays on top of another base graphic. This hack starts with the map in Figure 4-10 as the base image.

The map graphic
Figure 4-10. The map graphic

Then it places the star graphic in Figure 4-11 onto the map, over the city of San Francisco, as it might appear if you were looking up a location by city or Zip code.

The star graphic
Figure 4-11. The star graphic

The Code

Save the (rather simple) code in Example 4-8 as graphic.php.

Example 4-8. PHP making overlaying graphics almost trivial
	<?php
	$map =imagecreatefrompng("map.png");
	$star = imagecreatefromgif("star.gif");	
	imagecopy( $map, $star, 5, 180, 0, 0, imagesx( $star ), imagesy( $star ) );
	header("Content-type: image/png");
	imagepng($map);
	?>

The code starts by reading in the map and star graphics. Then it creates a new image, superimposing the star onto the map using the imagecopy() function. The new version of the map—which at this point exists only in memory—is then output to the browser using the imagepng() function.

Running the Hack

After uploading the PHP script and the images to your server, navigate your browser to graphic.php. There you will see an image like that shown in Figure 4-12.

The star graphic overlaid on the map graphic
Figure 4-12. The star graphic overlaid on the map graphic

Hacking the Hack

The star on the map is cool, but it’s a bit big. Instead of sitting on top of San Francisco, it ends up sitting on top of most of California. Let’s scale it down a little. Save the code in Example 4-9 as graphic2.php.

Example 4-9. A little bit of scaling
	<?
	$map =imagecreatefrompng("map.png");
	$star = imagecreatefromgif("star.gif");
	imagecopyresized( $map, $star, 25, 205, 0, 0,
		imagesx( $star )/5, imagesy( $star )/5,
		imagesx( $star ), imagesy( $star ) );
	header("Content-type: image/png");
	imagepng($map);
	?>

Then navigate to the new script in your web browser; you shouldsee the graphic shown in Figure 4-13.

The map overlaid with a scaled version of the star graphic
Figure 4-13. The map overlaid with a scaled version of the star graphic

This new version of the script uses the imagecopyresized() function to change the size of the star image as it’s copied onto the map. The script divides the star’s width and height by 5, scaling the image to 20% of its original size.

Tip

Because the star is a pixilated graphic, if you make the image larger than the original, you’ll start to see some jagged edges.

See Also

Access Your iPhoto Pictures with PHP

Use PHP’s XML capabilities to parse through iPhoto’s picture database.

Apple is a company known for producing innovative and easy-to-use products. Following on that line, it recently released the iLife suite (http://www.apple.com/ilife/), which makes it easy to produce and organize rich media. I was a bit dismayed by my options for sharing my photos from iPhoto, though. In particular, after having imported my digital photos from my camera and organizing them using iPhoto, I wanted to show off these pictures to family and friends. I didn’t want to sign up for hosting, open an account with a photo printing service, wait for hundreds of files to upload somewhere, export photos to a smaller size, or reorganize all of my images in some other program after having already done the work in iPhoto. I wanted them available to everybody—right now—and I didn’t want to have to lift a finger to make it so. I’d already done plenty of work by taking the actual photos, not to mention organizing and captioning them!

This is what got me working on myPhoto (http://agent0068.dyndns.org/~mike/projects/myPhoto). One Mac OS X feature that most users often do not notice is the built-in web server; Mac OS X includes both Apache and PHP, and both are itching to be enabled. When you combine this and a broadband connection with all of the information readily available in iPhoto, sharing photos becomes (as it should be) a snap.

If your PHP project requires a photo gallery component, it might be tempting to place the burden on users to upload, caption, and organize all of their photos into your system. However, if users have already done the work in iPhoto, do the rest for them! Armed with a simple XML parser, it’s possible to extract all of the meaningful data from iPhoto and reformat it into a simpler format that’s more appropriate and convenient for use with PHP.

A Look Behind the Scenes: iPhoto Data

The first logical step is to get up close and personal with iPhoto so that you know what data is easily available.

Tip

I am basing this discussion on iPhoto Version 5.x, the most current version of iPhoto available as of this writing. With a few small tweaks here or there, though, it’s trivial to apply these same concepts to other versions of iPhoto—something I’ve been doing since iPhoto 2.0.

Figure 4-14 shows a small selection from my iPhoto album.

iPhoto showing pictures from my wedding
Figure 4-14. iPhoto showing pictures from my wedding

A quick look in ~/Pictures/iPhoto Library/ shows almost everything we could ever need from iPhoto:

Directories broken down by date

For instance, ~/Pictures/iPhoto Library/2005/07/02/ contains photos from July 2, 2005. The image files in this directory are the actual full-size photos, but they contain all of the edits the user made from within iPhoto (i.e., rotations, color corrections, etc.). It also contains two other subdirectories: Thumbs, which contains 240 x 180 thumbnails corresponding to each image, and Originals, which contains the original, unmodified versions of the images (only if the user has performed any edits in iPhoto). Furthermore, in nearly all cases, these photos are in JPEG format, which is perfect for the Web.

Tip

One notable exception: if the user takes photos in RAW format (available on higher-end cameras), the Originals directory contains the RAW files and all other images are JPEG representations.

AlbumData.xml

This XML document contains all of the really interesting (and uninteresting) data surrounding these photos: file paths for a given photo, captions, ratings, modification dates, etc. This file also contains information about groups of photos—also called albums—as well as user-defined keywords. Some version information and meta-information is included as well, but that’s not terribly helpful.

So now we need to make some sense of that AlbumData.xml file. First off, it’s not just any XML file; it’s an Apple Property List. This means that a limited set of XML tags is being used to represent common programmatic data structures like strings, integers, arrays, and dictionaries (also known as associative arrays in some languages). Therefore, for the interesting structures within this file, we should look at some sample content, since the XML tags themselves aren’t terribly descriptive. Rather, the tagged content is where the meaty structure is. I’ve cut some pieces out for the sake of brevity, but the more important parts of the file are here.

The beginning of the file looks something like this—not terribly interesting:

	<?xml version="1.0" encoding="UTF-8"?>
	<plist version="1.0">
	<dict>
		<key>Application Version</key>
		<string>5.0.4 (263)</string>
		<key>Archive Path</key>
			<string>/Users/mike/Sites/myPhoto/iPhoto Library</string>

But further down is a listing of all the photos in the dictionary keyed by unique identifiers for each photo. In the following example, you can see that we’re looking at an individual photo with a unique ID of 5. Furthermore, it’s an image (rather than, say, a video) which has a caption of “No more pictures, please” as well as an optional keyword associated with it (the keyword’s unique keyword ID is 2):

	<key>Master Image List</key>
	<dict>
	<key>5</key>
	<dict>
		<key>MediaType</key>	
		<string>Image</string>
		<key>Caption</key>
		<string>No more pictures, please</string>
		<key>Aspect Ratio</key>
		<real>0.750000</real>
		<key>Rating</key>
		<integer>0</integer>
		<key>DateAsTimerInterval</key>
		<real>62050875.000000</real>
		<key>ImagePath</key>
		<string>/Users/mike/Sites/myPhoto/iPhoto Library/2002/12/19/DSC00107.JPG</string>
		<key>OriginalPath</key>
		<string>/Users/mike/Sites/myPhoto/iPhoto Library/2002/12/19/Originals/DSC00107.JPG</string>
		<key>ThumbPath</key> 
		<string>/Users/mike/Sites/myPhoto/iPhoto Library/2002/12/19/Thumbs/5.jpg</string>
		<key>Keywords</key>
		<array>
			<string>2</string>
		</array>
	</dict>
	<key>6</key>
	…and so on…
	</dict>

Another section of this file (shown in the next fragment of XML) lists all user-defined groups of photos, known in iPhoto as albums. These are stored in a user-defined order in an array (unlike the Master Image List, which is unordered and stored by keys). This includes all kinds of albums—normal albums, smart albums, folders, slideshow albums, book albums, etc. Various album attributes are described—a unique ID, a name, an ordered list of photo IDs for photos contained in the album, an indicator if the album is the “master” album (each photo library should have only one master album), the parent album ID if this album is in a “folder album,” etc.:

	<key>List of Albums</key>
	<array>
	<dict>
		<key>AlbumId</key>
		<integer>2</integer>
		<key>AlbumName</key>
		<string>Vacation to somewhere</string>
		<key>KeyList</key>
		<array>
			<string>4425</string>
			<string>4423</string>
			<string>4421</string>
			<string>4419</string>
		</array>
		<key>Master</key>
		<true/>
		<key>PhotoCount</key>
		<integer>2868</integer>
		<key>Parent</key>
		<integer>2196</integer>
	</dict>
	<dict>
	…and so on…
	</dict>
	</array>

Also worth noting is that there is a structure whose key is “List of Rolls,” which is structurally identical to “List of Albums.” This automatically-generated list groups photos together each time they are imported into iPhoto, treating the group as if it were one “roll” of film.

Finally, the last major section of the file is the list of keywords, a dictionary keyed by IDs. These are user-defined keywords that you can use to tag multiple photos, instead of manually captioning each photo with the same word. This consists of ID/keyword pairs; in this example, the ID is 1 and the keyword is _Favorite_:

	<key>List of Keywords</key>
	<dict>
	<key>1</key>
	<string>_Favorite_</string>
	<key>2</key>
	<string>…and so on…
	</dict>

Tip

Keep in mind that in older versions of iPhoto, the file format is slightly different; be sure you know and understand this file for the versions of iPhoto you plan on being compatible with. Minor details do change periodically, and they can cripple your parsing code if you don’t anticipate or account for them.

The Code

Save the code in Example 4-10 as iphoto_parse.php.

Example 4-10. Handling iPhoto XML parsing
	<?php
	//$curTag denotes the current tag that we're looking at in string-stack form
	//$curKey denotes the current tagged attribute so that we have some recollection
	//of what the last seen attribute was.
	// i.e. $curKey="AlbumName" for <key>AlbumName</key>
	//$data denotes the element between tags.
	// i.e. $data="Library" for <string>Library</string>
	//When reading code, note that $curKey is not necessarily equal to $data.

	$curTag="";
	$curKey="";
	$readingAlbums=false;
	$firstTimeAlbum=true;
	$firstTimeAlbumEntry=true;

	$readingImages=false;
	$firstTimeImage=true;
	$firstTimeImageEntry=true;
	$curID=0;

	$masterImageList=array();
	
	class Photo
	
	{
		var $Caption; 
		var $Date; 
		var $ImagePath; 
		var $ThumbPath;
	}

	function newPhoto($capt, $dat, $imgPath, $thumb) {
		$aPhoto=new Photo();
		$aPhoto->Caption=$capt;
		$aPhoto->Date=$dat;
		$aPhoto->ImagePath=$imgPath;
		$aPhoto->ThumbPath=$thumb;
		return $aPhoto;
	}
	//this function is called on opening tags
	function startElement($parser, $name, $attrs)
	{
		global $curTag;
		$curTag .= "^$name";
	}

	//this function is called on closing tags
	function endElement($parser, $name)
	{
		global $curTag;
		$caret_pos = strrpos($curTag,'^');
		$curTag = substr($curTag,0,$caret_pos);
	}

	//this function has all of the real logic to look at what's between the tags
	function characterData($parser, $data){
		global $curTag, $curKey, $outputAlbums, $outputImages,
			   $readingAlbums, $firstTimeAlbum, $firstTimeAlbumEntry,
			   $readingImages, $masterImageList, $firstTimeImage,
			   $firstTimeImageEntry, $curID;

		//do some simple cleaning to prevent garbage
		$data = str_replace('!$-a-0*', '&', $data);
		if(!ereg("(\t)+(\n)?$", $data) && !ereg("^\n$", $data))
								  //if $data=non-whitespace
		{
			//some common place-signatures…really just a list of unclosed tags
			$albumName = "^PLIST^DICT^ARRAY^DICT^KEY"; //album attributes, i.e
				"AlbumName"
			$integerData = "^PLIST^DICT^ARRAY^DICT^INTEGER";//album ID
			$stringData = "^PLIST^DICT^ARRAY^DICT^STRING"; //the actual album name
			$albumContents = "^PLIST^DICT^ARRAY^DICT^ARRAY^STRING"; //photo ID number
			$majorList = "^PLIST^DICT^KEY";		//"List of Albums", "Master Image
				List"
			$photoID = "^PLIST^DICT^DICT^KEY"; //the unique ID of an individual
				photo
			$photoAttr="^PLIST^DICT^DICT^DICT^KEY"; //"Caption", "Date", "ImagePath", etc
			$photoValStr="^PLIST^DICT^DICT^DICT^STRING"; //caption, file paths, etc
			$photoValReal="^PLIST^DICT^DICT^DICT^REAL"; // date, aspect ratio, etc

			if($curTag == $majorList)
			{
				if($data=="List of Albums")
				{
				//flag so that there's no ambiguity, i.e. for <key>List of Rolls</key>
				$readingAlbums=true;
				$readingImages=false;
				}
				else if($data=="Master Image List")
				{
				$readingAlbums=false;
				$readingImages=true;
				}
				else
				$readingAlbums=false;
			}

			if($readingAlbums)
			{
				if ($curTag==$integerData)
				{
				if($data == "AlbumId")
				{
				$curKey = $data;
				}
				}
				else if ($curTag==$albumName) //we're looking at an attribute, i.e
				AlbumName
				{				//so the next thing we'll see is the album name
				//or the listing of all photos contained in the album
				if($data == "AlbumName" || $data="KeyList")
				{
				$curKey = $data; //$curKey will be that reminder for us next time
				}
				}
				else if($curTag == $stringData || $curTag == $integerData)
				//now we are looking at interesting data….
				{
				if($curKey == "AlbumName") //so the last attribute we saw was AlbumName…
				{
				$curAlbum = $data; //say the album name was "Library"…
						       //then now $data="Library"
				$curAlbum = str_replace("&amp;", '&', $data);

				$serializedObj = "";
				if(!$firstTimeAlbum)
				$serializedObj.="\n\t\t)\n\t,\n";
				$serializedObj .= "\t\"".addslashes($curAlbum)."\" =>\n\t\tarray(\n";
				$firstTimeAlbum=false;
				fileWrite($outputAlbums,$serializedObj,'a');
				$firstTimeAlbumEntry=true;
				}
				}
				else if($curTag == $albumContents) // looking at a listing of photos {
				if($curKey == "KeyList")
				{
				//$data==the photo ID number of a photo in $curAlbum
				$serializedObj = "";
				if(!$firstTimeAlbumEntry)
				$serializedObj.=",\n";
				$serializedObj .= "\t\t\t$data";
				fileWrite($outputAlbums,$serializedObj,'a');
				$firstTimeAlbumEntry=false;
				}
			}
			//fill in all your other album cases of interest…
		}
		else if($readingImages)
		{
			if($curTag==$photoID) //we've encountered a new photo, store the ID…
			{
				$curID="";
				if(!$firstTimeImage)
				$curID="),\n";
				$curID.="\t\"$data\"=>array(";
				$firstTimeImageEntry=true;
				$firstTimeImage=false;
			}
			else if($curTag==$photoAttr)
			{
				if($data=="Caption" || $data=="DateAsTimerInterval" ||
				$data=="ImagePath" || $data=="ThumbPath")
				$curKey=$data;
				else
				$curKey="";
			}
			else if($curTag==$photoValStr || $curTag==$photoValReal)
			{
				if($curKey == "Caption" || $curKey == "DateAsTimerInterval" ||
				$curKey=="ImagePath" || $curKey=="ThumbPath")
				{
				if(!$firstTimeImageEntry)
				$curID.=", ";

				if($curKey=="Caption")
				$curID .= "\"caption\"=>\"".addslashes($data)."\"";
				else if($curKey=="DateAsTimerInterval") //timeinterval based dates
									//are measured in seconds from 1/1/2001
				$curID .= "\"date\"=>\"".
						date("F j, Y, g:i a", mktime(0,0,$data,1,1,2001)).
						"\"";
				else
				$curID .= "\"$curKey\"=>\"$data\"";
				$firstTimeImageEntry=false;
				}
				if($curKey=="ThumbPath") //the last attribute we see for a photo…
				fileWrite($outputImages,$curID,'a');
				//…and any other image data worth extracting…
				}
			}
		}
	}

	//this function is what you call to actually parse the XML
	function parseAlbumXML($albumFile)
	{
		global $outputAlbums, $outputImages;
		$xml_parser = xml_parser_create();
		xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);
		//hook the parser up with our helper functions
		xml_set_element_handler($xml_parser, "startElement", "endElement");
		xml_set_character_data_handler($xml_parser, "characterData");
		if (!($fp = fopen($albumFile, "r")))
			die("Can't open file: $albumFile");
		fileWrite($outputAlbums,"<?php\n\$albumList = array (\n",'w');
		fileWrite($outputImages,"<?php\n//key=photo ID, value={",'w');
		fileWrite($outputImages," [0]caption, [1]date, [2]image ",'w');
		fileWrite($outputImages,"path, [3]thumb path}\n\$masterList = array (\n",'w');
		while ($data = fread($fp, 4096))
		{
			$data = str_replace('&', '!$-a-0*', $data);
			if (!xml_parse($xml_parser, $data, feof($fp)))
			{
				die(sprintf("$albumFile : ".$lang["errXMLParse"].": %s at line %d",
				xml_error_string(xml_get_error_code($xml_parser)),
				xml_get_current_line_number($xml_parser)));
			}
		}
		fileWrite($outputAlbums,"\n\t\t)\n\t\n\n);\n?>",'a');
		fileWrite($outputImages,")\n);\n?>",'a');
		//we're done, throw out the parser
		xml_parser_free($xml_parser);
		echo "Done parsing.";
	}

	function fileWrite($dest, $dataToWrite, $writeMode)
	{
		global $err;
		if (is_writable($dest))
		{
			if (!$fp = fopen($dest, $writeMode))
			$err .= "Can't open file: ($dest) <br>";
			else
			{
				if (!fwrite($fp, $dataToWrite))
				$err .= "Can't write file: ($dest) <br>";
				fclose($fp);
			}
		}
		else
			$err .= "Bad file permissions: ($dest) <br>";
	}
	set_time_limit(0); //if you have an enormous AlbumData.xml,
	//PHP's default 30-second execution time-out is the enemy

	$outputImages="out_images.php";
	$outputAlbums="out_albums.php";
	parseAlbumXML("myPhoto/iPhoto Library/AlbumData.xml");
	?>

Also, to use the output from the preceding parser, save the code in Example 4-11 as iphoto_display.php; this file will handle displaying the photos on the Web.

Example 4-11. The script displaying the photos
	<?php
	include "out_images.php";
	$photoIDs=array_keys($masterList);
	$thumbsPerPage=6;
	$thumbsPerRow=3;
	if(!isset($_GET["tStart"]))
		$thumbStart=0;
	else
		$thumbStart=$_GET["tStart"];
	if($thumbStart+$thumbsPerPage>count($photoIDs))
		$thumbLimit=count($photoIDs);
	else
		$thumbLimit=$thumbStart+$thumbsPerPage;
	echo "<table border=\"0\" width=\"100%\">\n";
	for($x=$thumbStart; $x<$thumbLimit; $x++)
	{
		$aPhoto=$masterList[$photoIDs[$x]];
		$thumb="<table>";
		$thumb.="<tr><td align=\"center\"><img ";
		$thumb.="src=\"".$aPhoto["ThumbPath"]."\"></td></tr>";
		$thumb.="<tr><td align=\"center\"><small>";
		$thumb.=$aPhoto["date"]."<br>".$aPhoto["caption"]."</small></td></tr>";
		$thumb.="</table>";
		if($x % $thumbsPerRow == 0)
			echo "\n<!--New row-->\n<tr><td>\n".$thumb."\n</td>\n";
		else if($x % $thumbsPerRow == ($thumbsPerRow-1))
			echo "\n<td>\n".$thumb."\n</td></tr>\n<!--End row-->\n";
		else
			echo "\n<td>\n".$thumb."\n</td>\n";
	}
	echo "\n</table>\n";
	?>

Running the Hack

The last few lines of iphoto_parse.php contain hardcoded paths to the AlbumData.xml file, as well as to the output files (as does iphoto_display.php), so be sure that you enter the correct paths. Then, simply load up iphoto_parse.php in your web browser. Also, note that PHP will need to have permission to write to the output files; otherwise, you’ll get no output.

Your web browser will indicate when the script has finished executing with a page that says, “Done parsing.” Open the output files, and you should see an array in each, similar to the following samples.

out_albums.php will look something like this:

	<?php
	$albumList = array (
		"Library" =>
			array(
				4425,
				4423,
	…
				3796,
				3794,
				3792
			)
	);
	?>

And out_images.php will look something like this:

	<?php
	//key=photo ID, value={[0]caption, [1]date, [2]image path, [3]thumb path}
	$masterList = array (
	"13"=>array(
	"caption"=>"The wreath, out of focus again",
	"date"=>"December 23, 2002, 2:59 am",
	"ImagePath"=>"/~mike/myPhoto/iPhoto Library/2002/12/22/DSC00151.JPG",
	"ThumbPath"=>"/~mike/myPhoto/iPhoto Library/2002/12/22/Thumbs/13.jpg"),
	…
	);
	?>

You can also examine some of the resulting output visually by loading up iphoto_display.php in your web browser, as shown in Figure 4-15.

While XML is a versatile format, considering how verbose the AlbumData.xml file is and how large it can get for photo libraries of even moderate size, it needs to be massaged. After all, I have only 2,868 photos in my library, but my AlbumData.xml file is 2.4 MB. I thus chose to employ the XML parser included with PHP 4 (expat) to parse AlbumData.xml into meaningful components, which I then output using a much simpler format. Specifically, the output is piped into two separate files containing the data of interest represented as PHP arrays.

iPhoto wedding photos in my browser
Figure 4-15. iPhoto wedding photos in my browser

The core idea for the parser is to use a string representing the hierarchy of tags so that we have some context as we walk through the file’s content. It’s sort of like a stack that is represented as a string rather than as the more common array or linked list. Note that this parser parses only some of the elements of the albums section, as well as the images section of AlbumData.xml. I’ve also included a demonstration as to how you can work with the resulting output of this parser.

Before writing any code, it’s probably a good idea to decide how to serve your photos. For instance, by default, Mac OS X will not allow Apache (and therefore, PHP) access to ~/Pictures/ where iPhoto data is stored, so you need to get your permissions straight. You can approach this in a number of ways:

  • Modify your /etc/httpd/httpd.conf file.

  • Use a symbolic link.

  • Quit iPhoto, move your iPhoto Library folder into your ~/Sites/ folder, relaunch iPhoto, and when it panics that all the photos are gone, point it to the new location of the Library folder.

  • Upload your iPhoto Library folder to some other machine using FTP, rsync, or any other file-transfer program that floats your boat.

Hacking the Hack

You have a lot of room to work with this hack:

  • Add further cases to the XML parser so that it extracts all of the data that you’re interested in, rather than just the albums and the images that they contain.

  • Instead of outputting the processed AlbumData.xml file into a flat text file, store the information in an SQL database or some other, more versatile format.

  • If you’re going to be this user friendly by getting all of the information out of iPhoto, why not go the extra mile and make this entire process automatic? Automating this process is actually very simple. At this point, we have a means for parsing the XML file as well as a means for caching what we discover from parsing the XML file. The final step calls for knowing when we should be using the cache and when we should be rebuilding the cache. The answer to this question depends on your application, but here are some possibilities worth considering:

    • Run a cron job that invokes your cache rebuild function hourly/daily/whenever.

    • Keep track of the modification date of AlbumData.xml. If that date is newer than the last time you parsed it, reparse.

So, for example, using the latter approach, add a function that looks something like this:

	//returns a boolean value indicating whether or not
	//a cache rebuild (reparse) is necessary
	function needToUpdateCache()
	{
		global $cacheTime, $albumFile, $err;

		$cacheTimeFile="lastCacheTime.txt"; //text file where
				//a string indicates
				//last cache rebuild time.
				//i.e. "January 28 2005 16:31:26."
		$compareFile="iPhoto Library/AlbumData.xml";
		if (file_exists($cacheTimeFile))
		{
			//first, check the file where the last known cached time was stored
			if($fp = fopen($cacheTimeFile, "r"))
			{
				$lastTime = fread($fp, filesize($cacheTimeFile));
				fclose($fp);
			}
			else
			{
				$err.= "Can't read last cache time";
				return true;
			}

			//now, determine the last time the iPhoto data has changed
			//if we need to reparse, it will write the
			//current time into $cacheTimeFile
			//(since we will therefore reparse now)
			if($lastTime!=date ("F d Y H:i:s.", filemtime($compareFile)))
			{
				if (!$fp = fopen($cacheTimeFile, 'w'))
				{
				$err.= "Can't open file: $cacheTimeFile";
				}
				else
				{
				if (!fwrite($fp, date ("F d Y H:i:s.", filemtime($compareFile)) )) 
				$err.= "Can't open file: $cacheTimeFile";
				fclose($fp);
				}
				return true;
				}
				else
				return false;
				}
				else
				{
				$err.= "Can't find file: $cacheTimeFile"; 
				return true;
				}
			}

			//and at the beginning of every page load, call this to ensure
			//viewers are getting the latest photos
			if(needToUpdateCache())
			parseAlbumXML($pathToYourAlbumXMLFile);

This will ensure that you parse the file only when changes have been made in iPhoto that will require a reparse.

Michael Mulligan

See Also

Get PHP Hacks now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.